class Reply(object):
    def __init__(self, reply, request=None, events=None):
        if not isinstance(reply, dict):
            raise AttributeError("Reply payload must be a dict")

        if 'id' not in reply:
            raise AttributeError("Reply payload must contain an 'id' field")

        if not isinstance(reply.get('result'), dict):
            reply['result'] = {}

        self.reply   = reply
        self.id      = reply['id']
        self.result  = reply['result']
        self.request = (request or {})
        self.events  = (events or [])

        logging.debug(' << [{:04d}] OK'.format(
            self.id
        ))

    def as_dict(self):
        return {
            'request':    self.request,
            'message_id': self.id,
            'reply':      self.reply,
            'events':     self.events,
        }

    def get(self, key, fallback=None):
        return self.result.get(key, fallback)

    def ievents(self, type=None):
        for event in self.events:
            if isinstance(type, basestring):
                if type != str(event):
                    continue

            yield event class Base(object):
    supports_events = True
    domain = None

    def __init__(self, tab):
        if self.domain is None:
            raise ValueError("Cannot instantiate an RPC proxy without a domain class property.")

        self.tab       = tab
        self.callbacks = {}

    def initialize(self):
        pass

    def call(self, method, expect_reply=True, reply_timeout=None, **params):
        return self.tab.rpc(
            '{}.{}'.format(self.domain, method),
            expect_reply=expect_reply,
            reply_timeout=reply_timeout,
            **params
        )

    def enable(self):
        if self.supports_events:
            self.call('enable')

    def disable(self):
        if self.supports_events:
            self.call('disable')

    def call_boolean_response(self, method, field='result', **kwargs):
        if self.call(method, **kwargs).get(field) is True:
            return True

        return False

    def on(self, method, callback):
        # normalize method name
        if not method.startswith(self.domain + '.'):
            method = '{}.{}'.format(self.domain, method)

        # create handler dict if we need to
        if method not in self.callbacks:
            self.callbacks[method] = OrderedDict()

        callback_id = '{}.event_{}'.format(self.domain, uuid4())
        self.callbacks[method][callback_id] = callback

        logging.debug('Registered event handler {} for event {}'.format(
            callback_id,
            method
        ))

        return callback_id

    def remove_handler(self, callback_id):
        for _, callbacks in self.callbacks.items():
            for id, _ in callbacks.items():
                if callback_id == id:
                    del callbacks[callback_id]
                    return True

        return False

    def trigger(self, method, payload=None):
        event = Event(self, method, payload)

        if str(event) in self.callbacks:
            for callback_id, callback in self.callbacks[str(event)].items():
                if callable(callback):
                    response = callback(event)

                    if response is False:
                        break

        return event class Browser(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Browser
    """

    domain = 'Browser'
    supports_events = False

    def get_window_size(self):
        # response = self.call('getWindowBounds')
        pass class Console(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Console
    """
    domain = 'Console' class DOM(Base):
    domain = 'DOM'
    _you_never_forget_your_first_root_element = None
    _root_element = None
    _elements = {}

    def initialize(self):
        self.on('setChildNodes', self.on_child_nodes)
        self.on('childNodeInserted', self.on_child_inserted)
        self.on('childNodeRemoved', self.on_child_removed)
        self.tab.page.on('frameClearedScheduledNavigation', self.reset)

    def reset(self, *args, **kwargs):
        """
        Clears out any existing local definitions in the DOM tree. This is used when navigating
        between pages so that the new remote DOM can be stored.
        """
        self._root_element = None

        if kwargs.get('clear_initial_root', True):
            self._you_never_forget_your_first_root_element = None

    def clear_requests(self, *args, **kwargs):
        self.tab.reset_network_request_cache()

    def has_element(self, id):
        """
        Return whether we know about the given element ID or not.

        #### Arguments

        - **id** (`str`):

            The DOM element id.
        """
        if id in self._elements:
            return True
        return False

    def element(self, id, fallback=None):
        """
        Return the given ID as a `webfriend.rpc.dom.DOMElement`, or `None` if we have not
        received the element from Chrome.

        #### Arguments

        - **id** (`str`):

            The DOM element id.

        - **fallback** (any):

            The value to return if the element is not known.

        #### Returns
        - `webfriend.rpc.dom.DOMElement` if the element is known, **fallback** if not.
        """
        return self._elements.get(id, fallback)

    def element_at(self, x, y, shadow=False):
        """
        Return the topmost `webfriend.rpc.dom.DOMElement` at the given page coordinates, or `None`
        if there is no element there.

        #### Arguments

        - **x** (`int`):

            The X-coordinate to test at.

        - **y** (`int`):

            The Y-coordinate to test at.

        - **shadow** (`bool`):

            If specified, include "shadow DOMs" in the query; see
            https://developers.google.com/web/fundamentals/getting-started/primers/shadowdom

        #### Returns
        - `webfriend.rpc.dom.DOMElement` if an element is at the coordinates, `None` if not.
        """
        node_id = self.call(
            'getNodeForLocation',
            x=x,
            y=y,
            includeUserAgentShadowDOM=shadow
        ).get('nodeId')

        if node_id:
            return self.element(node_id)

        return None

    def print_node(self, node_id=None, level=0, indent=4):
        """
        Print details about the given element to the debug log.
        """
        if node_id is None:
            node_id = self.root.id

        node = self.element(node_id)

        if node is not None:
            logging.debug('TREE: {}{}'.format((' ' * indent * level), node))

            for child in node.children:
                self.print_node(child.id, level=level + 1)

    @property
    def root(self):
        """
        Returns the root DOM element for the current document.

        #### Returns
        The `webfriend.rpc.dom.DOMElement` representing the root element.
        """
        if self._root_element is None:
            self.reset()

            self._root_element = DOMElement(
                self,
                self.call('getDocument', depth=1, pierce=True).get('root')
            )

            # keep track of the VERY first root element encountered, so that if we switch roots
            # for traversing frames, we can always come back to this one
            if self._you_never_forget_your_first_root_element is None:
                self._you_never_forget_your_first_root_element = self._root_element

            self._elements[self._root_element.id] = self._root_element

        return self._root_element

    @property
    def scroll_width(self):
        return self.root.evaluate(
            "return document.documentElement.scrollWidth",
            return_by_value=True
        ).get('value', None)

    @property
    def scroll_height(self):
        return self.root.evaluate(
            "return document.documentElement.scrollHeight",
            return_by_value=True
        ).get('value', None)

    @property
    def frames(self):
        return self.query_all('frame')

    def root_to(self, selector):
        elements = self.select_nodes(selector, wait_for_match=True)

        if elements:
            element = self.ensure_unique_element(selector, elements['nodes'])

            subdoc = element.content_document

            if subdoc is not None:
                # reset, we've got a document, but leave the top-level document in place
                # so we can go back to it
                self.reset(clear_initial_root=False)

                self.call('requestChildNodes', nodeId=subdoc.id)
                self._root_element = subdoc
                logging.debug('DOM root is now {}'.format(self.root))
                return self.root
            else:
                raise InvalidElement("Element does not contain a nested document")

        raise NoSuchElement("No elements matched the query selector '{}'".format(selector))

    def root_to_top(self):
        if self._you_never_forget_your_first_root_element:
            self._root_element = self._you_never_forget_your_first_root_element

        logging.debug('DOM root is now {}'.format(self.root))
        return self.root

    def remove_node(self, node_id):
        """
        Remove a specific node (and all children) from the DOM.

        - **id** (`str`):

            The DOM element id.
        """
        self.call('removeNode', nodeId=node_id)

    def focus(self, node_id):
        """
        Set the focus on a specific element.

        - **id** (`str`):

            The DOM element id.
        """
        self.call('focus', nodeId=node_id)

    def query(self, selector, node_id=None, reply_timeout=None):
        """
        Query for a single element.  Matched elements will be populated in the local DOM element
        cache for rapid retrieval in subsequent calls.

        - **selector** (`str`):

            The page element to query for, given as a CSS-style selector, an ID (e.g. "#myid"), or
            an XPath query (e.g.: "xpath://body/p").

        - **node_id** (`str`):

            If specified, the query will be performed relative to the element with the given
            Node ID.  Otherwise, the root element will be used.

        - **reply_timeout** (`int`):

            The timeout (in milliseconds) before the query raises a
            `webfriend.exceptions.TimeoutError`.

        #### Returns
        The matching `webfriend.rpc.dom.DOMElement`.

        #### Raises
        - `webfriend.exceptions.EmptyResult` if zero elements were matched, or
        - `webfriend.exceptions.TooManyResults` if more than one elements were matched.
        - `webfriend.exceptions.TimeoutError` if the query times out.
        """
        if node_id is None:
            node_id = self.root.id

        # perform the call
        matched_node_id = self.call(
            'querySelector',
            reply_timeout=reply_timeout,
            selector=self.prepare_selector(selector),
            nodeId=node_id
        ).get('nodeId')

        if not matched_node_id:
            raise NoSuchElement(
                "No elements matched the query selector '{}' under document {}".format(
                    selector,
                    node_id
                )
            )

        # we try to retrieve the element from the local element cache because they typically
        # come in as a series of events before the querySelector reply comes back, meaning that
        # by the time we get to this point we _should_ have cached definitions
        #
        # if we don't have a cached copy, then return a skeleton instance so that we can still
        # perform operations on the returned element
        #
        return self.element(matched_node_id, DOMElement(self, {
            'nodeId': matched_node_id,
        }))

    def query_all(self, selector, node_id=None, reply_timeout=None):
        """
        Query for all matching elements. Matched elements will be populated in the local DOM element
        cache for rapid retrieval in subsequent calls.

        - **selector** (`str`):

            The page element to query for, given as a CSS-style selector, an ID (e.g. "#myid"), or
            an XPath query (e.g.: "xpath://body/p").

        - **node_id** (`str`):

            If specified, the query will be performed relative to the element with the given
            Node ID.  Otherwise, the root element will be used.

        - **reply_timeout** (`int`):

            The timeout (in milliseconds) before the query raises a
            `webfriend.exceptions.TimeoutError`.

        #### Returns
        A `list` of matching `webfriend.rpc.dom.DOMElement` (zero or more).

        #### Raises
        - `webfriend.exceptions.TimeoutError` if the query times out.
        """
        if node_id is None:
            node_id = self.root.id

        # perform the call
        node_ids = self.call(
            'querySelectorAll',
            reply_timeout=reply_timeout,
            selector=self.prepare_selector(selector),
            nodeId=node_id
        ).get('nodeIds', [])

        # we try to retrieve the element from the local element cache because they typically
        # come in as a series of events before the querySelector reply comes back, meaning that
        # by the time we get to this point we _should_ have cached definitions
        #
        # if we don't have a cached copy, then return a skeleton instance so that we can still
        # perform operations on the returned element
        #
        return [
            self.element(i, DOMElement(self, {
                'nodeId': i,
            })) for i in node_ids
        ]

    def xpath(self, expression, wait_for_match=True, timeout=10000, interval=250):
        """
        Query the DOM for elements matching the given XPath expression.

        #### Arguments

        - **expression** (`str`):

            An XPath expression to evaluate within the current DOM.

        #### Returns
        A list of matching elements.
        """

        # call getDocument first so subsequent calls to performSearch work properly
        # (thanks @jamcplusplus for catching this)
        self.root
        started_at = time.time()

        while time.time() < (started_at + (timeout / 1000.0)):
            try:
                # perform the call
                handle = self.perform_search(expression)
                count = handle.get('resultCount', 0)

                if count:
                    return self.get_search_results(handle.get('searchId'), to_index=count)
                else:
                    self.discard_search_results(handle.get('searchId'))

            except (
                exceptions.TimeoutError,
                exceptions.ProtocolError
            ):
                pass

            if not wait_for_match:
                break

            time.sleep(interval / 1000.0)

        return False

    def select_nodes(self, selector, wait_for_match=True, timeout=10000, interval=250):
        """
        Polls the DOM for an element that matches the given selector.  Either the element will be
        found and returned within the given timeout, or a TimeoutError will be raised.

        #### Arguments

        - **selector** (`str`):

            The CSS-style selector that specifies the DOM element to look for.

        - **timeout** (`int`):

            The number of milliseconds to wait for the element to be returned.

        - **interval** (`int`):

            The polling interval, in milliseconds, used for rechecking for the element.

        #### Returns
        `webfriend.rpc.dom.DOMElement`

        #### Raises
        `webfriend.exceptions.TimeoutError`
        """
        started_at = time.time()

        while time.time() <= (started_at + (timeout / 1000.0)):
            try:
                elements = self.query_all(selector, reply_timeout=interval)

                # if we're not waiting or we are and we've got elements, return
                if not wait_for_match or len(elements):
                    results = {
                        'selector': selector,
                        'count':    len(elements),
                        'nodes': [
                            self.element(e.id, e) for e in elements
                        ],
                    }

                    if not len(results['nodes']):
                        return False

                    return results
            except (
                exceptions.TimeoutError,
                exceptions.ProtocolError
            ):
                pass

            time.sleep(interval / 1000.0)

        return False

    def on_child_nodes(self, event):
        for node in event.get('nodes', []):
            element = DOMElement(self, node)

            logging.debug('Adding node {} {}'.format(
                element.id,
                element
            ))

            self._elements[node['nodeId']] = element

            if self.has_element(element.parent_id):
                self.element(element.parent_id).child_ids.add(element.id)

    def on_child_inserted(self, event):
        definition = event.get('node')
        parent_id = event.get('parentNodeId')

        element = DOMElement(self, definition)
        self._elements[element.id] = element

        logging.debug('Inserting node {} {}'.format(
            element.id,
            element
        ))

        if self.has_element(parent_id):
            try:
                self.element(parent_id).child_ids.add(element.id)
            except KeyError:
                pass

    def on_child_removed(self, event):
        node_id = event.get('nodeId')
        parent_id = event.get('parentNodeId')

        if self.has_element(parent_id):
            try:
                self.element(parent_id).child_ids.remove(node_id)
            except KeyError:
                pass

        if self.has_element(node_id):
            logging.debug('Removing node {} {}'.format(
                self._elements[node_id].id,
                self._elements[node_id]
            ))

            del self._elements[node_id]

    # def on_set_attribute_value()

    @property
    def resources(self):
        return self.tab._network_requests

    def get_resource(self, url=None, request_id=None):
        """
        Return a `dict` describing a specific network resource, or false if none was found.

        #### Arguments

        - **url** (`str`, optional):

            If specified, this is the URL of the requested resource to retrieve.

        - **request_id** (`str`, optional):

            If specified, this is the specific resource to retrieve by Request ID.

        #### Returns
        A `dict` representing the resource, or _false_ if a match was not found.
        """
        if not request_id and not url:
            raise ValueError("Must specify either url or request_id")

        if request_id:
            return self.tab.get_network_request(request_id)
        elif url:
            matching_requests = []

            for _, request in self.resources.items():
                try:
                    if url == request['request']['url']:
                        return request
                    elif url in request['request']['url']:
                        matching_requests.append(request)
                except KeyError:
                    continue

            if len(matching_requests) == 1:
                return matching_requests[0]
            elif len(matching_requests) > 1:
                raise exceptions.TooManyResults(
                    'The URL pattern "{}" matched {} requests, need at most 1'.format(
                        url,
                        len(matching_requests)
                    )
                )

        return False

    @classmethod
    def prepare_selector(cls, selector):
        return selector

    @classmethod
    def ensure_unique_element(cls, selector, elements):
        if elements is False:
            raise exceptions.EmptyResult("No elements matched the selector: {}".format(
                selector
            ))

        if isinstance(elements, dict):
            nodes = elements.get('nodes', [])
        else:
            nodes = elements

        if not len(nodes):
            raise exceptions.EmptyResult("No elements matched the selector: {}".format(
                selector
            ))

        elif len(nodes) > 1:
            raise exceptions.TooManyResults(
                "Selector {} is ambiguous, matched {} elements".format(
                    selector, len(nodes)
                )
            )

        return nodes[0]

    def perform_search(self, query, shadow=True):
        params = {
            'query': query,
        }

        if shadow:
            params['includeUserAgentShadowDOM'] = shadow

        return self.call('performSearch', **params)

    def get_search_results(self, search_id, from_index=0, to_index=0):
        node_ids = self.call(
            'getSearchResults',
            searchId=search_id,
            fromIndex=from_index,
            toIndex=to_index
        ).get('nodeIds', [])

        return [
            self.element(i, DOMElement(self, {
                'nodeId': i,
            })) for i in node_ids
        ]

    def discard_search_results(self, search_id):
        self.call('discardSearchResults', searchId=search_id)

    def push_node_by_path_to_frontend(self, path):
        node_ids = self.call('pushNodeByPathToFrontend', path=path).get('nodeIds', [])

        return [
            self.element(i, DOMElement(self, {
                'nodeId': i,
            })) for i in node_ids
        ]

    def push_node_by_backend_ids_to_frontend(self, backend_node_ids):
        return self.call('pushNodesByBackendIdsToFrontend', backendNodeIds=backend_node_ids)

    def request_node(self, remote_object):
        return self.call('requestNode', objectId=remote_object).get('nodeId') class Emulation(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Emulation
    """
    domain = 'Emulation'
    supports_events = False

    valid_orientations = [
        'portraitPrimary',
        'portraitSecondary',
        'landscapePrimary',
        'landscapeSecondary',
    ]

    def set_device_metrics_override(
        self,
        width=0,
        height=0,
        device_scale_factor=0,
        mobile=False,
        fit_window=False,
        position_x=0,
        position_y=0,
        screen_width=0,
        screen_height=0,
        screen_orientation_type=None,
        screen_orientation_angle=None
    ):
        params = {
            'width':             int(math.ceil(width)),
            'height':            int(math.ceil(height)),
            'deviceScaleFactor': device_scale_factor,
            'mobile':            mobile,
            'fitWindow':         fit_window,
        }

        if position_x:
            params['positionX'] = position_x

        if position_y:
            params['positionY'] = position_y

        if screen_width:
            params['screenWidth']  = screen_width

        if screen_height:
            params['screenHeight'] = screen_height

        if screen_orientation_type is not None:
            params['screenOrientation'] = {
                'type':  screen_orientation_type,
                'angle': (screen_orientation_angle or 0),
            }

        self.call('setDeviceMetricsOverride', **params)

    def clear_device_metrics_override(self):
        self.call('clearDeviceMetricsOverride')

    def force_viewport(self, x=0, y=0, scale=1.0):
        self.call('forceViewport', x=x, y=y, scale=scale)

    def reset_viewport(self):
        self.call('resetViewport')

    def set_page_scale_factor(self, factor):
        self.call('setPageScaleFactor', pageScaleFactor=factor)

    def reset_page_scale_factor(self):
        self.call('resetPageScaleFactor')

    def set_visible_size(self, width, height):
        self.call(
            'setVisibleSize',
            width=int(math.ceil(width)),
            height=int(math.ceil(height))
        )

    def enable_script_execution(self):
        self.call('setScriptExecutionDisabled', value=False)

    def disable_script_execution(self):
        self.call('setScriptExecutionDisabled', value=True)

    def set_geolocation_override(self, latitude, longitude, accuracy):
        self.call(
            'setGeolocationOverride',
            latitude=latitude,
            longitude=longitude,
            accuracy=accuracy
        )

    def clear_geolocation_override(self):
        self.call('clearGeolocationOverride')

    def enable_touch_emulation(self, configuration=None):
        params = {
            'enabled': True,
        }

        if configuration is not None:
            params['configuration'] = configuration

        self.call('setTouchEmulationEnabled', **params)

    def set_emulated_media(self, media):
        self.call('setEmulatedMedia', media=media)

    def set_cpu_throttling_rate(self, rate):
        self.call('setCPUThrottlingRate', rate=rate)

    def can_emulate(self):
        return self.call_boolean_response('canEmulate')

    def set_default_background_color_override(self, red, green, blue, alpha=1.0):
        self.call('setDefaultBackgroundColorOverride', color={
            'r': red,
            'g': green,
            'b': blue,
            'a': alpha,
        })

    def clear_default_background_color_override(self):
        self.call('setDefaultBackgroundColorOverride') class Input(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Input
    """

    domain = 'Input'
    supports_events = False
    key_event_types = ['keyDown', 'keyUp', 'rawKeyDown', 'char']
    key_events_with_text = ['keyDown', 'char']
    key_args = [
        ('timestamp', 'timestamp'),
        ('unmodified_text', 'unmodifiedText'),
        ('key_identifier', 'keyIdentifier'),
        ('code', 'code'),
        ('dom_key', 'key'),
        ('windows_virtual_keycode', 'windowsVirtualKeyCode'),
        ('native_virtual_keycode', 'nativeVirtualKeyCode'),
        ('is_autorepeat', 'autoRepeat'),
        ('is_keypad', 'isKeypad'),
        ('is_system', 'isSystemKey'),
    ]

    mouse_event_types = ['mousePressed', 'mouseReleased', 'mouseMoved']
    mouse_buttons = ['left', 'middle', 'right']

    def ignore_input(self):
        self.call('setIgnoreInputEvents', ignore=True)

    def accept_input(self):
        self.call('setIgnoreInputEvents', ignore=False)

    def dispatch_key_event(
        self,
        event_type='keyDown',
        alt=False,
        control=False,
        meta=False,
        shift=False,
        timestamp=None,
        text=None,
        unmodified_text=None,
        key_identifier=None,
        code=None,
        dom_key=None,
        windows_virtual_keycode=None,
        native_virtual_keycode=None,
        is_autorepeat=False,
        is_keypad=False,
        is_system=False
    ):
        """
        See: https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
        """
        params = {
            'type':      event_type,
            'modifiers': 0,
        }

        if event_type not in self.key_event_types:
            raise ValueError("event_type must be one of: {}".format(
                ', '.join(self.key_event_types)
            ))

        if event_type in self.key_events_with_text:
            if text is None:
                raise ValueError("text must be specified for '{}' event".format(event_type))
            else:
                params['text'] = text

        params['modifiers'] |= 1 if alt else 0
        params['modifiers'] |= 2 if control else 0
        params['modifiers'] |= 4 if meta else 0
        params['modifiers'] |= 8 if shift else 0

        for local, param_name in self.key_args:
            if locals()[local]:
                params[param_name] = locals()[local]

        self.call('dispatchKeyEvent', **params)

    def type_text(
        self,
        text,
        file=False,
        alt=False,
        control=False,
        meta=False,
        shift=False,
        is_keypad=False,
        key_down_time=30,
        key_down_jitter=0,
        delay=50,
        delay_jitter=0
    ):
        """
        Input the given textual data as keyboard input into the browser in its current state.

        #### Arguments

        - **text** (`str`, optional):

            The text string to input, one character or symbol at a time.

        - **file** (`str`, optional):

            If specified, read the text to input from the named file.

        - **alt**, **control**, **meta**, **shift** (`bool`):

            Declares that the Alt, Control, Meta/Command, and/or Shift keys (respectively) are
            depressed at the time of the click action.

        - **is_keypad** (`bool`, optional):

            Whether the text being input is issued via the numeric keypad or not.

        - **key_down_time** (`int`, optional):

            How long, in milliseconds, that each individual keystroke will remain down for.

        - **key_down_jitter** (`int`, optional):

            An amount of time, in milliseconds, to randomly vary the **key_down_time** duration
            from within each keystroke.

        - **delay** (`int`, optional):

            How long, in milliseconds, to wait between issuing individual keystrokes.

        - **delay_jitter** (`int`, optional):

            An amount of time, in milliseconds, to randomly vary the **delay** duration
            from between keystrokes.

        #### Returns
        The text that was submitted.
        """
        if file:
            text = open(text, 'r').read()
        elif text is None:
            raise ValueError("Must specify either 'text' or 'file' to read input from.")

        for char in text:
            self.dispatch_key_event(
                event_type='keyDown',
                text=char,
                alt=alt,
                control=control,
                meta=meta,
                shift=shift,
                is_keypad=is_keypad
            )

            # simulate the duration of the keypress
            if key_down_time:
                time.sleep((key_down_time * key_down_jitter * random()) / 1e3)

            # send the keyUp event
            self.dispatch_key_event(event_type='keyUp')

            # simulate the time between key presses
            if delay:
                time.sleep((delay * random() * delay_jitter) / 1e3)

        return text

    def dispatch_mouse_event(
        self,
        x,
        y,
        event_type='mouseMoved',
        button=None,
        alt=False,
        control=False,
        meta=False,
        shift=False,
        timestamp=None,
        click_count=1,
    ):
        """
        See https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
        """
        params = {
            'type':      event_type,
            'x':         int(x),
            'y':         int(y),
            'modifiers': 0,
        }

        if event_type not in self.mouse_event_types:
            raise ValueError("event_type must be one of: {}".format(
                ', '.join(self.mouse_event_types)
            ))

        if button and button not in self.mouse_buttons:
            raise ValueError("button must be one of: {}".format(
                ', '.join(self.mouse_buttons)
            ))

        params['modifiers'] |= 1 if alt else 0
        params['modifiers'] |= 2 if control else 0
        params['modifiers'] |= 4 if meta else 0
        params['modifiers'] |= 8 if shift else 0

        if timestamp:
            params['timestamp'] = timestamp

        if button:
            params['button'] = button

        if click_count:
            params['clickCount'] = click_count

        self.call('dispatchMouseEvent', **params)

    def click_at(
        self,
        x,
        y,
        button='left',
        alt=False,
        control=False,
        meta=False,
        shift=False,
        move_to=True,
        drag=False,
        start_x=None,
        start_y=None,
        drag_start_delay=50,
        move_finish_delay=50,
        click_duration=50
    ):
        """
        Dispatch low-level mouse events at the given coordinates to perform a variety of actions.

        #### Arguments

        - **x**, **y** (`int`):

            The X- and Y-coordinates describing the click location.

        **button** (`str`):

            Which mouse button the event will be for; one of: `left`, `middle`, or `right`.

        **alt**, **control**, **meta**, **shift** (`bool`):

            Declares that the Alt, Control, Meta/Command, and/or Shift keys (respectively) are
            depressed at the time of the click action.

        **move_to** (`bool`):

            Simulates the mouse moving from the starting location to the destination **x**/**y**
            coordinates.

        **drag** (`bool`):

            If specified, the click operation will be a "drag and drop" style movement, wherein
            a set of initial coordinates receive a mouse press, then (with the button still
            pressed) the mouse will move to the destination coordinates, and _then_ the button
            release event will fire.

        **start_x** (`int`):

            For **drag** operations, the starting X-coordinate.

        **start_y** (`int`):

            For **drag** operations, the starting Y-coordinate.

        **drag_start_delay** (`int`):

            For **drag** operations, how long to wait (in milliseconds) after the initial mouse
            button press before starting to move.

        **move_finish_delay** (`int`):

            If simulating mouse movement with **move_to**, how long to wait (in milliseconds)
            _after_ the move is completed, but before clicking or releasing.

        **click_duration** (`int`):

            If non-zero and not dragging, how long (in milliseconds) the mouse button should be
            held for when  performing the click; i.e.: how long between the mouse press and mouse
            release events.
        """

        is_dragging = False

        # dragging entails pressing the mouse, moving it, then releasing it
        if drag:
            if start_x is not None and start_y is not None:
                self.dispatch_mouse_event(
                    start_x,
                    start_y,
                    event_type='mousePressed',
                    button=button,
                    alt=alt,
                    control=control,
                    meta=meta,
                    shift=shift,
                )

                is_dragging = True
                time.sleep(drag_start_delay / 1e3)

            else:
                raise AttributeError("If drag is specified, start_x and start_y must be given as well")

        # move the mouse, optionally with a button pressed if we're dragging
        if move_to:
            self.dispatch_mouse_event(
                x,
                y,
                event_type='mouseMoved',
                button=(button if is_dragging else None),
                alt=alt,
                control=control,
                meta=meta,
                shift=shift,
            )

            time.sleep(move_finish_delay / 1e3)

        # if we're dragging, then release the button now that we're where we're going
        if is_dragging:
            self.dispatch_mouse_event(
                x,
                y,
                event_type='mouseReleased',
                button=button,
                alt=alt,
                control=control,
                meta=meta,
                shift=shift,
            )

            is_dragging = False
        else:
            # otherwise, click the button
            self.dispatch_mouse_event(
                x,
                y,
                event_type='mousePressed',
                button=button,
                alt=alt,
                control=control,
                meta=meta,
                shift=shift,
            )

            if click_duration:
                # hold it.....
                time.sleep(click_duration / 1e3)

            # and release
            self.dispatch_mouse_event(
                x,
                y,
                event_type='mouseReleased',
                button=button,
                alt=alt,
                control=control,
                meta=meta,
                shift=shift,
            ) class Network(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Network
    """
    domain = 'Network'

    connection_types = [
        'none', 'cellular2g', 'cellular3g', 'cellular4g', 'bluetooth', 'ethernet', 'wifi', 'wimax',
        'other'
    ]

    def set_user_agent(self, user_agent):
        self.call('setUserAgentOverride', userAgent=user_agent)

    def set_headers(self, headers):
        if not isinstance(headers, dict):
            raise AttributeError("Headers must be specified as a dict")

        self.call('setExtraHTTPHeaders', headers=headers)

    def get_response_body(self, request_id):
        reply = self.call('getResponseBody', requestId=request_id)
        body = reply.get('body')

        if not body:
            return None

        if reply.get('base64Encoded') is True:
            body = b64decode(body)

        return body

    @property
    def can_clear_browser_cache(self):
        return self.call_boolean_response('canClearBrowserCache')

    @property
    def can_clear_browser_cookies(self):
        return self.call_boolean_response('canClearBrowserCookies')

    @property
    def can_emulate_network_conditions(self):
        return self.call_boolean_response('canEmulateNetworkConditions')

    def clear_browser_cache(self):
        self.call('clearBrowserCache')

    def clear_browser_cookies(self):
        self.call('clearBrowserCookies')

    def emulate_network_conditions(
        self,
        offline=False,
        latency_ms=0,
        throughput_down_bps=None,
        throughput_up_bps=None,
        connection_type=None,
    ):
        params = {
            'offline': offline,
            'latency':            latency_ms,
            'downloadThroughput': 10e9,
            'uploadThroughput':   10e9,
        }

        if connection_type is not None:
            if connection_type not in self.connection_types:
                raise AttributeError("Connection Type must be one of: {}".format(
                    ', '.join(self.connection_types)
                ))

            params['connectionType'] = connection_type

        self.call('emulateNetworkConditions', **params)

    def disable_cache(self):
        self.call('setCacheDisabled', cacheDisabled=True)

    def enable_cache(self):
        self.call('setCacheDisabled', cacheDisabled=False)

    def set_blocked_urls(self, urls):
        if not isinstance(urls, list):
            raise AttributeError("Blocked URLs must be a list")
        self.call('setBlockedURLs', urls=urls)

    def replay_xhr(self, request_id):
        self.call('replayXHR', requestId=request_id)

    def get_cookies(self, urls=None):
        if isinstance(urls, list):
            reply = self.call('getCookies', urls=urls)
        else:
            reply = self.call('getAllCookies')

        return [
            Cookie(self, c) for c in reply.get('cookies', [])
        ]

    def delete_cookie(self, url, name):
        self.call('deleteCookie', cookieName=name, url=url)

    def set_cookie(
        self,
        url,
        name,
        value,
        domain=None,
        path=None,
        secure=None,
        http_only=None,
        same_site=None,
        expires=None
    ):
        """
        Create or update a cookie based on the given values.
        """
        params = {
            'url':   url,
            'name':  name,
            'value': value,
        }

        if domain is not None:
            params['domain'] = domain

        if path is not None:
            params['path'] = path

        if isinstance(secure, bool):
            params['secure'] = secure

        if isinstance(http_only, bool):
            params['httpOnly'] = http_only

        if isinstance(same_site, basestring):
            params['sameSite'] = same_site

        if isinstance(expires, int):
            params['expirationDate'] = expires
        elif isinstance(expires, datetime):
            params['expirationDate'] = int(expires.strftime('%s'))

        return self.call_boolean_response(self.call('setCookie', **params), 'success')

    def get_certificate(self, origin):
        # getCertificate
        return Exception("NOT IMPLEMENTED") class Overlay(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Overlay
    """
    domain = 'Overlay'
    supports_events = False

    default_highlight_color = {
        'r': 0,
        'g': 99,
        'b': 193,
        'a': 0.95,
    }

    default_highlight_outline_color = {
        'r': 255,
        'g': 255,
        'b': 255,
        'a': 0,
    }

    show_rulers = False
    show_extension_lines = False
    show_info_tooltip = False

    def show_paint_rectangles(self):
        self.call('setShowPaintRects', result=True)

    def hide_paint_rectangles(self):
        self.call('setShowPaintRects', result=False)

    def show_debug_borders(self):
        self.call('setShowDebugBorders', show=True)

    def hide_debug_borders(self):
        self.call('setShowDebugBorders', show=False)

    def show_fps_counter(self):
        self.call('setShowFPSCounter', show=True)

    def hide_fps_counter(self):
        self.call('setShowFPSCounter', show=False)

    def show_scroll_bottlenecks(self):
        self.call('setShowScrollBottleneckRects', show=True)

    def hide_scroll_bottlenecks(self):
        self.call('setShowScrollBottleneckRects', show=False)

    def show_size_on_resize(self):
        self.call('setShowViewportSizeOnResize', show=True)

    def hide_size_on_resize(self):
        self.call('setShowViewportSizeOnResize', show=False)

    def suspend(self):
        self.call('setSuspended', suspended=True)

    def resume(self):
        self.call('setSuspended', suspended=False)

    def enable_inspect_mode(self):
        self.call('setInspectMode', mode='searchForNode')

    def disable_inspect_mode(self):
        self.call('setInspectMode', mode='none')

    def highlight_rect(self, x, y, width, height, color=None, outline_color=None):
        if not isinstance(color, dict):
            color = self.default_highlight_color

        if not isinstance(outline_color, dict):
            outline_color = self.default_highlight_outline_color

        self.call(
            'highlightRect',
            x=x,
            y=y,
            width=width,
            height=height,
            color=color,
            outlineColor=outline_color
        )

    def highlight_node(
        self,
        node_id=None,
        backend_node_id=None,
        object_id=None,
        show_rulers=None,
        show_extension_lines=None,
        show_info_tooltip=None,
        content_color=None,
        padding_color=None,
        border_color=None,
        margin_color=None,
        event_target_color=None,
        shape_color=None,
        shape_margin_color=None,
    ):
        params = {
            'highlightConfig': {},
        }

        if node_id:
            params['nodeId'] = node_id
        elif backend_node_id:
            params['backendNodeId'] = backend_node_id
        elif object_id:
            params['objectId'] = object_id
        else:
            raise ValueError("Must specify one of node_id or object_id")

        if not show_rulers:
            params['highlightConfig']['showRulers'] = self.show_rulers

        if not show_info_tooltip:
            params['highlightConfig']['showInfo'] = self.show_info_tooltip

        if not show_extension_lines:
            params['highlightConfig']['showExtensionLines'] = self.show_extension_lines

        if isinstance(content_color, dict):
            params['highlightConfig']['contentColor'] = content_color
        else:
            params['highlightConfig']['contentColor'] = self.default_highlight_color

        if isinstance(padding_color, dict):
            params['highlightConfig']['paddingColor'] = padding_color

        if isinstance(border_color, dict):
            params['highlightConfig']['borderColor'] = border_color

        if isinstance(margin_color, dict):
            params['highlightConfig']['marginColor'] = margin_color

        if isinstance(event_target_color, dict):
            params['highlightConfig']['eventTargetColor'] = event_target_color

        if isinstance(shape_color, dict):
            params['highlightConfig']['shapeColor'] = shape_color

        if isinstance(shape_margin_color, dict):
            params['highlightConfig']['shapeMarginColor'] = shape_margin_color

        self.call('highlightNode', **params)

    def hide_highlight(self):
        self.call('hideHighlight') class Page(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Page
    """
    domain = 'Page'
    capture_formats = ['jpeg', 'png']

    def navigate(self, url, referrer=None, transition_type=None, clear_request_cache=True):
        params = {
            'url': url,
        }

        if referrer:
            params['referrer'] = referrer

        if transition_type:
            params['transitionType'] = transition_type

        # clear the network request cache between explicit page loads
        if clear_request_cache is True:
            self.tab.reset_network_request_cache()

        init_frame_id = self.call('navigate', **params).get('frameId')

        if url.startswith('http'):
            # block until the network first request finished (success or failure)
            self.tab.wait_for('Network.loadingFinished')

            # get the request corresponding to the frameID (this is the request for the given URL)
            net_request = self.tab.get_network_request(init_frame_id)

            if net_request:
                if net_request.get('success') is True:
                    event = net_request.get('response', {})
                    response = event.get('response', {})
                    status = response['status']

                    if status < 400:
                        return response
                    else:
                        exc = exceptions.HttpError('HTTP {}'.format(status))
                        exc.response = response
                        raise exc

                else:
                    pre_event = net_request.get('before', {})
                    fail_event = net_request.get('response', {})
                    msg = fail_event.get('errorText', 'UNKNOWN')
                    exc = exceptions.NetworkError(msg)
                    exc.event = fail_event
                    exc.url = pre_event.get('documentURL')
                    raise exc
        else:
            return None

    def reload(self, ignore_cache=False, eval_on_load=None):
        params = {
            'ignoreCache': ignore_cache,
        }

        if isinstance(eval_on_load, basestring):
            params['scriptToEvaluateOnLoad'] = eval_on_load

        self.call('reload', **params)

    def stop(self):
        self.call('stopLoading')

    def dialog(self, accept, text=None):
        params = {
            'accept': accept,
        }

        if isinstance(text, basestring):
            params['promptText'] = text

        self.call('handleJavaScriptDialog', **params)

    def capture_screenshot(
        self,
        destination,
        format=None,
        quality=None,
        from_surface=True,
        reply_timeout=None,
        clip=None
    ):
        params = {}

        if format is not None:
            if format in self.capture_formats:
                params['format'] = format
            else:
                raise AttributeError("Invalid format, must be one of: {}".format(
                    ', '.join(self.capture_formats)
                ))

        if format == 'jpeg' and isinstance(quality, int):
            params['quality'] = quality

        if from_surface is False:
            params['fromSurface'] = False

        if isinstance(clip, dict):
            params['clip'] = clip

        reply = self.call('captureScreenshot', reply_timeout=reply_timeout, **params)

        if isinstance(destination, basestring):
            data = open(destination, 'w')
        else:
            data = destination

        body = b64decode(reply.get('data'))
        data.write(body)

        return len(body)

    def start_screencast(
        self,
        format='png',
        jpeg_quality=85,
        max_width=None,
        max_height=None,
        every_nth=None,
        duration=None,
        destination=None,
        filename_format=None,
        additional_values=None
    ):
        if hasattr(self, '_active_screencast'):
            if self._active_screencast:
                raise Exception("There is already an active screencast session running.")
        else:
            self._active_screencast = None

        if not filename_format:
            filename_format = '{page_id}_{screencast_id}_{frame_number}.{extension}'

        params = {
            'format': format,
        }

        if not additional_values:
            additional_values = {}

        if format == 'png':
            additional_values['extension'] = 'png'
        elif format == 'jpeg':
            additional_values['extension'] = 'jpg'

            if jpeg_quality:
                params['quality'] = jpeg_quality

        if max_width:
            params['maxWidth'] = max_width

        if max_height:
            params['maxHeight'] = max_height

        if every_nth:
            params['everyNthFrame'] = every_nth

        if destination:
            abs_dir = os.path.abspath(destination)

            if not os.path.isdir(abs_dir):
                os.makedirs(abs_dir)

            # generate the frame handler function for this screencast
            handler = self.get_receive_screencast_frame_handler(
                utils.random_string(8),
                abs_dir,
                filename_format,
                **{
                    'duration':                 duration,
                    'poll_interval_multiplier': (every_nth or 1),
                    'extra':                    additional_values,
                }
            )

            # register the event handler
            self._active_screencast = self.on('screencastFrame', handler)

        # start the screencast
        self.call('startScreencast', **params)

        # wait for the screencast to start before returning
        self.tab.wait_for('Page.screencastVisibilityChanged')

        # note the time
        self._active_screencast_started_at = time.time()

        return {
            'started_at': self._active_screencast_started_at,
            'handler_id': self._active_screencast,
        }

    def get_receive_screencast_frame_handler(
        self,
        screencast_id,
        destination,
        filename_format,
        started_at=None,
        duration=None,
        poll_interval_multiplier=1,
        extra=None
    ):
        self._active_screencast_frame_count = 0

        def handle(frame):
            frame_session_id = frame.get('sessionId')

            filename = filename_format.format(**{
                'page_id':       self.tab.frame_id.replace('-', ''),
                'screencast_id': str(screencast_id),
                'frame_number':  self._active_screencast_frame_count,
                'metadata':      frame.get('metadata'),
                'extension':     extra.get('extension'),
                'extra':         (extra if isinstance(extra, dict) else {}),
            })

            # write the data out to the file
            with open(os.path.join(destination, filename), 'w') as file:
                data = frame.get('data')

                if data and len(data):
                    file.write(b64decode(data))

            # acknowledge the frame
            self.screencast_frame_ack(frame_session_id)
            logging.debug('Wrote frame {} to {}'.format(self._active_screencast_frame_count, filename))

            self._active_screencast_frame_count += 1

            # if we only want to screencast for a specific duration, check the
            # times and stop if necessary
            if duration:
                if hasattr(self, '_active_screencast_started_at'):
                    if time.time() > (self._active_screencast_started_at + (duration / 1e3)):
                        self.stop_screencast()

        return handle

    def stop_screencast(self, timeout=10000):
        if self._active_screencast:
            logging.debug('Stopping screencast {}'.format(self._active_screencast))
            self.remove_handler(self._active_screencast)
            self._active_screencast = None

        self.call('stopScreencast')
        return True

    def screencast_frame_ack(self, session_id):
        self.call('screencastFrameAck', sessionId=session_id)
        return True

    # addScriptToEvaluateOnLoad[ scriptSource=string ]
    #   -> string(identifier)
    #
    # removeScriptToEvaluateOnLoad
    #   -> string(identifier)
    #
    # setAutoAttachToCreatedPages [ autoAttach=bool ] -> Nothing
    #
    # getNavigationHistory
    #   -> integer(currentIndex)
    #   -> []entries
    #      @NavigationEntry
    #       id=integer           Unique id of the navigation history entry.
    #       url=string           URL of the navigation history entry.
    #       userTypedURL=string  URL that the user typed in the url bar.
    #       title=string         Title of the navigation history entry.
    #       transitionType=(
    #           link,
    #           typed,
    #           auto_bookmark,
    #           auto_subframe,
    #           manual_subframe,
    #           generated,
    #           auto_toplevel,
    #           form_submit,
    #           reload,
    #           keyword,
    #           keyword_generated,
    #           other
    #       )
    #
    # navigateToHistoryEntry [ entryId=integer ]
    #
    # getResourceTree
    # getResourceContent
    # searchInResource
    #
    # setDocumentContent [ frameId=string html=string ] class Runtime(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Runtime
    """
    domain = 'Runtime'
    supports_events = False

    def evaluate(
        self,
        expression,
        object_group=None,
        include_command_line_api=True,
        silent=None,
        context_id=None,
        return_by_value=True,
        generate_preview=None,
        user_gesture=None,
        await_promise=None,
        obj_own_properties=True,
        obj_accessors_only=False,
        obj_preview=False,
        wrapper_fn=True,
        data=None,
        calling_context=None
    ):
        if isinstance(data, dict):
            data_serialized = json.dumps(data)
        else:
            data_serialized = '{}'  # empty javascript object literal

        if wrapper_fn:
            fn_name = 'webfriend_{}_fn'.format(str(uuid4()).replace('-', '_'))

            # declare variable to hold the anonymous function, bound to the data that was passed in
            # and call the function
            expression = 'var {} = function(){{ {} }}.bind({}); {}();'.format(
                fn_name,
                expression,
                data_serialized,
                fn_name,
                fn_name
            )

        params = {
            'expression':    expression,
            'returnByValue': return_by_value,
        }

        if object_group is not None:
            params['objectGroup'] = object_group

        if include_command_line_api is not None:
            params['includeCommandLineAPI'] = include_command_line_api

        if silent is not None:
            params['silent'] = silent

        if context_id is not None:
            params['contextId'] = context_id

        if generate_preview is not None:
            params['generatePreview'] = generate_preview

        if user_gesture is not None:
            params['userGesture'] = user_gesture

        if await_promise is not None:
            params['awaitPromise'] = await_promise

        reply = self.call('evaluate', **params)

        if 'exceptionDetails' in reply.result:
            exception   = reply.result['exceptionDetails']
            detail      = exception.get('exception', {})
            line_number = exception['lineNumber'] + 1
            col_number  = exception['columnNumber'] + 1  # add 1 because javascript columns are zero-based, we're not

            if calling_context:
                if hasattr(calling_context, 'line'):
                    line_number += calling_context.line

            # provide LOTS of context from both where the injected script failed AND where
            # the script was injected from in our local call stack.
            raise EmbeddedScriptError('{}'.format(
                detail.get(
                    'description',
                    detail.get(
                        'value',
                        exception.get(
                            'text',
                            'Unknown Error'
                        )
                    )
                ),
                line_number
            ), line=line_number, col=col_number)

        else:
            result = reply.result.get('result', {})

            if return_by_value:
                return result.get('value')
            elif 'objectId' in result:
                return self.get_properties(
                    result['objectId'],
                    own_properties=obj_own_properties,
                    accessors_only=obj_accessors_only,
                    preview=obj_preview
                )
            else:
                return None

    def call_function_on(self, object_id, fn_name, arguments=None, return_by_value=False):
        return self.call(
            'callFunctionOn',
            objectId=object_id,
            functionDeclaration=fn_name,
            arguments=[{
                'value': v,
            } for v in (arguments or [])],
            returnByValue=return_by_value
        )

    def get_properties(
        self,
        object_id,
        own_properties=False,
        accessors_only=False,
        preview=False
    ):
        params = {
            'objectId': object_id,
        }

        if own_properties:
            params['ownProperties'] = True

        if accessors_only:
            params['accessorPropertiesOnly'] = True

        if preview:
            params['generatePreview'] = True

        return self.call('getProperties', **params).get('result')

    def release_object(self, object_id):
        self.call('releaseObject', objectId=object_id)

    def release_object_group(self, object_group):
        self.call('releaseObjectGroup', objectGroup=object_group) class Target(Base):
    """
    See: https://chromedevtools.github.io/devtools-protocol/tot/Target
    """
    domain = 'Target'
    supports_events = False

    def enable_auto_attach(self, wait_for_debugger=True):
        self.call('setAutoAttach', autoAttach=True, waitForDebuggerOnStart=wait_for_debugger)

    def disable_auto_attach(self):
        self.call('setAutoAttach', autoAttach=False)

    def enable_discover_targets(self):
        self.call('setDiscoverTargets', discover=True)

    def disable_discover_targets(self):
        self.call('setDiscoverTargets', discover=False)

    def enable_attach_to_frames(self):
        self.call('setAttachToFrames', value=True)

    def disable_attach_to_frames(self):
        self.call('setAttachToFrames', value=False)

    def send_message_to_target(self, target_id, message):
        self.call('sendMessageToTarget', targetId=target_id, message=message)

    def get_targets(self):
        return self.call('getTargets')

    def get_target_info(self, target_id):
        return self.call('getTargetInfo', targetId=target_id)

    def create_target(self, url, width=None, height=None, browser_context_id=None):
        params = {
            'url': url,
        }

        if width:
            params['width'] = width

        if height:
            params['height'] = height

        if browser_context_id:
            params['browserContextId'] = browser_context_id

        return self.call('createTarget', **params)

    def activate_target(self, target_id):
        self.call('activateTarget', targetId=target_id)

    def close_target(self, target_id):
        self.call('closeTarget', targetId=target_id)

    def attach_to_target(self, target_id):
        return self.call_boolean_response('attachToTarget', field='success', targetId=target_id)

    def detach_from_target(self, target_id):
        self.call('detachFromTarget', targetId=target_id)

    def create_browser_context(self):
        return self.call('createBrowserContext')

    def dispose_browser_context(self, browser_context_id):
        return self.call_boolean_response(
            'disposeBrowserContext',
            field='success',
            browserContextId=browser_context_id
        ) class WebfriendError(Exception):
class ProtocolError(WebfriendError):
class TimeoutError(WebfriendError):
class NotFound(WebfriendError):
class NetworkError(WebfriendError):
class HttpError(WebfriendError):
class EmptyResult(WebfriendError):
class TooManyResults(WebfriendError):
class NotImplemented(WebfriendError):
    def __init__(self, message, **kwargs): def _default(self, obj):
from webfriend.rpc import (
    Base,
    Browser,
    Console,
    DOM,
    Emulation,
    Input,
    Network,
    Overlay,
    Page,
    Reply,
    Runtime,
    Target,
)
from webfriend import exceptions
from webfriend.utils import patch_json  # noqa
from Queue import Queue, Empty, Full
from threading import Thread
import json
import time
import websocket
import logging

            The name of the event to wait for.

        - **timeout** (`int`):

            The timeout, in milliseconds, before raising a `webfriend.exceptions.TimeoutError`.

        #### Returns
        `webfriend.rpc.event.Event`

        #### Raises
        `webfriend.exceptions.TimeoutError`
        """
        wfc = self.wait_for_caller_response(event_name, **kwargs)
        started_at = time.time()

        for event in wfc:
            try:
                wfc.send(True)
            except StopIteration:
                pass

            return {
                'sequence': [event],
                'duration': (time.time() - started_at),
            }

    def wait_for_idle(self, idle, events=[], timeout=30000, poll_interval=250):
        """
        Blocks for a specified amount of time _after_ an event has been received, or until
        **timeout** elapses (whichever comes first).

        This is useful for waiting for events to occur after performing an action, then giving some
        amount of time for those events to "settle" (e.g.: allowing the page time to react to those
        events without knowing ahead of time what, if any, listeners will be responding.)  A common
        use case for this would be to wait a few seconds _after_ a resize has occurred for anything
        that just loaded to finish doing so.


        #### Arguments

        - **idle** (`int`):

            The amount of time, in milliseconds, that the event stream should be idle before
            returning.

        - **events** (`list`, optional):

            If not empty, the **idle** time will be interpreted as the amount of time since _any
            of these specific events_ have occurred.  The default is to wait for the browser to be
            idle with respect to _any_ events.

        - **timeout** (`int`):

            The maximum amount of time to wait before raising a
            `webfriend.exceptions.TimeoutError`.

        - **poll_interval** (`int`):

            How often to check the event timings to see if the idle time has elapsed.

        #### Returns
        An `int` representing the number of milliseconds we waited for.

        #### Raises
        `webfriend.exceptions.TimeoutError`
        """
        started_at = time.time()
        idle = (idle / 1e3)

        # clear out the old timings we're interested in
        if len(events):
            for name in events:
                if name in self.last_event_m:
                    del self.last_event_m[name]

        while time.time() < (started_at + (timeout / 1e3)):
            # if we don't have an event filter, then we just want to wait idle seconds
            # after ANY event has been received
            if not len(events):
                # if the time since ANY event has met/exceeded our idle time
                if time.time() >= (self.last_event_t + idle):
                    return (time.time() - started_at) * 1e3

            else:
                for name in events:
                    if name in self.last_event_m:
                        # if the time since this event has met/exceeded our idle time
                        if time.time() >= (self.last_event_m[name] + idle):
                            # now that we've gotten the event, remove it so subsequent calls wait
                            # for the next one to occur
                            del self.last_event_m[name]
                            return (time.time() - started_at) * 1e3

            time.sleep(poll_interval / 1e3)

        raise exceptions.TimeoutError("Timed out waiting for events to stop coming in")

    def evaluate(self, *args, **kwargs):
        return self.runtime.evaluate(*args, **kwargs)

    def rpc(self, method, expect_reply=True, reply_timeout=None, context=None, **params):
        payload = {
            'method': method,
        }

        if len(params):
            payload['params'] = params

        return self.send(
            payload,
            expect_reply=expect_reply,
            reply_timeout=reply_timeout,
            context=context
        )

    def get_domain_instance(self, domain):
        for attr in dir(self):
            instance = getattr(self, attr)


            if isinstance(instance, Base):