├── .gitignore ├── LICENSE.md ├── README.md ├── chrome_remote_interface ├── __init__.py ├── basic_addons.py ├── library.py └── protocol.json ├── example.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Mitin Svyatoslav 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software and person who used, copied, 12 | modified, merged, published, distributed, sublicensed, and/or sold copies of the 13 | Software must to make twenty naked selfies every month and send it to author 14 | of software every month or pay million dollars to author every month. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | chrome-remote-interface-python 2 | ======================= 3 | 4 | [Chrome Debugging Protocol] interface that helps to instrument Chrome (or any 5 | other suitable [implementation](#implementations)) by providing a simple 6 | abstraction of commands and notifications using a straightforward Python 7 | API. 8 | 9 | This module is one of the many [third-party protocol clients][3rd-party]. 10 | 11 | It is only for Python 3.5 for now 12 | 13 | [3rd-party]: https://developer.chrome.com/devtools/docs/debugging-clients#chrome-remote-interface 14 | 15 | Sample API usage 16 | ---------------- 17 | 18 | The following snippet loads `https://github.com` and prints every response body length: 19 | 20 | ```python 21 | import asyncio 22 | import chrome_remote_interface 23 | 24 | if __name__ == '__main__': 25 | class callbacks: 26 | async def start(tabs): 27 | await tabs.add() 28 | async def tab_start(tabs, tab): 29 | await tab.Page.enable() 30 | await tab.Network.enable() 31 | await tab.Page.navigate(url='http://github.com') 32 | async def network__loading_finished(tabs, tab, requestId, **kwargs): 33 | try: 34 | body = tabs.helpers.old_helpers.unpack_response_body(await tab.Network.get_response_body(requestId=requestId)) 35 | print('body length:', len(body)) 36 | except tabs.FailResponse as e: 37 | print('fail:', e) 38 | async def page__frame_stopped_loading(tabs, tab, **kwargs): 39 | print('finish') 40 | tabs.terminate() 41 | async def any(tabs, tab, callback_name, parameters): 42 | pass 43 | # print('Unknown event fired', callback_name) 44 | 45 | asyncio.get_event_loop().run_until_complete(chrome_remote_interface.Tabs.run('localhost', 9222, callbacks)) 46 | ``` 47 | 48 | We use these types of callbacks: 49 | * ```start(tabs)``` - fired on the start. 50 | * ```tab_start(tabs, tab, manual)``` - fired on tab create. 51 | * ```network__response_received(tabs, tab, **kwargs)``` - callback for chrome [Network.responseReceived](https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-responseReceived) event. 52 | * ```any(tabs, tab, callback_name, parameters)``` - fallback which fired when there is no callback found. 53 | * ```tab_close(tabs, tab)``` - fired when tab is closed 54 | * ```tab_suicide(tabs, tab)``` - fired when tab is closed without your wish (and socket too) 55 | * ```close(tabs)``` - fired when all tabs are closed 56 | 57 | We can add tab using method ```tabs.add()``` and remove it with ```tabs[n].remove()``` or ```tab.remove()```. 58 | 59 | Each method can throw ```FailReponse``` exception when something goes wrong. 60 | 61 | You can terminate your programm by calling ```tabs.terminate()```. 62 | 63 | Installation 64 | ------------ 65 | 66 | ```bash 67 | git clone https://github.com/wasiher/chrome-remote-interface-python.git 68 | python3 setup.py install 69 | ``` 70 | 71 | Setup (all description from [here](https://github.com/cyrus-and/chrome-remote-interface)) 72 | ----- 73 | 74 | An instance of either Chrome itself or another implementation needs to be 75 | running on a known port in order to use this module (defaults to 76 | `localhost:9222`). 77 | 78 | ### Chrome/Chromium 79 | 80 | #### Desktop 81 | 82 | Start Chrome with the `--remote-debugging-port` option, for example: 83 | 84 | google-chrome --remote-debugging-port=9222 85 | 86 | ##### Headless 87 | 88 | Since version 57, additionally use the `--headless` option, for example: 89 | 90 | google-chrome --headless --remote-debugging-port=9222 91 | 92 | Please note that currently the *DevTools* methods are not properly supported in 93 | headless mode; use the [Target domain] instead. See [#83] and [#84] for more 94 | information. 95 | 96 | [#83]: https://github.com/cyrus-and/chrome-remote-interface/issues/83 97 | [#84]: https://github.com/cyrus-and/chrome-remote-interface/issues/84 98 | [Target domain]: https://chromedevtools.github.io/debugger-protocol-viewer/tot/Target/ 99 | 100 | #### Android 101 | 102 | Plug the device and enable the [port forwarding][adb], for example: 103 | 104 | adb forward tcp:9222 localabstract:chrome_devtools_remote 105 | 106 | [adb]: https://developer.chrome.com/devtools/docs/remote-debugging-legacy 107 | 108 | ##### WebView 109 | 110 | In order to be inspectable, a WebView must 111 | be [configured for debugging][webview] and the corresponding process ID must be 112 | known. There are several ways to obtain it, for example: 113 | 114 | adb shell grep -a webview_devtools_remote /proc/net/unix 115 | 116 | Finally, port forwarding can be enabled as follows: 117 | 118 | adb forward tcp:9222 localabstract:webview_devtools_remote_ 119 | 120 | [webview]: https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews#configure_webviews_for_debugging 121 | 122 | ### Edge 123 | 124 | Install and run the [Edge Diagnostics Adapter][edge-adapter]. 125 | 126 | [edge-adapter]: https://github.com/Microsoft/edge-diagnostics-adapter 127 | 128 | ### Node.js 129 | 130 | Start Node.js with the `--inspect` option, for example: 131 | 132 | node --inspect=9222 script.js 133 | 134 | ### Safari (iOS) 135 | 136 | Install and run the [iOS WebKit Debug Proxy][iwdp]. 137 | 138 | [iwdp]: https://github.com/google/ios-webkit-debug-proxy 139 | 140 | Chrome Debugging Protocol versions 141 | ---------------------------------- 142 | 143 | You can update it using this way (It will be downloaded automatically first time) 144 | 145 | ```python 146 | import chrome_remote_interface 147 | chrome_remote_interface.Protocol.update_protocol() 148 | ``` 149 | 150 | Protocols are loaded from [here](https://chromium.googlesource.com/chromium/src/+/master/third_party/WebKit/Source/core/inspector/browser_protocol.json) and [here](https://chromium.googlesource.com/chromium/src/+/master/third_party/WebKit/Source/core/inspector/browser_protocol.json) 151 | 152 | 153 | Contributors 154 | ------------ 155 | 156 | - [Me](https://github.com/wasiher) 157 | 158 | Resources 159 | --------- 160 | 161 | - [Chrome Debugging Protocol] 162 | - [Chrome Debugging Protocol Viewer](https://chromedevtools.github.io/debugger-protocol-viewer/) 163 | - [Chrome Debugging Protocol Google group](https://groups.google.com/forum/#!forum/chrome-debugging-protocol) 164 | - [devtools-protocol official repo](https://github.com/ChromeDevTools/devtools-protocol) 165 | - [Showcase Chrome Debugging Protocol Clients](https://developer.chrome.com/devtools/docs/debugging-clients) 166 | - [Awesome chrome-devtools](https://github.com/ChromeDevTools/awesome-chrome-devtools) 167 | 168 | [Chrome Debugging Protocol]: https://developer.chrome.com/devtools/docs/debugger-protocol 169 | -------------------------------------------------------------------------------- /chrome_remote_interface/__init__.py: -------------------------------------------------------------------------------- 1 | ''' Client for the Google Chrome browser's remote debugging api. 2 | 3 | Chrome Debugging Protocol interface that helps to instrument Chrome (or any other suitable implementation) by providing a simple abstraction of commands and notifications using a straightforward Python API. 4 | This module is one of the many third-party protocol clients. 5 | ''' 6 | 7 | 8 | from .library import ( 9 | Protocol, 10 | FailResponse, 11 | API, 12 | TabsSync, 13 | SocketClientSync, 14 | Tabs, 15 | SocketClient 16 | ) -------------------------------------------------------------------------------- /chrome_remote_interface/basic_addons.py: -------------------------------------------------------------------------------- 1 | import pyperclip, asyncio, json, base64 2 | 3 | class targets: 4 | ''' 5 | Used to handle tabs which opened without our wish 6 | ''' 7 | class events: 8 | async def tab_start(tabs, tab): 9 | await tab.Target.set_discover_targets(True) 10 | # await tab.Page.set_auto_attach_to_created_pages(True) 11 | await tab.Inspector.enable() 12 | async def target__target_created(tabs, tab, targetInfo, **kwargs): 13 | if targetInfo.type != 'browser' and targetInfo.targetId not in tabs._initial_tabs and targetInfo.targetId not in tabs._tabs: 14 | try: 15 | tab = await tab.__class__(tabs._host, tabs._port, tabs, targetInfo.targetId).__aenter__() 16 | tab.manual = False 17 | tabs._tabs[tab.id] = tab 18 | await tab.Runtime.run_if_waiting_for_debugger() 19 | tab._emit_event('tab_start') 20 | except (KeyError, ValueError, tabs.ConnectionClosed, tabs.InvalidHandshake): 21 | pass 22 | async def inspector__detached(tabs, tab, reason): 23 | await tab.close(force=True) 24 | tab._emit_event('tab_suicide', reason=reason) 25 | async def inspector__target_crashed(tabs, tab): 26 | await tab.close(force=True) 27 | tab._emit_event('tab_suicide', reason='target_crashed') 28 | 29 | class KeysTuple: 30 | def __init__(self, code, key, text, unmodified_text, windows_virtual_key_code, native_virtual_key_code, is_system_key, is_keypad): 31 | self.code = code 32 | self.key = key 33 | self.text = text 34 | self.unmodified_text = unmodified_text 35 | self.key_code = native_virtual_key_code 36 | self.windows_virtual_key_code = windows_virtual_key_code 37 | self.native_virtual_key_code = native_virtual_key_code 38 | self.is_system_key = is_system_key 39 | self.is_keypad = is_keypad 40 | def __repr__(self): 41 | return 'KeysTuple({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7})'.format( 42 | self.code, self.key, self.text, self.unmodified_text, self.windows_virtual_key_code, 43 | self.native_virtual_key_code, self.is_keypad, self.is_system_key 44 | ) 45 | class keyboard: 46 | ''' 47 | Basic example of key input. 48 | Only basic chars right now :( 49 | ''' 50 | class macros: 51 | async def Input__press_key(tabs, tab, key, modifiers=None): 52 | if modifiers is None: 53 | modifiers = tabs.helpers.keyboard.modifiers() 54 | button = tabs.helpers.keyboard.buttons[key] 55 | await tab.Input.dispatch_key_event(type='keyDown', timestamp=tab.timestamp(), code=button.code, windowsVirtualKeyCode=button.windows_virtual_key_code, nativeVirtualKeyCode=button.native_virtual_key_code, key=button.key, autoRepeat=False, isKeypad=button.is_keypad, isSystemKey=button.is_system_key, modifiers=modifiers) 56 | if button.text is not None: 57 | await tab.Input.dispatch_key_event(type='char', timestamp=tab.timestamp(), code=button.code, windowsVirtualKeyCode=button.windows_virtual_key_code, nativeVirtualKeyCode=button.native_virtual_key_code, key=button.key, autoRepeat=False, isKeypad=button.is_keypad, isSystemKey=button.is_system_key, modifiers=modifiers, text=button.text, unmodifiedText=button.unmodified_text) 58 | await tab.Input.dispatch_key_event(type='keyUp', timestamp=tab.timestamp(), code=button.code, windowsVirtualKeyCode=button.windows_virtual_key_code, nativeVirtualKeyCode=button.native_virtual_key_code, key=button.key, autoRepeat=False, isKeypad=button.is_keypad, isSystemKey=button.is_system_key, modifiers=modifiers) 59 | async def Input__send_keys(tabs, tab, keys): 60 | for key in keys: 61 | await Input.press_key(key) 62 | async def Input__paste(tabs, tab, data): 63 | try: 64 | backup = pyperclip.determine_clipboard()[1]() # Not sure if is correct D: 65 | except: 66 | backup = None 67 | pyperclip.copy(data) 68 | await tab.Input.press_key(tabs.helpers.keyboard.PASTE) 69 | if backup is not None: 70 | pyperclip.copy(backup) 71 | class helpers: 72 | buttons = { 73 | '\b': KeysTuple('Backspace', 'Backspace', None, None, 8, 8, False, False), 74 | '\t': KeysTuple('Tab', 'Tab', None, None, 9, 9, False, False), 75 | '\r': KeysTuple('Enter', 'Enter', '\r', '\r', 13, 13, False, True), 76 | '\u001b': KeysTuple('Escape', 'Escape', None, None, 27, 27, False, False), 77 | ' ': KeysTuple('Space', ' ', ' ', ' ', 32, 32, False, True), 78 | '!': KeysTuple('Digit1', '!', '!', '1', 49, 49, True, True), 79 | '\'': KeysTuple('Quote', '\'', '\'', '\'', 222, 222, True, True), 80 | '#': KeysTuple('Digit3', '#', '#', '3', 51, 51, True, True), 81 | '$': KeysTuple('Digit4', '$', '$', '4', 52, 52, True, True), 82 | '%': KeysTuple('Digit5', '%', '%', '5', 53, 53, True, True), 83 | '&': KeysTuple('Digit7', '&', '&', '7', 55, 55, True, True), 84 | '\'': KeysTuple('Quote', '\'', '\'', '\'', 222, 222, False, True), 85 | '(': KeysTuple('Digit9', '(', '(', '9', 57, 57, True, True), 86 | '),': KeysTuple('Digit0', '),', '),', '0', 48, 48, True, True), 87 | '*': KeysTuple('Digit8', '*', '*', '8', 56, 56, True, True), 88 | '+': KeysTuple('Equal', '+', '+', '=', 187, 187, True, True), 89 | ',': KeysTuple('Comma', ',', ',', ',', 188, 188, False, True), 90 | '-': KeysTuple('Minus', '-', '-', '-', 189, 189, False, True), 91 | '.': KeysTuple('Period', '.', '.', '.', 190, 190, False, True), 92 | '/': KeysTuple('Slash', '/', '/', '/', 191, 191, False, True), 93 | '0': KeysTuple('Digit0', '0', '0', '0', 48, 48, False, True), 94 | '1': KeysTuple('Digit1', '1', '1', '1', 49, 49, False, True), 95 | '2': KeysTuple('Digit2', '2', '2', '2', 50, 50, False, True), 96 | '3': KeysTuple('Digit3', '3', '3', '3', 51, 51, False, True), 97 | '4': KeysTuple('Digit4', '4', '4', '4', 52, 52, False, True), 98 | '5': KeysTuple('Digit5', '5', '5', '5', 53, 53, False, True), 99 | '6': KeysTuple('Digit6', '6', '6', '6', 54, 54, False, True), 100 | '7': KeysTuple('Digit7', '7', '7', '7', 55, 55, False, True), 101 | '8': KeysTuple('Digit8', '8', '8', '8', 56, 56, False, True), 102 | '9': KeysTuple('Digit9', '9', '9', '9', 57, 57, False, True), 103 | ':': KeysTuple('Semicolon', ':', ':', ';', 186, 186, True, True), 104 | ';': KeysTuple('Semicolon', ';', ';', ';', 186, 186, False, True), 105 | '<': KeysTuple('Comma', '<', '<', ',', 188, 188, True, True), 106 | '=': KeysTuple('Equal', '=', '=', '=', 187, 187, False, True), 107 | '>': KeysTuple('Period', '>', '>', '.', 190, 190, True, True), 108 | '?': KeysTuple('Slash', '?', '?', '/', 191, 191, True, True), 109 | '@': KeysTuple('Digit2', '@', '@', '2', 50, 50, True, True), 110 | 'A': KeysTuple('KeyA', 'A', 'A', 'a', 65, 65, True, True), 111 | 'B': KeysTuple('KeyB', 'B', 'B', 'b', 66, 66, True, True), 112 | 'C': KeysTuple('KeyC', 'C', 'C', 'c', 67, 67, True, True), 113 | 'D': KeysTuple('KeyD', 'D', 'D', 'd', 68, 68, True, True), 114 | 'E': KeysTuple('KeyE', 'E', 'E', 'e', 69, 69, True, True), 115 | 'F': KeysTuple('KeyF', 'F', 'F', 'f', 70, 70, True, True), 116 | 'G': KeysTuple('KeyG', 'G', 'G', 'g', 71, 71, True, True), 117 | 'H': KeysTuple('KeyH', 'H', 'H', 'h', 72, 72, True, True), 118 | 'I': KeysTuple('KeyI', 'I', 'I', 'i', 73, 73, True, True), 119 | 'J': KeysTuple('KeyJ', 'J', 'J', 'j', 74, 74, True, True), 120 | 'K': KeysTuple('KeyK', 'K', 'K', 'k', 75, 75, True, True), 121 | 'L': KeysTuple('KeyL', 'L', 'L', 'l', 76, 76, True, True), 122 | 'M': KeysTuple('KeyM', 'M', 'M', 'm', 77, 77, True, True), 123 | 'N': KeysTuple('KeyN', 'N', 'N', 'n', 78, 78, True, True), 124 | 'O': KeysTuple('KeyO', 'O', 'O', 'o', 79, 79, True, True), 125 | 'P': KeysTuple('KeyP', 'P', 'P', 'p', 80, 80, True, True), 126 | 'Q': KeysTuple('KeyQ', 'Q', 'Q', 'q', 81, 81, True, True), 127 | 'R': KeysTuple('KeyR', 'R', 'R', 'r', 82, 82, True, True), 128 | 'S': KeysTuple('KeyS', 'S', 'S', 's', 83, 83, True, True), 129 | 'T': KeysTuple('KeyT', 'T', 'T', 't', 84, 84, True, True), 130 | 'U': KeysTuple('KeyU', 'U', 'U', 'u', 85, 85, True, True), 131 | 'V': KeysTuple('KeyV', 'V', 'V', 'v', 86, 86, True, True), 132 | 'W': KeysTuple('KeyW', 'W', 'W', 'w', 87, 87, True, True), 133 | 'X': KeysTuple('KeyX', 'X', 'X', 'x', 88, 88, True, True), 134 | 'Y': KeysTuple('KeyY', 'Y', 'Y', 'y', 89, 89, True, True), 135 | 'Z': KeysTuple('KeyZ', 'Z', 'Z', 'z', 90, 90, True, True), 136 | '[': KeysTuple('BracketLeft', '[', '[', '[', 219, 219, False, True), 137 | '\\': KeysTuple('Backslash', '\\', '\\', '\\', 220, 220, False, True), 138 | ']': KeysTuple('BracketRight', ']', ']', ']', 221, 221, False, True), 139 | '^': KeysTuple('Digit6', '^', '^', '6', 54, 54, True, True), 140 | '_': KeysTuple('Minus', '_', '_', '-', 189, 189, True, True), 141 | '`': KeysTuple('Backquote', '`', '`', '`', 192, 192, False, True), 142 | 'a': KeysTuple('KeyA', 'a', 'a', 'a', 65, 65, False, True), 143 | 'b': KeysTuple('KeyB', 'b', 'b', 'b', 66, 66, False, True), 144 | 'c': KeysTuple('KeyC', 'c', 'c', 'c', 67, 67, False, True), 145 | 'd': KeysTuple('KeyD', 'd', 'd', 'd', 68, 68, False, True), 146 | 'e': KeysTuple('KeyE', 'e', 'e', 'e', 69, 69, False, True), 147 | 'f': KeysTuple('KeyF', 'f', 'f', 'f', 70, 70, False, True), 148 | 'g': KeysTuple('KeyG', 'g', 'g', 'g', 71, 71, False, True), 149 | 'h': KeysTuple('KeyH', 'h', 'h', 'h', 72, 72, False, True), 150 | 'i': KeysTuple('KeyI', 'i', 'i', 'i', 73, 73, False, True), 151 | 'j': KeysTuple('KeyJ', 'j', 'j', 'j', 74, 74, False, True), 152 | 'k': KeysTuple('KeyK', 'k', 'k', 'k', 75, 75, False, True), 153 | 'l': KeysTuple('KeyL', 'l', 'l', 'l', 76, 76, False, True), 154 | 'm': KeysTuple('KeyM', 'm', 'm', 'm', 77, 77, False, True), 155 | 'n': KeysTuple('KeyN', 'n', 'n', 'n', 78, 78, False, True), 156 | 'o': KeysTuple('KeyO', 'o', 'o', 'o', 79, 79, False, True), 157 | 'p': KeysTuple('KeyP', 'p', 'p', 'p', 80, 80, False, True), 158 | 'q': KeysTuple('KeyQ', 'q', 'q', 'q', 81, 81, False, True), 159 | 'r': KeysTuple('KeyR', 'r', 'r', 'r', 82, 82, False, True), 160 | 's': KeysTuple('KeyS', 's', 's', 's', 83, 83, False, True), 161 | 't': KeysTuple('KeyT', 't', 't', 't', 84, 84, False, True), 162 | 'u': KeysTuple('KeyU', 'u', 'u', 'u', 85, 85, False, True), 163 | 'v': KeysTuple('KeyV', 'v', 'v', 'v', 86, 86, False, True), 164 | 'w': KeysTuple('KeyW', 'w', 'w', 'w', 87, 87, False, True), 165 | 'x': KeysTuple('KeyX', 'x', 'x', 'x', 88, 88, False, True), 166 | 'y': KeysTuple('KeyY', 'y', 'y', 'y', 89, 89, False, True), 167 | 'z': KeysTuple('KeyZ', 'z', 'z', 'z', 90, 90, False, True), 168 | '[': KeysTuple('BracketLeft', '[', '[', '[', 219, 219, False, True), 169 | '\\': KeysTuple('Backslash', '\\', '\\', '\\', 220, 220, False, True), 170 | ']': KeysTuple('BracketRight', ']', ']', ']', 221, 221, False, True), 171 | '~': KeysTuple('Backquote', '~', '~', '`', 192, 192, True, True), 172 | '\u007f': KeysTuple('Delete', 'Delete', None, None, 46, 46, False, False), 173 | '¥': KeysTuple('IntlYen', '¥', '¥', '¥', 220, 220, False, True), 174 | '\u0102': KeysTuple('AltLeft', 'Alt', None, None, 164, 164, False, False), 175 | '\u0104': KeysTuple('CapsLock', 'CapsLock', None, None, 20, 20, False, False), 176 | '\u0105': KeysTuple('ControlLeft', 'Control', None, None, 162, 162, False, False), 177 | '\u0106': KeysTuple('Fn', 'Fn', None, None, 0, 0, False, False), 178 | '\u0107': KeysTuple('FnLock', 'FnLock', None, None, 0, 0, False, False), 179 | '\u0108': KeysTuple('Hyper', 'Hyper', None, None, 0, 0, False, False), 180 | '\u0109': KeysTuple('MetaLeft', 'Meta', None, None, 91, 91, False, False), 181 | '\u010a': KeysTuple('NumLock', 'NumLock', None, None, 144, 144, False, False), 182 | '\u010c': KeysTuple('ScrollLock', 'ScrollLock', None, None, 145, 145, False, False), 183 | '\u010d': KeysTuple('ShiftLeft', 'Shift', None, None, 160, 160, False, False), 184 | '\u010e': KeysTuple('Super', 'Super', None, None, 0, 0, False, False), 185 | '\u0301': KeysTuple('ArrowDown', 'ArrowDown', None, None, 40, 40, False, False), 186 | '\u0302': KeysTuple('ArrowLeft', 'ArrowLeft', None, None, 37, 37, False, False), 187 | '\u0303': KeysTuple('ArrowRight', 'ArrowRight', None, None, 39, 39, False, False), 188 | '\u0304': KeysTuple('ArrowUp', 'ArrowUp', None, None, 38, 38, False, False), 189 | '\u0305': KeysTuple('End', 'End', None, None, 35, 35, False, False), 190 | '\u0306': KeysTuple('Home', 'Home', None, None, 36, 36, False, False), 191 | '\u0307': KeysTuple('PageDown', 'PageDown', None, None, 34, 34, False, False), 192 | '\u0308': KeysTuple('PageUp', 'PageUp', None, None, 33, 33, False, False), 193 | '\u0401': KeysTuple('NumpadClear', 'Clear', None, None, 12, 12, False, False), 194 | '\u0402': KeysTuple('Copy', 'Copy', None, None, 0, 0, False, False), 195 | '\u0404': KeysTuple('Cut', 'Cut', None, None, 0, 0, False, False), 196 | '\u0407': KeysTuple('Insert', 'Insert', None, None, 45, 45, False, False), 197 | '\u0408': KeysTuple('Paste', 'Paste', None, None, 0, 0, False, False), 198 | '\u0409': KeysTuple('Redo', 'Redo', None, None, 0, 0, False, False), 199 | '\u040a': KeysTuple('Undo', 'Undo', None, None, 0, 0, False, False), 200 | '\u0502': KeysTuple('Again', 'Again', None, None, 0, 0, False, False), 201 | '\u0504': KeysTuple('Abort', 'Cancel', None, None, 0, 0, False, False), 202 | '\u0505': KeysTuple('ContextMenu', 'ContextMenu', None, None, 93, 93, False, False), 203 | '\u0507': KeysTuple('Find', 'Find', None, None, 0, 0, False, False), 204 | '\u0508': KeysTuple('Help', 'Help', None, None, 47, 47, False, False), 205 | '\u0509': KeysTuple('Pause', 'Pause', None, None, 19, 19, False, False), 206 | '\u050b': KeysTuple('Props', 'Props', None, None, 0, 0, False, False), 207 | '\u050c': KeysTuple('Select', 'Select', None, None, 41, 41, False, False), 208 | '\u050d': KeysTuple('ZoomIn', 'ZoomIn', None, None, 0, 0, False, False), 209 | '\u050e': KeysTuple('ZoomOut', 'ZoomOut', None, None, 0, 0, False, False), 210 | '\u0601': KeysTuple('BrightnessDown', 'BrightnessDown', None, None, 216, 0, False, False), 211 | '\u0602': KeysTuple('BrightnessUp', 'BrightnessUp', None, None, 217, 0, False, False), 212 | '\u0604': KeysTuple('Eject', 'Eject', None, None, 0, 0, False, False), 213 | '\u0605': KeysTuple('LogOff', 'LogOff', None, None, 0, 0, False, False), 214 | '\u0606': KeysTuple('Power', 'Power', None, None, 152, 0, False, False), 215 | '\u0608': KeysTuple('PrintScreen', 'PrintScreen', None, None, 44, 44, False, False), 216 | '\u060b': KeysTuple('WakeUp', 'WakeUp', None, None, 0, 0, False, False), 217 | '\u0705': KeysTuple('Convert', 'Convert', None, None, 28, 28, False, False), 218 | '\u070d': KeysTuple('NonConvert', 'NonConvert', None, None, 29, 29, False, False), 219 | '\u0711': KeysTuple('Lang1', 'HangulMode', None, None, 21, 21, False, False), 220 | '\u0712': KeysTuple('Lang2', 'HanjaMode', None, None, 25, 25, False, False), 221 | '\u0716': KeysTuple('Lang4', 'Hiragana', None, None, 0, 0, False, False), 222 | '\u0718': KeysTuple('KanaMode', 'KanaMode', None, None, 21, 21, False, False), 223 | '\u071a': KeysTuple('Lang3', 'Katakana', None, None, 0, 0, False, False), 224 | '\u071d': KeysTuple('Lang5', 'ZenkakuHankaku', None, None, 0, 0, False, False), 225 | '\u0801': KeysTuple('F1', 'F1', None, None, 112, 112, False, False), 226 | '\u0802': KeysTuple('F2', 'F2', None, None, 113, 113, False, False), 227 | '\u0803': KeysTuple('F3', 'F3', None, None, 114, 114, False, False), 228 | '\u0804': KeysTuple('F4', 'F4', None, None, 115, 115, False, False), 229 | '\u0805': KeysTuple('F5', 'F5', None, None, 116, 116, False, False), 230 | '\u0806': KeysTuple('F6', 'F6', None, None, 117, 117, False, False), 231 | '\u0807': KeysTuple('F7', 'F7', None, None, 118, 118, False, False), 232 | '\u0808': KeysTuple('F8', 'F8', None, None, 119, 119, False, False), 233 | '\u0809': KeysTuple('F9', 'F9', None, None, 120, 120, False, False), 234 | '\u080a': KeysTuple('F10', 'F10', None, None, 121, 121, False, False), 235 | '\u080b': KeysTuple('F11', 'F11', None, None, 122, 122, False, False), 236 | '\u080c': KeysTuple('F12', 'F12', None, None, 123, 123, False, False), 237 | '\u080d': KeysTuple('F13', 'F13', None, None, 124, 124, False, False), 238 | '\u080e': KeysTuple('F14', 'F14', None, None, 125, 125, False, False), 239 | '\u080f': KeysTuple('F15', 'F15', None, None, 126, 126, False, False), 240 | '\u0810': KeysTuple('F16', 'F16', None, None, 127, 127, False, False), 241 | '\u0811': KeysTuple('F17', 'F17', None, None, 128, 128, False, False), 242 | '\u0812': KeysTuple('F18', 'F18', None, None, 129, 129, False, False), 243 | '\u0813': KeysTuple('F19', 'F19', None, None, 130, 130, False, False), 244 | '\u0814': KeysTuple('F20', 'F20', None, None, 131, 131, False, False), 245 | '\u0815': KeysTuple('F21', 'F21', None, None, 132, 132, False, False), 246 | '\u0816': KeysTuple('F22', 'F22', None, None, 133, 133, False, False), 247 | '\u0817': KeysTuple('F23', 'F23', None, None, 134, 134, False, False), 248 | '\u0818': KeysTuple('F24', 'F24', None, None, 135, 135, False, False), 249 | '\u0a01': KeysTuple('Close', 'Close', None, None, 0, 0, False, False), 250 | '\u0a02': KeysTuple('MailForward', 'MailForward', None, None, 0, 0, False, False), 251 | '\u0a03': KeysTuple('MailReply', 'MailReply', None, None, 0, 0, False, False), 252 | '\u0a04': KeysTuple('MailSend', 'MailSend', None, None, 0, 0, False, False), 253 | '\u0a05': KeysTuple('MediaPlayPause', 'MediaPlayPause', None, None, 179, 179, False, False), 254 | '\u0a07': KeysTuple('MediaStop', 'MediaStop', None, None, 178, 178, False, False), 255 | '\u0a08': KeysTuple('MediaTrackNext', 'MediaTrackNext', None, None, 176, 176, False, False), 256 | '\u0a09': KeysTuple('MediaTrackPrevious', 'MediaTrackPrevious', None, None, 177, 177, False, False), 257 | '\u0a0a': KeysTuple('New', 'New', None, None, 0, 0, False, False), 258 | '\u0a0b': KeysTuple('Open', 'Open', None, None, 43, 43, False, False), 259 | '\u0a0c': KeysTuple('Print', 'Print', None, None, 0, 0, False, False), 260 | '\u0a0d': KeysTuple('Save', 'Save', None, None, 0, 0, False, False), 261 | '\u0a0e': KeysTuple('SpellCheck', 'SpellCheck', None, None, 0, 0, False, False), 262 | '\u0a0f': KeysTuple('AudioVolumeDown', 'AudioVolumeDown', None, None, 174, 174, False, False), 263 | '\u0a10': KeysTuple('AudioVolumeUp', 'AudioVolumeUp', None, None, 175, 175, False, False), 264 | '\u0a11': KeysTuple('AudioVolumeMute', 'AudioVolumeMute', None, None, 173, 173, False, False), 265 | '\u0b01': KeysTuple('LaunchApp2', 'LaunchApplication2', None, None, 183, 183, False, False), 266 | '\u0b02': KeysTuple('LaunchCalendar', 'LaunchCalendar', None, None, 0, 0, False, False), 267 | '\u0b03': KeysTuple('LaunchMail', 'LaunchMail', None, None, 180, 180, False, False), 268 | '\u0b04': KeysTuple('MediaSelect', 'LaunchMediaPlayer', None, None, 181, 181, False, False), 269 | '\u0b05': KeysTuple('LaunchMusicPlayer', 'LaunchMusicPlayer', None, None, 0, 0, False, False), 270 | '\u0b06': KeysTuple('LaunchApp1', 'LaunchApplication1', None, None, 182, 182, False, False), 271 | '\u0b07': KeysTuple('LaunchScreenSaver', 'LaunchScreenSaver', None, None, 0, 0, False, False), 272 | '\u0b08': KeysTuple('LaunchSpreadsheet', 'LaunchSpreadsheet', None, None, 0, 0, False, False), 273 | '\u0b09': KeysTuple('LaunchWebBrowser', 'LaunchWebBrowser', None, None, 0, 0, False, False), 274 | '\u0b0c': KeysTuple('LaunchContacts', 'LaunchContacts', None, None, 0, 0, False, False), 275 | '\u0b0d': KeysTuple('LaunchPhone', 'LaunchPhone', None, None, 0, 0, False, False), 276 | '\u0c01': KeysTuple('BrowserBack', 'BrowserBack', None, None, 166, 166, False, False), 277 | '\u0c02': KeysTuple('BrowserFavorites', 'BrowserFavorites', None, None, 171, 171, False, False), 278 | '\u0c03': KeysTuple('BrowserForward', 'BrowserForward', None, None, 167, 167, False, False), 279 | '\u0c04': KeysTuple('BrowserHome', 'BrowserHome', None, None, 172, 172, False, False), 280 | '\u0c05': KeysTuple('BrowserRefresh', 'BrowserRefresh', None, None, 168, 168, False, False), 281 | '\u0c06': KeysTuple('BrowserSearch', 'BrowserSearch', None, None, 170, 170, False, False), 282 | '\u0c07': KeysTuple('BrowserStop', 'BrowserStop', None, None, 169, 169, False, False), 283 | '\u0d0a': KeysTuple('ChannelDown', 'ChannelDown', None, None, 0, 0, False, False), 284 | '\u0d0b': KeysTuple('ChannelUp', 'ChannelUp', None, None, 0, 0, False, False), 285 | '\u0d12': KeysTuple('ClosedCaptionToggle', 'ClosedCaptionToggle', None, None, 0, 0, False, False), 286 | '\u0d15': KeysTuple('Exit', 'Exit', None, None, 0, 0, False, False), 287 | '\u0d22': KeysTuple('Guide', 'Guide', None, None, 0, 0, False, False), 288 | '\u0d25': KeysTuple('Info', 'Info', None, None, 0, 0, False, False), 289 | '\u0d2c': KeysTuple('MediaFastForward', 'MediaFastForward', None, None, 0, 0, False, False), 290 | '\u0d2d': KeysTuple('MediaLast', 'MediaLast', None, None, 0, 0, False, False), 291 | '\u0d2f': KeysTuple('MediaPlay', 'MediaPlay', None, None, 0, 0, False, False), 292 | '\u0d30': KeysTuple('MediaRecord', 'MediaRecord', None, None, 0, 0, False, False), 293 | '\u0d31': KeysTuple('MediaRewind', 'MediaRewind', None, None, 0, 0, False, False), 294 | '\u0d43': KeysTuple('Settings', 'Settings', None, None, 0, 0, False, False), 295 | '\u0d4e': KeysTuple('ZoomToggle', 'ZoomToggle', None, None, 251, 251, False, False), 296 | '\u0e02': KeysTuple('AudioBassBoostToggle', 'AudioBassBoostToggle', None, None, 0, 0, False, False), 297 | '\u0f02': KeysTuple('SpeechInputToggle', 'SpeechInputToggle', None, None, 0, 0, False, False), 298 | '\u1001': KeysTuple('SelectTask', 'AppSwitch', None, None, 0, 0, False, False), 299 | } 300 | keyboard.helpers.BACKSPACE = keyboard.helpers.buttons['\b'] 301 | keyboard.helpers.TAB = keyboard.helpers.buttons['\t'] 302 | keyboard.helpers.ENTER = keyboard.helpers.buttons['\r'] 303 | keyboard.helpers.ESCAPE = keyboard.helpers.buttons['\u001b'] 304 | keyboard.helpers.QUOTE = keyboard.helpers.buttons['\''] 305 | keyboard.helpers.BACKSLASH = keyboard.helpers.buttons['\\'] 306 | keyboard.helpers.DELETE = keyboard.helpers.buttons['\u007f'] 307 | keyboard.helpers.ALT = keyboard.helpers.buttons['\u0102'] 308 | keyboard.helpers.CAPS_LOCK = keyboard.helpers.buttons['\u0104'] 309 | keyboard.helpers.CONTROL = keyboard.helpers.buttons['\u0105'] 310 | keyboard.helpers.FN = keyboard.helpers.buttons['\u0106'] 311 | keyboard.helpers.FN_LOCK = keyboard.helpers.buttons['\u0107'] 312 | keyboard.helpers.HYPER = keyboard.helpers.buttons['\u0108'] 313 | keyboard.helpers.META = keyboard.helpers.buttons['\u0109'] 314 | keyboard.helpers.NUM_LOCK = keyboard.helpers.buttons['\u010a'] 315 | keyboard.helpers.SCROLL_LOCK = keyboard.helpers.buttons['\u010c'] 316 | keyboard.helpers.SHIFT = keyboard.helpers.buttons['\u010d'] 317 | keyboard.helpers.SUPER = keyboard.helpers.buttons['\u010e'] 318 | keyboard.helpers.ARROW_DOWN = keyboard.helpers.buttons['\u0301'] 319 | keyboard.helpers.ARROW_LEFT = keyboard.helpers.buttons['\u0302'] 320 | keyboard.helpers.ARROW_RIGHT = keyboard.helpers.buttons['\u0303'] 321 | keyboard.helpers.ARROW_UP = keyboard.helpers.buttons['\u0304'] 322 | keyboard.helpers.END = keyboard.helpers.buttons['\u0305'] 323 | keyboard.helpers.HOME = keyboard.helpers.buttons['\u0306'] 324 | keyboard.helpers.PAGE_DOWN = keyboard.helpers.buttons['\u0307'] 325 | keyboard.helpers.PAGE_UP = keyboard.helpers.buttons['\u0308'] 326 | keyboard.helpers.CLEAR = keyboard.helpers.buttons['\u0401'] 327 | keyboard.helpers.COPY = keyboard.helpers.buttons['\u0402'] 328 | keyboard.helpers.CUT = keyboard.helpers.buttons['\u0404'] 329 | keyboard.helpers.INSERT = keyboard.helpers.buttons['\u0407'] 330 | keyboard.helpers.PASTE = keyboard.helpers.buttons['\u0408'] 331 | keyboard.helpers.REDO = keyboard.helpers.buttons['\u0409'] 332 | keyboard.helpers.UNDO = keyboard.helpers.buttons['\u040a'] 333 | keyboard.helpers.AGAIN = keyboard.helpers.buttons['\u0502'] 334 | keyboard.helpers.CANCEL = keyboard.helpers.buttons['\u0504'] 335 | keyboard.helpers.CONTEXT_MENU = keyboard.helpers.buttons['\u0505'] 336 | keyboard.helpers.FIND = keyboard.helpers.buttons['\u0507'] 337 | keyboard.helpers.HELP = keyboard.helpers.buttons['\u0508'] 338 | keyboard.helpers.PAUSE = keyboard.helpers.buttons['\u0509'] 339 | keyboard.helpers.PROPS = keyboard.helpers.buttons['\u050b'] 340 | keyboard.helpers.SELECT = keyboard.helpers.buttons['\u050c'] 341 | keyboard.helpers.ZOOM_IN = keyboard.helpers.buttons['\u050d'] 342 | keyboard.helpers.ZOOM_OUT = keyboard.helpers.buttons['\u050e'] 343 | keyboard.helpers.BRIGHTNESS_DOWN = keyboard.helpers.buttons['\u0601'] 344 | keyboard.helpers.BRIGHTNESS_UP = keyboard.helpers.buttons['\u0602'] 345 | keyboard.helpers.EJECT = keyboard.helpers.buttons['\u0604'] 346 | keyboard.helpers.LOG_OFF = keyboard.helpers.buttons['\u0605'] 347 | keyboard.helpers.POWER = keyboard.helpers.buttons['\u0606'] 348 | keyboard.helpers.PRINT_SCREEN = keyboard.helpers.buttons['\u0608'] 349 | keyboard.helpers.WAKE_UP = keyboard.helpers.buttons['\u060b'] 350 | keyboard.helpers.CONVERT = keyboard.helpers.buttons['\u0705'] 351 | keyboard.helpers.NON_CONVERT = keyboard.helpers.buttons['\u070d'] 352 | keyboard.helpers.HANGUL_MODE = keyboard.helpers.buttons['\u0711'] 353 | keyboard.helpers.HANJA_MODE = keyboard.helpers.buttons['\u0712'] 354 | keyboard.helpers.HIRAGANA = keyboard.helpers.buttons['\u0716'] 355 | keyboard.helpers.KANA_MODE = keyboard.helpers.buttons['\u0718'] 356 | keyboard.helpers.KATAKANA = keyboard.helpers.buttons['\u071a'] 357 | keyboard.helpers.ZENKAKU_HANKAKU = keyboard.helpers.buttons['\u071d'] 358 | keyboard.helpers.F1 = keyboard.helpers.buttons['\u0801'] 359 | keyboard.helpers.F2 = keyboard.helpers.buttons['\u0802'] 360 | keyboard.helpers.F3 = keyboard.helpers.buttons['\u0803'] 361 | keyboard.helpers.F4 = keyboard.helpers.buttons['\u0804'] 362 | keyboard.helpers.F5 = keyboard.helpers.buttons['\u0805'] 363 | keyboard.helpers.F6 = keyboard.helpers.buttons['\u0806'] 364 | keyboard.helpers.F7 = keyboard.helpers.buttons['\u0807'] 365 | keyboard.helpers.F8 = keyboard.helpers.buttons['\u0808'] 366 | keyboard.helpers.F9 = keyboard.helpers.buttons['\u0809'] 367 | keyboard.helpers.F10 = keyboard.helpers.buttons['\u080a'] 368 | keyboard.helpers.F11 = keyboard.helpers.buttons['\u080b'] 369 | keyboard.helpers.F12 = keyboard.helpers.buttons['\u080c'] 370 | keyboard.helpers.F13 = keyboard.helpers.buttons['\u080d'] 371 | keyboard.helpers.F14 = keyboard.helpers.buttons['\u080e'] 372 | keyboard.helpers.F15 = keyboard.helpers.buttons['\u080f'] 373 | keyboard.helpers.F16 = keyboard.helpers.buttons['\u0810'] 374 | keyboard.helpers.F17 = keyboard.helpers.buttons['\u0811'] 375 | keyboard.helpers.F18 = keyboard.helpers.buttons['\u0812'] 376 | keyboard.helpers.F19 = keyboard.helpers.buttons['\u0813'] 377 | keyboard.helpers.F20 = keyboard.helpers.buttons['\u0814'] 378 | keyboard.helpers.F21 = keyboard.helpers.buttons['\u0815'] 379 | keyboard.helpers.F22 = keyboard.helpers.buttons['\u0816'] 380 | keyboard.helpers.F23 = keyboard.helpers.buttons['\u0817'] 381 | keyboard.helpers.F24 = keyboard.helpers.buttons['\u0818'] 382 | keyboard.helpers.CLOSE = keyboard.helpers.buttons['\u0a01'] 383 | keyboard.helpers.MAIL_FORWARD = keyboard.helpers.buttons['\u0a02'] 384 | keyboard.helpers.MAIL_REPLY = keyboard.helpers.buttons['\u0a03'] 385 | keyboard.helpers.MAIL_SEND = keyboard.helpers.buttons['\u0a04'] 386 | keyboard.helpers.MEDIA_PLAY_PAUSE = keyboard.helpers.buttons['\u0a05'] 387 | keyboard.helpers.MEDIA_STOP = keyboard.helpers.buttons['\u0a07'] 388 | keyboard.helpers.MEDIA_TRACK_NEXT = keyboard.helpers.buttons['\u0a08'] 389 | keyboard.helpers.MEDIA_TRACK_PREVIOUS = keyboard.helpers.buttons['\u0a09'] 390 | keyboard.helpers.NEW = keyboard.helpers.buttons['\u0a0a'] 391 | keyboard.helpers.OPEN = keyboard.helpers.buttons['\u0a0b'] 392 | keyboard.helpers.PRINT = keyboard.helpers.buttons['\u0a0c'] 393 | keyboard.helpers.SAVE = keyboard.helpers.buttons['\u0a0d'] 394 | keyboard.helpers.SPELL_CHECK = keyboard.helpers.buttons['\u0a0e'] 395 | keyboard.helpers.AUDIO_VOLUME_DOWN = keyboard.helpers.buttons['\u0a0f'] 396 | keyboard.helpers.AUDIO_VOLUME_UP = keyboard.helpers.buttons['\u0a10'] 397 | keyboard.helpers.AUDIO_VOLUME_MUTE = keyboard.helpers.buttons['\u0a11'] 398 | keyboard.helpers.LAUMCH_APPLICATION_2 = keyboard.helpers.buttons['\u0b01'] 399 | keyboard.helpers.LAUNCH_CALENDAT = keyboard.helpers.buttons['\u0b02'] 400 | keyboard.helpers.LAUNCH_MAIL = keyboard.helpers.buttons['\u0b03'] 401 | keyboard.helpers.LAUNCH_MEDIA_PLAYER = keyboard.helpers.buttons['\u0b04'] 402 | keyboard.helpers.LAUNCH_MUSIC_PLAYER = keyboard.helpers.buttons['\u0b05'] 403 | keyboard.helpers.LAUNCH_APPLICATION_1 = keyboard.helpers.buttons['\u0b06'] 404 | keyboard.helpers.LAUNCH_SCREEN_SAVER = keyboard.helpers.buttons['\u0b07'] 405 | keyboard.helpers.LAUNCH_SPREADSHEET = keyboard.helpers.buttons['\u0b08'] 406 | keyboard.helpers.LAUNCH_WEB_BROWSER = keyboard.helpers.buttons['\u0b09'] 407 | keyboard.helpers.LAUNCH_CONTACTS = keyboard.helpers.buttons['\u0b0c'] 408 | keyboard.helpers.LAUNCH_PHONE = keyboard.helpers.buttons['\u0b0d'] 409 | keyboard.helpers.BROWSER_BACK = keyboard.helpers.buttons['\u0c01'] 410 | keyboard.helpers.BROWSER_FAVORITES = keyboard.helpers.buttons['\u0c02'] 411 | keyboard.helpers.BROWSER_FORWARD = keyboard.helpers.buttons['\u0c03'] 412 | keyboard.helpers.BROWSER_HOME = keyboard.helpers.buttons['\u0c04'] 413 | keyboard.helpers.BROWSER_REFRESH = keyboard.helpers.buttons['\u0c05'] 414 | keyboard.helpers.BROWSER_SEARCH = keyboard.helpers.buttons['\u0c06'] 415 | keyboard.helpers.BROWSER_STOP = keyboard.helpers.buttons['\u0c07'] 416 | keyboard.helpers.CHANNEL_DOWN = keyboard.helpers.buttons['\u0d0a'] 417 | keyboard.helpers.CHANNEL_UP = keyboard.helpers.buttons['\u0d0b'] 418 | keyboard.helpers.CLOSED_CAPTION_TOGGLE = keyboard.helpers.buttons['\u0d12'] 419 | keyboard.helpers.EXIT = keyboard.helpers.buttons['\u0d15'] 420 | keyboard.helpers.GUIDE = keyboard.helpers.buttons['\u0d22'] 421 | keyboard.helpers.INFO = keyboard.helpers.buttons['\u0d25'] 422 | keyboard.helpers.MEDIA_FAST_FORWARD = keyboard.helpers.buttons['\u0d2c'] 423 | keyboard.helpers.MEDIA_LAST = keyboard.helpers.buttons['\u0d2d'] 424 | keyboard.helpers.MEDIA_PLAY = keyboard.helpers.buttons['\u0d2f'] 425 | keyboard.helpers.MEDIA_RECORD = keyboard.helpers.buttons['\u0d30'] 426 | keyboard.helpers.MEDIA_REWIND = keyboard.helpers.buttons['\u0d31'] 427 | keyboard.helpers.SETTINGS = keyboard.helpers.buttons['\u0d43'] 428 | keyboard.helpers.ZOOM_TOGGLE = keyboard.helpers.buttons['\u0d4e'] 429 | keyboard.helpers.AUDIO_BASS_BOOST_TOGGLE = keyboard.helpers.buttons['\u0e02'] 430 | keyboard.helpers.SPEECH_INPUT_TOGGLE = keyboard.helpers.buttons['\u0f02'] 431 | keyboard.helpers.APP_SWITCH = keyboard.helpers.buttons['\u1001'] 432 | class modifiers(dict): 433 | ALT = 1 434 | CTRL = 2 435 | META = 4 436 | COMMAND = 4 437 | SHIFT = 8 438 | def __init__(self): 439 | super().__init__() 440 | for key in filter(lambda x: not x.startswith('_') and x.isupper(), dir(self)): 441 | self[key] = getattr(self, key) 442 | def __call__(self, *args): 443 | if len(args) == 1 and hasattr(args[0], '__iter__'): 444 | args = args[0] 445 | result = 0 446 | for flag in set(args): 447 | result ^= flag 448 | return result 449 | keyboard.helpers.modifiers = modifiers() 450 | class types(list): 451 | KEY_DOWN = 'keyDown' 452 | KEY_UP = 'keyUp' 453 | RAW_KEY_DOWN = 'rawKeyDown' 454 | CHAR = 'char' 455 | def __init__(self): 456 | super().__init__() 457 | self.extend(filter(lambda x: not x.startswith('_') and x.isupper(), dir(self))) 458 | keyboard.helpers.types = types() 459 | 460 | class dom: 461 | ''' 462 | DOM handling (decided to remove it for now, because it doesn't work properly) 463 | ''' 464 | disabled = True 465 | async def _load_dom_tree(tabs, tab, root): 466 | kv = {} 467 | async def extract(element): 468 | tab._emit_event('dom__new_node_found', node=element) 469 | kv[element.nodeId] = element 470 | if element.children is not None: 471 | for child in element.children: 472 | await extract(child) 473 | if element.contentDocument is not None: 474 | await extract(element.contentDocument) 475 | if element.shadowRoots is not None: 476 | for shadow in element.shadowRoots: 477 | await extract(shadow) 478 | if element.templateContent is not None: 479 | await extract(element.templateContent) 480 | if element.pseudoElements is not None: 481 | for pseudo in element.pseudoElements: 482 | await extract(pseudo) 483 | if element.importedDocument is not None: 484 | await extract(element.importedDocument) 485 | await extract(root) 486 | return kv 487 | class events: 488 | async def tab_start(tabs, tab): 489 | await tab.DOM.enable() 490 | async with tab.lock('dom'): 491 | tab.dom_kv = await dom._load_dom_tree(tabs, tab, await tab.DOM.get_document(depth=-1, pierce=True)) 492 | async def dom__document_updated(tabs, tab): 493 | async with tab.lock('dom'): 494 | tab.dom_kv = await dom._load_dom_tree(tabs, tab, await tab.DOM.get_document(depth=-1, pierce=True)) 495 | async def dom__set_child_nodes(tabs, tab, parentId, nodes): # ??????? 496 | async with tab.lock('dom'): 497 | tab.dom_kv[parentId].children = nodes 498 | for node in nodes: 499 | tab.dom_kv.update(await dom._load_dom_tree(tabs, tab, node)) 500 | async def dom__attribute_modified(tabs, tab, nodeId, name, value): 501 | async with tab.lock('dom'): 502 | attributes = tab.dom_kv[nodeId].attributes 503 | try: 504 | attributes[attributes.index(name) + 1] = value 505 | except ValueError: 506 | attributes += [name, value] 507 | async def dom__attribute_removed(tabs, tab, nodeId, name): 508 | async with tab.lock('dom'): 509 | attributes = tab.dom_kv[nodeId].attributes 510 | index = attributes.index(name) 511 | attributes.pop(index) 512 | attributes.pop(index) 513 | async def dom__inline_style_invalidated(tabs, tab, nodeIds): 514 | async with tab.lock('dom'): 515 | coroutines = [] 516 | for nodeId in nodeIds: 517 | coroutines.append(tab.DOM.get_attributes(nodeId)) 518 | if len(coroutines) > 0: 519 | results, _ = await asyncio.wait(coroutines) 520 | for nodeId, result in zip(nodeIds, results): 521 | tab.dom_kv[nodeId].attributes = result.result() 522 | async def dom__character_data_modified(tabs, tab, nodeId, characterData): 523 | async with tab.lock('dom'): 524 | tab.dom_kv[nodeId].nodeValue = characterData 525 | async def dom__child_node_count_updated(tabs, tab, nodeId, childNodeCount): 526 | async with tab.lock('dom'): 527 | tab.dom_kv[nodeId].childNodeCount = childNodeCount 528 | async def dom__child_node_inserted(tabs, tab, parentNodeId, previousNodeId, node): 529 | async with tab.lock('dom'): 530 | index = 0 531 | children = tab.dom_kv[parentNodeId].children 532 | if children is None: 533 | tab.dom_kv[parentNodeId].children = [node] 534 | else: 535 | current_index = 0 536 | for child in children: 537 | if child.nodeId == previousNodeId: 538 | index = current_index + 1 539 | break 540 | current_index += 1 541 | children.insert(index, node) 542 | tab.dom_kv.update(await dom._load_dom_tree(tabs, tab, node)) 543 | async def dom__child_node_removed(tabs, tab, parentNodeId, nodeId): 544 | async with tab.lock('dom'): 545 | children = tab.dom_kv[parentNodeId].children 546 | index = None 547 | current_index = 0 548 | for child in children: 549 | if child.nodeId == nodeId: 550 | index = current_index 551 | break 552 | current_index += 1 553 | children.pop(index) 554 | async def dom__shadow_root_pushed(tabs, tab, hostId, root): 555 | async with tab.lock('dom'): 556 | shadowRoots = tab.dom_kv[hostId].shadowRoots 557 | if shadowRoots is None: 558 | tab.dom_kv[hostId].shadowRoots = [root] 559 | else: 560 | shadowRoots.append(root) 561 | tab.dom_kv.update(await dom._load_dom_tree(tabs, tab, root)) 562 | async def dom__shadow_root_popped(tabs, tab, hostId, rootId): 563 | async with tab.lock('dom'): 564 | shadowRoots = tab.dom_kv[hostId].shadowRoots 565 | index = None 566 | current_index = 0 567 | for shadow in shadowRoots: 568 | if shadow.nodeId == rootId: 569 | index = current_index 570 | break 571 | current_index += 1 572 | shadowRoots.pop(index) 573 | async def dom__pseudo_element_added(tabs, tab, parentId, pseudoElement): 574 | async with tab.lock('dom'): 575 | pseudoElements = tab.dom_kv[parentId].pseudoElements 576 | if pseudoElements is None: 577 | tab.dom_kv[parentId].pseudoElements = [pseudoElement] 578 | else: 579 | pseudoElements.append(pseudoElement) 580 | tab.dom_kv.update(await dom._load_dom_tree(tabs, tab, pseudoElement)) 581 | async def dom__pseudo_element_removed(tabs, tab, parentId, pseudoElementId): 582 | async with tab.lock('dom'): 583 | index = None 584 | pseudoElements = tab.dom_kv[parentId].pseudoElements 585 | current_index = 0 586 | for child in pseudoElements: 587 | if child.nodeId == pseudoElementId: 588 | index = current_index 589 | current_index += 1 590 | del pseudoElements[index] 591 | async def dom__distributed_nodes_updated(tabs, tab, insertionPointId, distributedNodes): 592 | async with tab.lock('dom'): 593 | tab.dom_kv[insertionPointId].distributedNodes = distributedNodes 594 | 595 | class isolated_evaluate: 596 | ''' 597 | Evaluate javascript in isolated world on frame navigate 598 | ''' 599 | class macros: 600 | async def __evaluate_on_frame(tabs, tab, code, world_name, keyword): 601 | if not hasattr(tab, keyword): 602 | setattr(tab, keyword, []) 603 | if not hasattr(tab, '_execution_context_id_to_world_name'): 604 | tab._execution_context_id_to_world_name = {} 605 | getattr(tab, keyword).append((code, world_name)) 606 | @classmethod 607 | async def Runtime__isolated_evaluate_on_frame_navigate(cls, tabs, tab, code, world_name='default_world'): 608 | return await cls.__evaluate_on_frame(tabs, tab, code, world_name, '_code_for_frame_navigate') 609 | async def Runtime__isolated_evaluate_file_on_frame_navigate(tabs, tab, path, *args, **kwargs): 610 | with open(path, 'r') as f: 611 | code = f.read() 612 | return await tab.Runtime.isolated_evaluate_on_frame_navigate(code, world_name, *args, **kwargs) 613 | @classmethod 614 | async def Runtime__isolated_evaluate_on_frame_stopped_loading(cls, tabs, tab, code, world_name='default_world'): 615 | return await cls.__evaluate_on_frame(tabs, tab, code, world_name, '_code_for_frame_stopped_loading') 616 | async def Runtime__isolated_evaluate_file_on_frame_stopped_loading(tabs, tab, path, *args, **kwargs): 617 | with open(path, 'r') as f: 618 | code = f.read() 619 | return await tab.Runtime.isolated_evaluate_on_frame_stopped_loading(code, world_name, *args, **kwargs) 620 | class events: 621 | async def tab_start(tabs, tab): 622 | await tab.Page.enable() 623 | await tab.Runtime.enable() 624 | async def runtime__console_api_called(tabs, tab, type, args, executionContextId, **kwargs): 625 | if hasattr(tab, '_execution_context_id_to_world_name') and executionContextId in tab._execution_context_id_to_world_name: 626 | world_name = tab._execution_context_id_to_world_name[executionContextId] 627 | if len(args) > 0 and args[0].type == 'string': 628 | status = args[0].value 629 | if status == 'OK_RESULT': 630 | value = json.loads(args[1].value) 631 | tab._emit_event('runtime__isolated_evaluate_callback', value=value, error=None, world_name=world_name) 632 | elif status == 'FAIL_RESULT': 633 | error = args[1].value 634 | tab._emit_event('runtime__isolated_evaluate_callback', value=None, error=error, world_name=world_name) 635 | async def __frame(tabs, tab, frameId, keyword): 636 | if hasattr(tab, keyword): 637 | worlds = {} 638 | for code, world_name in getattr(tab, keyword): 639 | if world_name not in worlds: 640 | world = await tab.Page.create_isolated_world(frameId=frameId, worldName=world_name, grantUniveralAccess=True) 641 | await tab.Runtime.evaluate(expression=''' 642 | const send = function(value) { 643 | try { 644 | console.log('OK_RESULT', JSON.stringify(value)) 645 | } catch(e) { 646 | console.error('FAIL_RESULT', `JSON decode error: ${e.toString()}`) 647 | } 648 | } 649 | const trace = function(value) { 650 | if('stack' in value) { 651 | try { 652 | send(value.stack) 653 | return 654 | } catch(e) {} 655 | } 656 | try { 657 | send(value.toString()) 658 | return 659 | } catch(e) {} 660 | send('Uncaught unknown error') 661 | } 662 | ''', objectGroup='console', includeCommandLineAPI=True, silent=False, contextId=world, 663 | returnByValue=False, generatePreview=True, userGesture=True, awaitPromise=False) 664 | tab._execution_context_id_to_world_name[world] = world_name 665 | worlds[world_name] = world 666 | else: 667 | world = worlds[world_name] 668 | info = await tab.Runtime.evaluate(expression=code, objectGroup='console', includeCommandLineAPI=True, 669 | silent=False, contextId=world, returnByValue=False, 670 | generatePreview=True, userGesture=True, awaitPromise=False) 671 | if 'exceptionDetails' in info: 672 | details = info['exceptionDetails'] 673 | if details is not None: 674 | raise RuntimeError('{0} {1} at line {2} column {3}'.format(details.text, details.exception.description, 675 | details.lineNumber, details.columnNumber)) 676 | @classmethod 677 | async def page__frame_navigated(cls, tabs, tab, frame): 678 | return await cls.__frame(tabs, tab, frame.id, '_code_for_frame_navigate') 679 | @classmethod 680 | async def page__frame_stopped_loading(cls, tabs, tab, frameId): 681 | return await cls.__frame(tabs, tab, frameId, '_code_for_frame_stopped_loading') 682 | 683 | class old_helpers: 684 | class helpers: 685 | def unpack_response_body(packed): 686 | result = packed['body'] 687 | if packed['base64Encoded']: 688 | result = base64.b64decode(result) 689 | return result 690 | 691 | 692 | 693 | 694 | 695 | 696 | -------------------------------------------------------------------------------- /chrome_remote_interface/library.py: -------------------------------------------------------------------------------- 1 | import requests, base64, json, os, types, copy, collections, traceback, concurrent, time 2 | from . import basic_addons 3 | 4 | try: 5 | import websocket 6 | except ImportError: 7 | pass 8 | 9 | try: 10 | import asyncio 11 | import websockets 12 | except ImportError: 13 | pass 14 | 15 | 16 | def call_method(host, port, method, param=None): 17 | ''' 18 | Calls method from remote target. 19 | If method not found (non 200 status) raises AttributeError 20 | ''' 21 | if param is None: 22 | resp = requests.get('http://{0}:{1}/json/{2}/'.format(host, port, method)) 23 | else: 24 | resp = requests.get('http://{0}:{1}/json/{2}/{3}'.format(host, port, method, param)) 25 | if resp.status_code == 200: 26 | return resp.text 27 | else: 28 | raise AttributeError('No such method: {0}. Got {1} response'.format(method, resp.status_code)) 29 | 30 | class Protocol: 31 | ''' 32 | Load and updates protocol 33 | ''' 34 | BASE = 'https://chromium.googlesource.com' 35 | BROWSER_PROTOCOL = '/chromium/src/+/master/third_party/WebKit/Source/core/inspector/browser_protocol.json?format=TEXT' 36 | JS_PROTOCOL = '/v8/v8/+/master/src/inspector/js_protocol.json?format=TEXT' 37 | PROTOCOL_FILE_NAME = 'protocol.json' 38 | 39 | _protocol = None 40 | 41 | @classmethod 42 | def get_protocol(cls, host=None, port=None): 43 | cls._check(host, port) 44 | return cls._protocol 45 | 46 | @classmethod 47 | def update_protocol(cls): 48 | cls._protocol = cls._update_protocol() 49 | return cls._protocol 50 | 51 | @classmethod 52 | def _check(cls, host, port): 53 | if cls._protocol is None: 54 | cls._protocol = cls._load_protocol(host, port) 55 | 56 | @classmethod 57 | def _load_protocol(cls, host, port): 58 | if host is not None or port is not None: 59 | try: 60 | return json.loads(call_method(host, port, 'protocol')) 61 | except AttributeError: 62 | pass 63 | try: 64 | with open (cls._get_protocol_file_path(), 'r') as f: 65 | return json.load(f) 66 | except (json.JSONDecodeError, FileNotFoundError): 67 | return cls._update_protocol() 68 | 69 | @classmethod 70 | def _update_protocol(cls): 71 | result = cls._download_protocol(cls.BASE + cls.BROWSER_PROTOCOL) 72 | result['domains'] += cls._download_protocol(cls.BASE + cls.JS_PROTOCOL)['domains'] 73 | with open(cls._get_protocol_file_path(), 'w') as f: 74 | json.dump(result, f, indent=4, sort_keys=True) 75 | return result 76 | 77 | @classmethod 78 | def _download_protocol(cls, url): 79 | resp = requests.get(url) 80 | data = base64.b64decode(resp.text) 81 | return json.loads(data) 82 | 83 | @classmethod 84 | def _get_protocol_file_path(cls): 85 | return os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.PROTOCOL_FILE_NAME) 86 | 87 | class FailResponse(Exception): 88 | ''' 89 | Raised if chrome doesn't like our request 90 | ''' 91 | def __init__(self, message, code): 92 | super().__init__(message) 93 | self.code = code 94 | 95 | class API: 96 | ''' 97 | Making magick with json data to create classes and functions 98 | ''' 99 | def __init__(self, host=None, port=None): 100 | raw_type_handlers = [] 101 | self._raw_protocol = copy.deepcopy(Protocol.get_protocol(host, port)) 102 | self.events = self._empty_class() 103 | self._event_name_to_event = {} 104 | self._method_name_to_method = {} 105 | versions = self._raw_protocol['version'] 106 | for value in self._raw_protocol['domains']: 107 | domain = value['domain'] 108 | if not hasattr(self, domain): 109 | setattr(self, domain, self._repr_class(domain)) 110 | current_child = getattr(self, domain) 111 | if 'description' in value: 112 | current_child.__doc__ = value['description'] 113 | current_child.experimental = value['experimental'] if 'experimental' in value else False 114 | current_child.dependencies = value['dependencies'] if 'dependencies' in value else [] 115 | raw_types = value['types'] if 'types' in value else [] 116 | for raw_type in raw_types: 117 | self._connect_raw_type(raw_type, domain, raw_type_handlers, current_child, raw_type['id'], domain+'.'+raw_type['id']) 118 | raw_commands = value['commands'] if 'commands' in value else [] # DIR 119 | for raw_command in raw_commands: 120 | method = self._make_send_method(domain+'.'+raw_command['name'], domain+'.'+raw_command['name']) 121 | method.parameters = {} 122 | raw_parameters = raw_command['parameters'] if 'parameters' in raw_command else [] 123 | for raw_parameter in raw_parameters: 124 | self._connect_raw_parameter_or_result(raw_parameter, domain, raw_type_handlers, method.parameters) 125 | method.returns = {} 126 | raw_returns = raw_command['returns'] if 'returns' in raw_command else [] 127 | for raw_return in raw_returns: 128 | self._connect_raw_parameter_or_result(raw_return, domain, raw_type_handlers, method.returns) 129 | method.redirect = raw_command['redirect'] if 'redirect' in raw_command else None 130 | method.experimental = raw_command['experimental'] if 'experimental' in raw_command else False 131 | if 'description' in raw_command: 132 | method.__doc__ = raw_command['description'] 133 | pythonic_name = self._pythonic_method_name(raw_command['name']) 134 | self._method_name_to_method[domain + '.' + raw_command['name']] = method 135 | setattr(current_child, pythonic_name, method) 136 | raw_events = value['events'] if 'events' in value else [] # DOC? 137 | for raw_event in raw_events: 138 | callback_name = domain.lower() + '__' + self._pythonic_method_name(raw_event['name']) 139 | event_name = domain + '.' + raw_event['name'] 140 | event = self._repr_class(callback_name) 141 | self._event_name_to_event[event_name] = event 142 | if 'description' in raw_event: 143 | event.__doc__ = raw_event['description'] 144 | event.experimental = raw_event['experimental'] if 'experimental' in raw_event else False 145 | raw_parameters = raw_event['parameters'] if 'parameters' in raw_event else [] 146 | event.parameters = {} 147 | for raw_parameter in raw_parameters: 148 | self._connect_raw_parameter_or_result(raw_parameter, domain, raw_type_handlers, event.parameters) 149 | setattr(self.events, callback_name, event) 150 | while len(raw_type_handlers) > 0: 151 | for parent, key, raw_type, connect_function, access_function, domain, class_repr, callbacks in raw_type_handlers: 152 | self._connect_raw_type(raw_type, domain, raw_type_handlers, parent, key, class_repr, connect_function, access_function, callbacks) 153 | 154 | def _connect_raw_type(self, raw_type, domain, raw_type_handlers, parent, key, class_repr=None, connect_function=setattr, access_function=getattr, callback=None): 155 | success_remove_tuple = parent, key, raw_type, connect_function, access_function, domain, class_repr, callback 156 | if '$ref' in raw_type: 157 | ref = raw_type['$ref'] 158 | try: 159 | var1, var2 = ref.split('.', 1) 160 | except ValueError: 161 | var1 = domain 162 | var2 = ref 163 | try: 164 | if len(raw_type) == 1: 165 | connect_function(parent, key, getattr(getattr(self, var1), var2)) 166 | if success_remove_tuple in raw_type_handlers: 167 | raw_type_handlers.remove(success_remove_tuple) 168 | if callback is not None: 169 | callback() 170 | else: 171 | connect_function(parent, key, copy.deepcopy(getattr(getattr(self, var1), var2))) 172 | if success_remove_tuple in raw_type_handlers: 173 | raw_type_handlers.remove(success_remove_tuple) 174 | if callback is not None: 175 | callback() 176 | obj = access_function(parent, key) 177 | if 'optional' in raw_type: 178 | obj.optional = raw_type['optional'] 179 | if 'experimental' in raw_type: 180 | obj.experimental = raw_type['experimental'] 181 | if 'description' in raw_type: 182 | obj.__doc__ = raw_type['description'] 183 | except AttributeError: 184 | if success_remove_tuple not in raw_type_handlers: 185 | raw_type_handlers.append(success_remove_tuple) 186 | elif 'type' in raw_type: 187 | t = raw_type['type'] 188 | if t == 'array': 189 | class CoolType(list, metaclass=self.CustomClassReprType): 190 | def __init__(slf, values): 191 | if slf.min_items is not None and len(values) < slf.min_items: 192 | raise ValueError('Min items is lower than') 193 | if slf.max_items is not None and len(values) > slf.max_items: 194 | raise ValueError('Max items is lower than') 195 | if slf.type is None: 196 | super().__init__(values) 197 | else: 198 | resulting_values = [] 199 | for value in values: 200 | resulting_values.append(self._float_hook(slf.type(value))) 201 | super().__init__(resulting_values) 202 | CoolType._class_repr = class_repr if class_repr is not None else list.__name__ 203 | CoolType._type = list 204 | CoolType.max_items = raw_type['max_items'] if 'max_items' in raw_type else None 205 | CoolType.min_items = raw_type['min_items'] if 'min_items' in raw_type else None 206 | raw_items = raw_type['items'] if 'items' in raw_type else None 207 | if raw_items is None: 208 | CoolType.type = None 209 | else: 210 | self._connect_raw_type(raw_items, domain, raw_type_handlers, CoolType, 'type') 211 | elif t == 'object': 212 | if 'properties' in raw_type: 213 | class CoolType(dict, metaclass=self.CustomClassReprType): 214 | def __init__(slf, values=None, **kwargs): 215 | if values is not None: 216 | pass 217 | else: 218 | values = kwargs 219 | to_add = set(slf.property_names.keys()) 220 | for key in values: 221 | if key not in slf.property_names: 222 | raise ValueError('there is no such property: {0}'.format(key)) 223 | else: 224 | slf[key] = self._float_hook(slf.property_names[key].type(values[key])) 225 | to_add.remove(key) 226 | to_remove_from_add = set() 227 | for key in to_add: 228 | if slf.property_names[key].optional: 229 | slf[key] = None 230 | to_remove_from_add.add(key) 231 | to_add = to_add.difference(to_remove_from_add) 232 | if len(to_add) > 0: 233 | raise ValueError('Not enough parameters: {0}'.format(', '.join(to_add))) 234 | def __getattr__(self, name): 235 | if name in self: 236 | return self[name] 237 | def __repr__(self): 238 | return '{0}({1})'.format(self._class_repr, super().__repr__()) 239 | def __dir__(self): 240 | return super().__dir__() + list(self.keys()) 241 | CoolType._class_repr = class_repr 242 | raw_properties = raw_type['properties'] if 'properties' in raw_type else [] 243 | CoolType.property_names = {} 244 | for raw_property in raw_properties: 245 | CoolType.property_names[raw_property['name']] = self._make_ppr(raw_property) 246 | self._connect_raw_type(raw_property, domain, raw_type_handlers, CoolType.property_names[raw_property['name']], 'type') 247 | else: 248 | CoolType = self._dummy_cool_type(class_repr, dict) 249 | CoolType._type = dict 250 | elif t == 'string': 251 | enum = raw_type['enum'] if 'enum' in raw_type else None 252 | if enum is None: 253 | CoolType = self._dummy_cool_type(class_repr, str) 254 | else: 255 | class CoolType(str, metaclass=self.CustomClassReprType): 256 | def __new__(cls, value): 257 | if value not in cls.enum: 258 | raise ValueError('string must be one of the following: {0}'.format(', '.join(cls.enum))) 259 | return value 260 | CoolType.enum = enum 261 | CoolType._type = str 262 | CoolType._class_repr = class_repr if class_repr is not None else str.__name__ 263 | elif t in ['integer', 'number', 'any', 'boolean']: 264 | name_to_class = {'integer': int, 'number': float, 'boolean': bool, 'any': None} 265 | CoolType = self._dummy_cool_type(class_repr, name_to_class[t]) 266 | if name_to_class[t] is not None: 267 | CoolType._class_repr = name_to_class[t].__name__ 268 | else: 269 | raise ValueError('Unknown type: {0}'.format(t)) 270 | if 'description' in raw_type: 271 | CoolType.__doc__ = raw_type['description'] 272 | CoolType.experimental = raw_type['experimental'] if 'experimental' in raw_type else None 273 | CoolType.deprecated = raw_type['deprecated'] if 'deprecated' in raw_type else False 274 | CoolType.optional = raw_type['optional'] if 'optional' in raw_type else False 275 | connect_function(parent, key, CoolType) 276 | if success_remove_tuple in raw_type_handlers: 277 | raw_type_handlers.remove(success_remove_tuple) 278 | if callback is not None: 279 | callback() 280 | else: 281 | raise ValueError('There is no type or $ref {0}'.format(raw_type)) 282 | 283 | def _connect_raw_parameter_or_result(self, raw_value, domain, raw_type_handlers, parent): 284 | value = self._empty_class() 285 | name = raw_value.pop('name') 286 | def callback(): 287 | o = parent[name] 288 | if hasattr(o, '_type'): 289 | o._class_repr = o._type.__name__ 290 | parent[name] = self._make_ppr(raw_value) 291 | self._connect_raw_type(raw_value, domain, raw_type_handlers, parent[name], 'type', callback=callback) 292 | 293 | def _make_send_method(self, original_name, class_repr): 294 | class result(): 295 | def __call__(slf, *args, **kwargs): 296 | if len(args) == 0: 297 | pass 298 | elif len(args) == 1: 299 | positionalArgumentFound = False 300 | for key, value in slf.parameters.items(): 301 | if not value.optional: 302 | if positionalArgumentFound: 303 | raise TypeError('This object access more than one non optional value') 304 | if key not in kwargs: 305 | kwargs[key] = args[0] 306 | positionalArgumentFound 307 | else: 308 | raise TypeError('You can\'t use positional argument if it is already set by key') 309 | else: 310 | raise TypeError('You can pass maximum 1 positional argument ({0} given)'.format(len(args))) 311 | for key, value in slf.parameters.items(): 312 | if not value.optional and key not in kwargs: 313 | raise TypeError('Required argument \'{0}\' not found'.format(key)) 314 | for key, arg in kwargs.items(): 315 | if key not in slf.parameters: 316 | raise TypeError('got an unexpected keyword argument \'{0}\''.format(key)) 317 | param = slf.parameters[key].type 318 | potential_class = param._type if hasattr(param, '_type') else param 319 | valid = (arg.__class__ == potential_class) or (potential_class == float and arg.__class__ == int) 320 | if not valid: 321 | raise ValueError('Param \'{0}\' must be {1}'.format(key, potential_class)) 322 | return self.send_raw(slf._original_name, kwargs, slf.returns) 323 | def __repr__(self): 324 | return self._class_repr 325 | result._original_name = original_name 326 | result._class_repr = class_repr 327 | return result() 328 | 329 | def _unpack_event(self, event_name, values): 330 | domain, pidor = event_name.split('.') 331 | callback_name = domain.lower() + '__' + self._pythonic_method_name(pidor) 332 | try: 333 | parameters = self._event_name_to_event[event_name].parameters 334 | result = {} 335 | for key, value in values.items(): 336 | if key in parameters: 337 | result[key] = parameters[key].type(value) 338 | return result, callback_name 339 | except KeyError: 340 | return values, callback_name 341 | 342 | def _float_hook(self, value): 343 | # Some whalues should be integers in my opinion (not numbers), but they are (response code for example) 344 | # So we will cast int like floats to int 345 | if isinstance(value, float) and int(value) == value: 346 | return int(value) 347 | else: 348 | return value 349 | 350 | def _unpack_response(self, method_name, values): 351 | try: 352 | returns = self._method_name_to_method[method_name].returns 353 | result = {} 354 | for key, value in values.items(): 355 | if key in returns: 356 | result[key] = returns[key].type(value) 357 | except KeyError: 358 | result = values 359 | if len(result) == 0: 360 | return 361 | elif len(result) == 1: 362 | return next(iter(result.items()))[1] 363 | else: 364 | return result 365 | 366 | def _pythonic_method_name(self, old_name): 367 | old_one = list(old_name) 368 | new_one = [] 369 | previous_was_low = False 370 | previous_was_underscore = False 371 | previous_was_first = True 372 | for i in reversed(range(len(old_one))): 373 | c = old_one[i] 374 | if c.isupper(): 375 | if previous_was_low: 376 | new_one.append('_'+c.lower()) 377 | previous_was_underscore = True 378 | else: 379 | new_one.append(c.lower()) 380 | previous_was_underscore = False 381 | previous_was_low = False 382 | else: 383 | if previous_was_low: 384 | new_one.append(c) 385 | else: 386 | if previous_was_underscore or previous_was_first: 387 | new_one.append(c) 388 | else: 389 | new_one.append(c+'_') 390 | previous_was_underscore = False 391 | previous_was_low = True 392 | previous_was_first = False 393 | return ''.join(reversed(new_one)) 394 | 395 | def _dummy_cool_type(self, class_repr, t): 396 | class CoolType(metaclass=self.CustomClassReprType): 397 | def __new__(cls, obj): 398 | if obj is None: 399 | return obj 400 | if cls._type == float and type(obj) == int: 401 | obj = float(obj) 402 | if cls._type is not None and cls._type != type(obj): 403 | raise ValueError('Type must be {0}, not {1}'.format(cls._type, type(obj))) 404 | return self._float_hook(obj) 405 | CoolType._type = t 406 | CoolType._class_repr = class_repr 407 | return CoolType 408 | 409 | class CustomClassReprType(type): 410 | def __repr__(self): 411 | if self._class_repr is not None: 412 | return self._class_repr 413 | else: 414 | return super().__repr__() 415 | 416 | def _make_ppr(self, raw_thing): 417 | result = self._empty_class() 418 | result.optional = raw_thing.pop('optional') if 'optional' in raw_thing else False 419 | result.deprecated = raw_thing.pop('deprecated') if 'deprecated' in raw_thing else False 420 | result.experimental = raw_thing.pop('experimental') if 'experimental' in raw_thing else False 421 | if 'description' in raw_thing: 422 | result.__doc__ = raw_thing.pop('description') 423 | return result 424 | 425 | def _empty_class(self): 426 | class a: pass 427 | return a 428 | 429 | def _repr_class(self, class_repr): 430 | class a(metaclass=self.CustomClassReprType): pass 431 | a._class_repr = class_repr 432 | return a 433 | 434 | class TabsSync: 435 | ''' 436 | These tabs can be used to work synchronously from terminal 437 | ''' 438 | def __init__(self, host, port, *callbacks_collection, fail_on_exception=True): 439 | self._host = host 440 | self._port = port 441 | self._tabs = {} 442 | self._callbacks_collection = list(callbacks_collection) 443 | for callbacks in self._callbacks_collection: 444 | callbacks.start(self) 445 | self._initial_tabs = [] 446 | try: 447 | initial_list = json.loads(call_method(self._host, self._port, 'list')) 448 | except AttributeError: 449 | initial_list = [] 450 | for el in initial_list: 451 | self._initial_tabs.append(el['id']) 452 | 453 | FailResponse = FailResponse 454 | 455 | def __repr__(self): 456 | return '{0}({1}:{2})'.format(type(self).__name__, self._host, self._port) 457 | 458 | def __enter__(self): 459 | return self 460 | 461 | def __exit__(self, type, value, traceback): 462 | self.close() 463 | 464 | def close(self): 465 | for tab in self._tabs: 466 | tab.close() 467 | for callbacks in self._callbacks_collection: 468 | if hasattr(callbacks, 'close'): 469 | callbacks.close(self) 470 | 471 | @property 472 | def host(self): 473 | return self._host 474 | 475 | @property 476 | def port(self): 477 | return self._port 478 | 479 | def add(self): 480 | tab = SocketClientSync(self._host, self._port, self) 481 | tab.manual = True 482 | self._tabs[tab.id] = tab 483 | for callbacks in self._callbacks_collection: 484 | if hasattr(callbacks, 'tab_start'): 485 | callbacks.tab_start(self, tab) 486 | return tab 487 | 488 | def keys(self): 489 | return self._tabs.keys() 490 | 491 | def __getitem__(self, key): 492 | if isinstance(key, str): 493 | if self._tabs[key] is None: 494 | raise KeyError(key) 495 | return self._tabs[key] 496 | else: 497 | raise TypeError('{0} key must be str'.format(type(self).__name__)) 498 | 499 | def __contains__(self, key): 500 | if isinstance(key, str): 501 | return key in self._tabs 502 | else: 503 | raise TypeError('{0} key must be str'.format(type(self).__name__)) 504 | 505 | def remove(self, value): 506 | if isinstance(value, str): 507 | key = value 508 | else: 509 | key = None 510 | for k in self._tabs: 511 | if self._tabs[k] == value: 512 | key = k 513 | if key is None: 514 | raise ValueError('Tab not found') 515 | self._tabs[key].close() 516 | 517 | class SocketClientSync(API): 518 | ''' 519 | These tab client can be used to work synchronously from terminal 520 | ''' 521 | def __init__(self, host, port, tabs=None, tab_id=None): 522 | super().__init__(host, port) 523 | self._host = host 524 | self._port = port 525 | if tab_id is None: 526 | tab_info = json.loads(call_method(self._host, self._port, 'new')) 527 | self._id = tab_info['id'] 528 | self._ws_url = tab_info['webSocketDebuggerUrl'] 529 | else: 530 | try: 531 | tab_info = None 532 | for current_tab_info in json.loads(call_method(self._host, self._port, 'list')): 533 | if current_tab_info['id'] == tab_id: 534 | tab_info = current_tab_info 535 | if tab_info is None: 536 | raise ValueError('Tab {0} not found'.format(tab_id)) 537 | self._id = tab_info['id'] 538 | self._ws_url = tab_info['webSocketDebuggerUrl'] 539 | except: 540 | self._id = tab_id 541 | self._ws_url = 'ws://{0}:{1}/devtools/page/{2}'.format(self._host, self._port, tab_id) 542 | self._soc = websocket.create_connection(self._ws_url) 543 | self._i = 0 544 | self._tabs = tabs 545 | 546 | @property 547 | def ws_url(self): 548 | return self._ws_url 549 | 550 | @property 551 | def id(self): 552 | return self._id 553 | 554 | def __repr__(self): 555 | return 'SocketClientSync("{0}")'.format(self._ws_url) 556 | 557 | def __enter__(self): 558 | return self 559 | 560 | def __exit__(self, type, value, traceback): 561 | self._soc.close() 562 | 563 | def send_raw(self, method, params=None, expectedTypes=None): 564 | self._i += 1 565 | self._soc.send(json.dumps({'id': self._i, 'method': method, 'params': params})) 566 | while 1: 567 | resp = json.loads(self._soc.recv()) 568 | if 'id' in resp and 'result' in resp: 569 | i = resp['id'] 570 | if i != self._i: 571 | raise RuntimeError('ids are not the same {0} != {0}'.format(i, self._i)) 572 | result = resp['result'] 573 | if expectedTypes is not None: 574 | return self._unpack_response(method, result) 575 | else: 576 | return result 577 | elif 'method' in resp and 'params' in resp: 578 | method = resp['method'] 579 | params = resp['params'] 580 | self._handle_event(method, params) 581 | elif 'error' in resp: 582 | i = resp['id'] 583 | if i != self._i: 584 | raise RuntimeError('ids are not the same {0} != {0}'.format(i, self._i)) 585 | raise FailResponse(resp['error']['message'], resp['error']['code']) 586 | else: 587 | raise RuntimeError('Unknown data came: {0}'.format(resp)) 588 | 589 | def _handle_event(self, method, params): 590 | parameters, callback_name = self._unpack_event(method, params) 591 | if self._tabs is not None: 592 | for callbacks in self._tabs._callbacks_collection: 593 | if hasattr(callbacks, callback_name): 594 | getattr(callbacks, callback_name)(**parameters) 595 | elif hasattr(callbacks, 'any'): 596 | callbacks.any(parameters) 597 | else: 598 | pass 599 | 600 | def recv(self): 601 | n = 0 602 | self._soc.settimeout(0.1) 603 | try: 604 | got = self._soc.recv() 605 | while 1: 606 | try: 607 | val = json.loads(got) 608 | self._handle_event(val['method'], val['params']) 609 | n += 1 610 | break 611 | except json.JSONDecodeError as e: 612 | self._handle_event(got[:e.pos]) 613 | n += 1 614 | got = got[e.pos:] 615 | except websocket.WebSocketTimeoutException: 616 | pass 617 | self._soc.settimeout(None) 618 | return n 619 | 620 | def close(self): 621 | if not self.closed: 622 | self._soc.close() 623 | self._soc = None 624 | for callbacks in self._tabs._callbacks_collection: 625 | if self._tabs is not None and hasattr(callbacks, 'tab_close'): 626 | callbacks.tab_close(self._tabs, self) 627 | call_method(self._host, self._port, 'close', self._id) 628 | 629 | @property 630 | def closed(self): 631 | return self._soc is None 632 | 633 | def remove(self): 634 | self.close() 635 | if self._tabs is not None: 636 | del self._tabs[self.id] 637 | self._tabs = None 638 | 639 | class Tabs: 640 | ''' 641 | Tabs here 642 | ''' 643 | def __init__(self, host, port, *callbacks_collection, excluded_basic_addons=[], fail_on_exception=True): 644 | self._host = host 645 | self._port = port 646 | self._tabs = {} 647 | self._terminate_lock = asyncio.Lock() 648 | self._callbacks_collection = list(callbacks_collection) 649 | self._fail_on_exception = fail_on_exception 650 | self._macros = [] 651 | class helpers: 652 | pass 653 | self.helpers = helpers 654 | for key in dir(basic_addons): 655 | if not key.startswith('_') and key not in excluded_basic_addons: 656 | addon = getattr(basic_addons, key) 657 | if not (hasattr(addon, 'disabled') and addon.disabled): 658 | if hasattr(addon, 'macros'): 659 | macros = getattr(addon, 'macros') 660 | for key2 in dir(macros): 661 | if not key2.startswith('_'): 662 | try: 663 | domain_name, prop_name = key2.split('__', 1) 664 | method = getattr(macros, key2) 665 | self._macros.append((domain_name, prop_name, method)) 666 | except ValueError: 667 | pass 668 | if hasattr(addon, 'helpers'): 669 | addon_helpers = getattr(addon, 'helpers') 670 | if not hasattr(self.helpers, key): 671 | class module_helpers: 672 | pass 673 | setattr(self.helpers, key, module_helpers) 674 | else: 675 | module_helpers = getattr(self.helpers, key) 676 | for key2 in dir(addon_helpers): 677 | if not key2.startswith('_'): 678 | if not hasattr(module_helpers, key2): 679 | setattr(module_helpers, key2, getattr(addon_helpers, key2)) 680 | else: 681 | raise KeyError('Duplicating helper {0}.{1}'.format(key, key2)) 682 | if hasattr(addon, 'events'): 683 | self._callbacks_collection.append(getattr(addon, 'events')) 684 | self._initial_tabs = [] 685 | try: 686 | initial_list = json.loads(call_method(self._host, self._port, 'list')) 687 | except AttributeError: 688 | initial_list = [] 689 | for el in initial_list: 690 | self._initial_tabs.append(el['id']) 691 | self._start_time = time.time() 692 | 693 | FailResponse = FailResponse 694 | ConnectionClosed = websockets.ConnectionClosed 695 | InvalidHandshake = websockets.InvalidHandshake 696 | 697 | def __repr__(self): 698 | return '{0}({1}:{2})'.format(type(self).__name__, self._host, self._port) 699 | 700 | async def __aenter__(self): 701 | await self._terminate_lock.acquire() 702 | coroutines = [] 703 | for callbacks in self._callbacks_collection: 704 | if hasattr(callbacks, 'start'): 705 | coroutines.append(callbacks.start(self)) 706 | if len(coroutines) > 0: 707 | await asyncio.wait(coroutines) 708 | return self 709 | 710 | async def __aexit__(self, type, value, traceback): 711 | coroutines = [] 712 | for callbacks in self._callbacks_collection: 713 | if hasattr(callbacks, 'close'): 714 | coroutines.append(callbacks.close(self)) 715 | if len(coroutines) > 0: 716 | await asyncio.wait(coroutines) 717 | await asyncio.wait([self._tabs[key].__aexit__(None, None, None) for key in self._tabs]) 718 | 719 | @property 720 | def host(self): 721 | return self._host 722 | 723 | @property 724 | def port(self): 725 | return self._port 726 | 727 | async def add(self): 728 | tab = await SocketClient(self._host, self._port, self).__aenter__() 729 | tab.manual = True 730 | self._tabs[tab.id] = tab 731 | coroutines = [] 732 | for callbacks in self._callbacks_collection: 733 | if hasattr(callbacks, 'tab_start'): 734 | asyncio.ensure_future(tab._run_later(callbacks.tab_start(self, tab))) 735 | return tab 736 | 737 | def keys(self): 738 | return self._tabs.keys() 739 | 740 | def __getitem__(self, key): 741 | if isinstance(key, str): 742 | if self._tabs[key] is None: 743 | raise KeyError(key) 744 | return self._tabs[key] 745 | else: 746 | raise TypeError('{0} key must be str'.format(type(self).__name__)) 747 | 748 | def __contains__(self, key): 749 | if isinstance(key, str): 750 | return key in self._tabs 751 | else: 752 | raise TypeError('{0} key must be str'.format(type(self).__name__)) 753 | 754 | @classmethod 755 | async def run(cls, host, port, *args, **kwargs): 756 | async with Tabs(host, port, *args, **kwargs) as tabs: 757 | await tabs._terminate_lock.acquire() 758 | 759 | def terminate(self): 760 | if self._terminate_lock.locked(): 761 | self._terminate_lock.release() 762 | 763 | def timestamp(self): 764 | return time.time() - self._start_time 765 | 766 | async def remove(self, value): 767 | if isinstance(value, str): 768 | key = value 769 | else: 770 | key = None 771 | for k in self._tabs: 772 | if self._tabs[k] == value: 773 | key = k 774 | if key is None: 775 | raise ValueError('Tab not found') 776 | await self._tabs[key].__aexit___() 777 | del self._tabs[key] 778 | call_method(self._host, self._port, 'close', self._id) 779 | 780 | class SocketClient(API): 781 | ''' 782 | this client is cool 783 | ''' 784 | def __init__(self, host, port, tabs, tab_id=None): 785 | super().__init__(host, port) 786 | self._host = host 787 | self._port = port 788 | if tab_id is None: 789 | tab_info = json.loads(call_method(self._host, self._port, 'new')) 790 | self._id = tab_info['id'] 791 | self._ws_url = tab_info['webSocketDebuggerUrl'] 792 | else: 793 | try: 794 | tab_info = None 795 | for current_tab_info in json.loads(call_method(self._host, self._port, 'list')): 796 | if current_tab_info['id'] == tab_id: 797 | tab_info = current_tab_info 798 | if tab_info is None: 799 | raise ValueError('Tab {0} not found'.format(tab_id)) 800 | self._id = tab_info['id'] 801 | self._ws_url = tab_info['webSocketDebuggerUrl'] 802 | except AttributeError: 803 | self._id = tab_id 804 | self._ws_url = 'ws://{0}:{1}/devtools/page/{2}'.format(self._host, self._port, tab_id) 805 | self._i = 0 806 | self._tabs = tabs 807 | self._method_responses = {} 808 | self._recv_data_lock = {} 809 | self._pending_tasks = [] 810 | for domain_name, prop_name, method in tabs._macros: 811 | if not hasattr(self, domain_name): 812 | domain = self._empty_class() 813 | setattr(self, domain_name, domain) 814 | else: 815 | domain = getattr(self, domain_name) 816 | if not hasattr(domain, prop_name): 817 | setattr(domain, prop_name, self._wrap_macros(method)) 818 | else: 819 | raise KeyError('Duplicating macros {0}.{1}'.format(domain, prop_name)) 820 | self._locks = {} 821 | class lock: 822 | def __init__(slf, key='default'): 823 | slf.key = key 824 | async def __aenter__(slf): 825 | if slf.key not in self._locks: 826 | self._locks[slf.key] = asyncio.Lock() 827 | await self._locks[slf.key].acquire() 828 | async def __aexit__(slf, exc_type, exc, tb): 829 | self._locks[slf.key].release() 830 | self.lock = lock 831 | self._start_time = time.time() 832 | 833 | def _wrap_macros(self, macro): 834 | async def wrapped(*args, **kwargs): 835 | return await macro(self._tabs, self, *args, **kwargs) 836 | return wrapped 837 | 838 | async def _run_later(self, coroutine): 839 | task = asyncio.Task(coroutine) 840 | self._pending_tasks.append(task) 841 | try: 842 | await task 843 | finally: 844 | self._pending_tasks.remove(task) 845 | 846 | @property 847 | def ws_url(self): 848 | return self._ws_url 849 | 850 | @property 851 | def id(self): 852 | return self._id 853 | 854 | def __repr__(self): 855 | return '{0}("{1}")'.format(type(self).__name__, self._ws_url) 856 | 857 | async def __aenter__(self): 858 | self._soc = await websockets.connect(self._ws_url) 859 | async def loop(): 860 | try: 861 | while 1: 862 | resp = json.loads(await self._soc.recv()) 863 | if 'id' in resp: 864 | self._method_responses[resp['id']] = resp 865 | self._recv_data_lock[resp['id']].release() 866 | elif 'method' in resp: 867 | asyncio.ensure_future(self._run_later(self._handle_event(resp['method'], resp['params']))) 868 | else: 869 | raise RuntimeError('Unknown data came: {0}'.format(resp)) 870 | except (websockets.ConnectionClosed, concurrent.futures.CancelledError): 871 | pass 872 | except Exception as e: 873 | traceback.print_exc() 874 | asyncio.ensure_future(self._run_later(loop())) 875 | return self 876 | 877 | async def __aexit__(self, type, value, traceback): 878 | await self.close() 879 | 880 | def _emit_event(self, event_name, **kwargs): 881 | coroutines = [] 882 | for callbacks in self._tabs._callbacks_collection: 883 | if hasattr(callbacks, event_name): 884 | coroutines.append(getattr(callbacks, event_name)(self._tabs, self, **kwargs)) 885 | elif hasattr(callbacks, 'any'): 886 | coroutines.append(callbacks.any(tabs, tab, **kwargs)) 887 | if len(coroutines) > 0: 888 | asyncio.ensure_future(self._run_later(asyncio.wait(coroutines))) 889 | 890 | async def send_raw(self, method, params=None, expectedTypes=None): 891 | self._i += 1 892 | i = self._i 893 | self._recv_data_lock[i] = asyncio.Lock() 894 | await self._recv_data_lock[i].acquire() 895 | await self._soc.send(json.dumps({'id': i, 'method': method, 'params': params})) 896 | await self._recv_data_lock[i].acquire() 897 | del self._recv_data_lock[i] 898 | resp = self._method_responses.pop(i) 899 | if 'result' in resp: 900 | result = resp['result'] 901 | if expectedTypes is not None: 902 | return self._unpack_response(method, result) 903 | else: 904 | return result 905 | elif 'error' in resp: 906 | raise FailResponse(resp['error']['message'], resp['error']['code']) 907 | else: 908 | raise RuntimeError('Unknown data came: {0}'.format(resp)) 909 | 910 | async def _handle_event(self, method, params): 911 | try: 912 | parameters, callback_name = self._unpack_event(method, params) 913 | if self._tabs is not None: 914 | for callbacks in self._tabs._callbacks_collection: 915 | if hasattr(callbacks, callback_name): 916 | asyncio.ensure_future(self._run_later(getattr(callbacks, callback_name)(self._tabs, self, **parameters))) 917 | elif hasattr(callbacks, 'any'): 918 | asyncio.ensure_future(self._run_later(callbacks.any(self._tabs, self, callback_name, parameters))) 919 | else: 920 | pass 921 | except Exception as e: 922 | traceback.print_exc() 923 | 924 | async def close(self, force=False): 925 | if not self.closed: 926 | await self._soc.close() 927 | try: 928 | call_method(self._host, self._port, 'close', self._id) 929 | except AttributeError: 930 | pass 931 | if not self.closed or force: 932 | coroutines = [] 933 | for callbacks in self._tabs._callbacks_collection: 934 | if self._tabs is not None and hasattr(callbacks, 'tab_close'): 935 | coroutines.append(callbacks.tab_close(self._tabs, self)) 936 | if len(coroutines) > 0: 937 | await asyncio.wait(coroutines) 938 | for task in self._pending_tasks: 939 | task.cancel() 940 | 941 | @property 942 | def closed(self): 943 | return self._soc.close_reason is not None 944 | 945 | def timestamp(self): 946 | return time.time() - self._start_time 947 | 948 | async def remove(self): 949 | await self.close() 950 | if self._tabs is not None: 951 | del self._tabs[self] 952 | self._tabs = None 953 | 954 | 955 | 956 | 957 | 958 | 959 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import chrome_remote_interface 3 | 4 | if __name__ == '__main__': 5 | class callbacks: 6 | async def start(tabs): 7 | await tabs.add() 8 | async def tab_start(tabs, tab): 9 | await tab.Page.enable() 10 | await tab.Network.enable() 11 | await tab.Page.navigate(url='http://github.com') 12 | async def network__loading_finished(tabs, tab, requestId, **kwargs): 13 | try: 14 | body = tabs.helpers.unpack_response_body(await tab.Network.get_response_body(requestId=requestId)) 15 | print('body length:', len(body)) 16 | except tabs.FailResponse as e: 17 | print('fail:', e) 18 | async def page__frame_stopped_loading(tabs, tab, **kwargs): 19 | print('finish') 20 | tabs.terminate() 21 | async def any(tabs, tab, callback_name, parameters): 22 | pass 23 | # print('Unknown event fired', callback_name) 24 | 25 | asyncio.get_event_loop().run_until_complete(chrome_remote_interface.Tabs.run('localhost', 9222, callbacks)) 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | websocket-client 3 | requests 4 | pyperclip -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import chrome_remote_interface 2 | from distutils.core import setup 3 | 4 | 5 | setup( 6 | name='chrome_remote_interface', 7 | version='0.1', 8 | description='Client for talking to the Google Chrome remote shell port', 9 | author='wasiher', 10 | author_email='watashiwaher@gmail.com', 11 | url='https://github.com/wasiher/chrome_remote_interface_python', 12 | packages=['chrome_remote_interface'], 13 | install_requires=['websockets', 'websocket-client', 'requests'], 14 | ) --------------------------------------------------------------------------------