Robot.UIWeb is ready for Linux. Add selenium

dev-linux
Mikhail 2 years ago
parent 04afab6dde
commit d72ab349c6

@ -0,0 +1,22 @@
Copyright 2006 Dan-Haim. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of Dan Haim nor the names of his contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.

@ -0,0 +1,321 @@
Metadata-Version: 2.1
Name: PySocks
Version: 1.7.1
Summary: A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information.
Home-page: https://github.com/Anorov/PySocks
Author: Anorov
Author-email: anorov.vorona@gmail.com
License: BSD
Keywords: socks,proxy
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
Description-Content-Type: text/markdown
PySocks
=======
PySocks lets you send traffic through SOCKS and HTTP proxy servers. It is a modern fork of [SocksiPy](http://socksipy.sourceforge.net/) with bug fixes and extra features.
Acts as a drop-in replacement to the socket module. Seamlessly configure SOCKS proxies for any socket object by calling `socket_object.set_proxy()`.
----------------
Features
========
* SOCKS proxy client for Python 2.7 and 3.4+
* TCP supported
* UDP mostly supported (issues may occur in some edge cases)
* HTTP proxy client included but not supported or recommended (you should use urllib2's or requests' own HTTP proxy interface)
* urllib2 handler included. `pip install` / `setup.py install` will automatically install the `sockshandler` module.
Installation
============
pip install PySocks
Or download the tarball / `git clone` and...
python setup.py install
These will install both the `socks` and `sockshandler` modules.
Alternatively, include just `socks.py` in your project.
--------------------------------------------
*Warning:* PySocks/SocksiPy only supports HTTP proxies that use CONNECT tunneling. Certain HTTP proxies may not work with this library. If you wish to use HTTP (not SOCKS) proxies, it is recommended that you rely on your HTTP client's native proxy support (`proxies` dict for `requests`, or `urllib2.ProxyHandler` for `urllib2`) instead.
--------------------------------------------
Usage
=====
## socks.socksocket ##
import socks
s = socks.socksocket() # Same API as socket.socket in the standard lib
s.set_proxy(socks.SOCKS5, "localhost") # SOCKS4 and SOCKS5 use port 1080 by default
# Or
s.set_proxy(socks.SOCKS4, "localhost", 4444)
# Or
s.set_proxy(socks.HTTP, "5.5.5.5", 8888)
# Can be treated identical to a regular socket object
s.connect(("www.somesite.com", 80))
s.sendall("GET / HTTP/1.1 ...")
print s.recv(4096)
## Monkeypatching ##
To monkeypatch the entire standard library with a single default proxy:
import urllib2
import socket
import socks
socks.set_default_proxy(socks.SOCKS5, "localhost")
socket.socket = socks.socksocket
urllib2.urlopen("http://www.somesite.com/") # All requests will pass through the SOCKS proxy
Note that monkeypatching may not work for all standard modules or for all third party modules, and generally isn't recommended. Monkeypatching is usually an anti-pattern in Python.
## urllib2 Handler ##
Example use case with the `sockshandler` urllib2 handler. Note that you must import both `socks` and `sockshandler`, as the handler is its own module separate from PySocks. The module is included in the PyPI package.
import urllib2
import socks
from sockshandler import SocksiPyHandler
opener = urllib2.build_opener(SocksiPyHandler(socks.SOCKS5, "127.0.0.1", 9050))
print opener.open("http://www.somesite.com/") # All requests made by the opener will pass through the SOCKS proxy
--------------------------------------------
Original SocksiPy README attached below, amended to reflect API changes.
--------------------------------------------
SocksiPy
A Python SOCKS module.
(C) 2006 Dan-Haim. All rights reserved.
See LICENSE file for details.
*WHAT IS A SOCKS PROXY?*
A SOCKS proxy is a proxy server at the TCP level. In other words, it acts as
a tunnel, relaying all traffic going through it without modifying it.
SOCKS proxies can be used to relay traffic using any network protocol that
uses TCP.
*WHAT IS SOCKSIPY?*
This Python module allows you to create TCP connections through a SOCKS
proxy without any special effort.
It also supports relaying UDP packets with a SOCKS5 proxy.
*PROXY COMPATIBILITY*
SocksiPy is compatible with three different types of proxies:
1. SOCKS Version 4 (SOCKS4), including the SOCKS4a extension.
2. SOCKS Version 5 (SOCKS5).
3. HTTP Proxies which support tunneling using the CONNECT method.
*SYSTEM REQUIREMENTS*
Being written in Python, SocksiPy can run on any platform that has a Python
interpreter and TCP/IP support.
This module has been tested with Python 2.3 and should work with greater versions
just as well.
INSTALLATION
-------------
Simply copy the file "socks.py" to your Python's `lib/site-packages` directory,
and you're ready to go. [Editor's note: it is better to use `python setup.py install` for PySocks]
USAGE
------
First load the socks module with the command:
>>> import socks
>>>
The socks module provides a class called `socksocket`, which is the base to all of the module's functionality.
The `socksocket` object has the same initialization parameters as the normal socket
object to ensure maximal compatibility, however it should be noted that `socksocket` will only function with family being `AF_INET` and
type being either `SOCK_STREAM` or `SOCK_DGRAM`.
Generally, it is best to initialize the `socksocket` object with no parameters
>>> s = socks.socksocket()
>>>
The `socksocket` object has an interface which is very similiar to socket's (in fact
the `socksocket` class is derived from socket) with a few extra methods.
To select the proxy server you would like to use, use the `set_proxy` method, whose
syntax is:
set_proxy(proxy_type, addr[, port[, rdns[, username[, password]]]])
Explanation of the parameters:
`proxy_type` - The type of the proxy server. This can be one of three possible
choices: `PROXY_TYPE_SOCKS4`, `PROXY_TYPE_SOCKS5` and `PROXY_TYPE_HTTP` for SOCKS4,
SOCKS5 and HTTP servers respectively. `SOCKS4`, `SOCKS5`, and `HTTP` are all aliases, respectively.
`addr` - The IP address or DNS name of the proxy server.
`port` - The port of the proxy server. Defaults to 1080 for socks and 8080 for http.
`rdns` - This is a boolean flag than modifies the behavior regarding DNS resolving.
If it is set to True, DNS resolving will be preformed remotely, on the server.
If it is set to False, DNS resolving will be preformed locally. Please note that
setting this to True with SOCKS4 servers actually use an extension to the protocol,
called SOCKS4a, which may not be supported on all servers (SOCKS5 and http servers
always support DNS). The default is True.
`username` - For SOCKS5 servers, this allows simple username / password authentication
with the server. For SOCKS4 servers, this parameter will be sent as the userid.
This parameter is ignored if an HTTP server is being used. If it is not provided,
authentication will not be used (servers may accept unauthenticated requests).
`password` - This parameter is valid only for SOCKS5 servers and specifies the
respective password for the username provided.
Example of usage:
>>> s.set_proxy(socks.SOCKS5, "socks.example.com") # uses default port 1080
>>> s.set_proxy(socks.SOCKS4, "socks.test.com", 1081)
After the set_proxy method has been called, simply call the connect method with the
traditional parameters to establish a connection through the proxy:
>>> s.connect(("www.sourceforge.net", 80))
>>>
Connection will take a bit longer to allow negotiation with the proxy server.
Please note that calling connect without calling `set_proxy` earlier will connect
without a proxy (just like a regular socket).
Errors: Any errors in the connection process will trigger exceptions. The exception
may either be generated by the underlying socket layer or may be custom module
exceptions, whose details follow:
class `ProxyError` - This is a base exception class. It is not raised directly but
rather all other exception classes raised by this module are derived from it.
This allows an easy way to catch all proxy-related errors. It descends from `IOError`.
All `ProxyError` exceptions have an attribute `socket_err`, which will contain either a
caught `socket.error` exception, or `None` if there wasn't any.
class `GeneralProxyError` - When thrown, it indicates a problem which does not fall
into another category.
* `Sent invalid data` - This error means that unexpected data has been received from
the server. The most common reason is that the server specified as the proxy is
not really a SOCKS4/SOCKS5/HTTP proxy, or maybe the proxy type specified is wrong.
* `Connection closed unexpectedly` - The proxy server unexpectedly closed the connection.
This may indicate that the proxy server is experiencing network or software problems.
* `Bad proxy type` - This will be raised if the type of the proxy supplied to the
set_proxy function was not one of `SOCKS4`/`SOCKS5`/`HTTP`.
* `Bad input` - This will be raised if the `connect()` method is called with bad input
parameters.
class `SOCKS5AuthError` - This indicates that the connection through a SOCKS5 server
failed due to an authentication problem.
* `Authentication is required` - This will happen if you use a SOCKS5 server which
requires authentication without providing a username / password at all.
* `All offered authentication methods were rejected` - This will happen if the proxy
requires a special authentication method which is not supported by this module.
* `Unknown username or invalid password` - Self descriptive.
class `SOCKS5Error` - This will be raised for SOCKS5 errors which are not related to
authentication.
The parameter is a tuple containing a code, as given by the server,
and a description of the
error. The possible errors, according to the RFC, are:
* `0x01` - General SOCKS server failure - If for any reason the proxy server is unable to
fulfill your request (internal server error).
* `0x02` - connection not allowed by ruleset - If the address you're trying to connect to
is blacklisted on the server or requires authentication.
* `0x03` - Network unreachable - The target could not be contacted. A router on the network
had replied with a destination net unreachable error.
* `0x04` - Host unreachable - The target could not be contacted. A router on the network
had replied with a destination host unreachable error.
* `0x05` - Connection refused - The target server has actively refused the connection
(the requested port is closed).
* `0x06` - TTL expired - The TTL value of the SYN packet from the proxy to the target server
has expired. This usually means that there are network problems causing the packet
to be caught in a router-to-router "ping-pong".
* `0x07` - Command not supported - For instance if the server does not support UDP.
* `0x08` - Address type not supported - The client has provided an invalid address type.
When using this module, this error should not occur.
class `SOCKS4Error` - This will be raised for SOCKS4 errors. The parameter is a tuple
containing a code and a description of the error, as given by the server. The
possible error, according to the specification are:
* `0x5B` - Request rejected or failed - Will be raised in the event of an failure for any
reason other then the two mentioned next.
* `0x5C` - request rejected because SOCKS server cannot connect to identd on the client -
The Socks server had tried an ident lookup on your computer and has failed. In this
case you should run an identd server and/or configure your firewall to allow incoming
connections to local port 113 from the remote server.
* `0x5D` - request rejected because the client program and identd report different user-ids -
The Socks server had performed an ident lookup on your computer and has received a
different userid than the one you have provided. Change your userid (through the
username parameter of the set_proxy method) to match and try again.
class `HTTPError` - This will be raised for HTTP errors. The message will contain
the HTTP status code and provided error message.
After establishing the connection, the object behaves like a standard socket.
Methods like `makefile()` and `settimeout()` should behave just like regular sockets.
Call the `close()` method to close the connection.
In addition to the `socksocket` class, an additional function worth mentioning is the
`set_default_proxy` function. The parameters are the same as the `set_proxy` method.
This function will set default proxy settings for newly created `socksocket` objects,
in which the proxy settings haven't been changed via the `set_proxy` method.
This is quite useful if you wish to force 3rd party modules to use a SOCKS proxy,
by overriding the socket object.
For example:
>>> socks.set_default_proxy(socks.SOCKS5, "socks.example.com")
>>> socket.socket = socks.socksocket
>>> urllib.urlopen("http://www.sourceforge.net/")
PROBLEMS
---------
Please open a GitHub issue at https://github.com/Anorov/PySocks

@ -0,0 +1,10 @@
PySocks-1.7.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
PySocks-1.7.1.dist-info/LICENSE,sha256=cCfiFOAU63i3rcwc7aWspxOnn8T2oMUsnaWz5wfm_-k,1401
PySocks-1.7.1.dist-info/METADATA,sha256=zbQMizjPOOP4DhEiEX24XXjNrYuIxF9UGUpN0uFDB6Y,13235
PySocks-1.7.1.dist-info/RECORD,,
PySocks-1.7.1.dist-info/WHEEL,sha256=t_MpApv386-8PVts2R6wsTifdIn0vbUDTVv61IbqFC8,92
PySocks-1.7.1.dist-info/top_level.txt,sha256=TKSOIfCFBoK9EY8FBYbYqC3PWd3--G15ph9n8-QHPDk,19
__pycache__/socks.cpython-310.pyc,,
__pycache__/sockshandler.cpython-310.pyc,,
socks.py,sha256=xOYn27t9IGrbTBzWsUUuPa0YBuplgiUykzkOB5V5iFY,31086
sockshandler.py,sha256=2SYGj-pwt1kjgLoZAmyeaEXCeZDWRmfVS_QG6kErGtY,3966

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.33.3)
Root-Is-Purelib: true
Tag: py3-none-any

@ -0,0 +1,145 @@
Metadata-Version: 2.1
Name: async-generator
Version: 1.10
Summary: Async generators and context managers for Python 3.5+
Home-page: https://github.com/python-trio/async_generator
Author: Nathaniel J. Smith
Author-email: njs@pobox.com
License: MIT -or- Apache License 2.0
Keywords: async
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Framework :: AsyncIO
Requires-Python: >=3.5
.. image:: https://img.shields.io/badge/chat-join%20now-blue.svg
:target: https://gitter.im/python-trio/general
:alt: Join chatroom
.. image:: https://img.shields.io/badge/docs-read%20now-blue.svg
:target: https://async-generator.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://travis-ci.org/python-trio/async_generator.svg?branch=master
:target: https://travis-ci.org/python-trio/async_generator
:alt: Automated test status
.. image:: https://ci.appveyor.com/api/projects/status/af4eyed8o8tc3t0r/branch/master?svg=true
:target: https://ci.appveyor.com/project/python-trio/trio/history
:alt: Automated test status (Windows)
.. image:: https://codecov.io/gh/python-trio/async_generator/branch/master/graph/badge.svg
:target: https://codecov.io/gh/python-trio/async_generator
:alt: Test coverage
The async_generator library
===========================
Python 3.6 added `async generators
<https://www.python.org/dev/peps/pep-0525/>`__. (What's an async
generator? `Check out my 5-minute lightning talk demo from PyCon 2016
<https://youtu.be/PulzIT8KYLk?t=24m30s>`__.) Python 3.7 adds some more
tools to make them usable, like ``contextlib.asynccontextmanager``.
This library gives you all that back to Python 3.5.
For example, this code only works in Python 3.6+:
.. code-block:: python3
async def load_json_lines(stream_reader):
async for line in stream_reader:
yield json.loads(line)
But this code does the same thing, and works on Python 3.5+:
.. code-block:: python3
from async_generator import async_generator, yield_
@async_generator
async def load_json_lines(stream_reader):
async for line in stream_reader:
await yield_(json.loads(line))
Or in Python 3.7, you can write:
.. code-block:: python3
from contextlib import asynccontextmanager
@asynccontextmanager
async def background_server():
async with trio.open_nursery() as nursery:
value = await nursery.start(my_server)
try:
yield value
finally:
# Kill the server when the scope exits
nursery.cancel_scope.cancel()
This is the same, but back to 3.5:
.. code-block:: python3
from async_generator import async_generator, yield_, asynccontextmanager
@asynccontextmanager
@async_generator
async def background_server():
async with trio.open_nursery() as nursery:
value = await nursery.start(my_server)
try:
await yield_(value)
finally:
# Kill the server when the scope exits
nursery.cancel_scope.cancel()
(And if you're on 3.6, you can use ``@asynccontextmanager`` with
native generators.)
Let's do this
=============
* Install: ``python3 -m pip install -U async_generator`` (or on Windows,
maybe ``py -3 -m pip install -U async_generator``
* Manual: https://async-generator.readthedocs.io/
* Bug tracker and source code: https://github.com/python-trio/async_generator
* Real-time chat: https://gitter.im/python-trio/general
* License: MIT or Apache 2, your choice
* Contributor guide: https://trio.readthedocs.io/en/latest/contributing.html
* Code of conduct: Contributors are requested to follow our `code of
conduct
<https://trio.readthedocs.io/en/latest/code-of-conduct.html>`__ in
all project spaces.
How come some of those links talk about "trio"?
===============================================
`Trio <https://trio.readthedocs.io>`__ is a new async concurrency
library for Python that's obsessed with usability and correctness we
want to make it *easy* to get things *right*. The ``async_generator``
library is maintained by the Trio project as part of that mission, and
because Trio uses ``async_generator`` internally.
You can use ``async_generator`` with any async library. It works great
with ``asyncio``, or Twisted, or whatever you like. (But we think Trio
is pretty sweet.)

@ -0,0 +1,21 @@
async_generator-1.10.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
async_generator-1.10.dist-info/METADATA,sha256=FZoDYGYfSdJkSUSR5T_YMqq2TnwYa-RFOm6SbhWFzGA,4870
async_generator-1.10.dist-info/RECORD,,
async_generator-1.10.dist-info/WHEEL,sha256=NzFAKnL7g-U64xnS1s5e3mJnxKpOTeOtlXdFwS9yNXI,92
async_generator-1.10.dist-info/top_level.txt,sha256=Qc2NF6EJciFqrZ6gAdWuQIveMaqWJw4jqv1anjEHT_U,16
async_generator/__init__.py,sha256=6eYc-CD3B5kQx8LzMhTEqJyKQH5UTsy8IZhR3AvcVb8,454
async_generator/__pycache__/__init__.cpython-310.pyc,,
async_generator/__pycache__/_impl.cpython-310.pyc,,
async_generator/__pycache__/_util.cpython-310.pyc,,
async_generator/__pycache__/_version.cpython-310.pyc,,
async_generator/_impl.py,sha256=t1p5goS6TrQmpxLL4vOFa9zpl0SorICI8WZc8e81lxU,16106
async_generator/_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
async_generator/_tests/__pycache__/__init__.cpython-310.pyc,,
async_generator/_tests/__pycache__/conftest.cpython-310.pyc,,
async_generator/_tests/__pycache__/test_async_generator.cpython-310.pyc,,
async_generator/_tests/__pycache__/test_util.cpython-310.pyc,,
async_generator/_tests/conftest.py,sha256=eL5uA75o6d9feDTeEXu8vYDg-kgbnfuaGILJKGyWOFw,1211
async_generator/_tests/test_async_generator.py,sha256=HBXQAlZdt68hzSQ6eMoSYsoO--Bic3Ojv1Z71hCQb7U,27936
async_generator/_tests/test_util.py,sha256=-vLPOW_V2etk3Bf2M3cqEOGjxeRPFnSGfftsIYBaCCQ,6373
async_generator/_util.py,sha256=jtBz2-fn6ec0JmaKKY-sC0TAq6zqGAKL3qs7LYQ4uFw,4358
async_generator/_version.py,sha256=Nas37COFU-AbvlukCSCZDPrvCzgtKiG5QGZXnYenLC8,21

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.31.1)
Root-Is-Purelib: true
Tag: py3-none-any

@ -0,0 +1,23 @@
from ._version import __version__
from ._impl import (
async_generator,
yield_,
yield_from_,
isasyncgen,
isasyncgenfunction,
get_asyncgen_hooks,
set_asyncgen_hooks,
)
from ._util import aclosing, asynccontextmanager
__all__ = [
"async_generator",
"yield_",
"yield_from_",
"aclosing",
"isasyncgen",
"isasyncgenfunction",
"asynccontextmanager",
"get_asyncgen_hooks",
"set_asyncgen_hooks",
]

@ -0,0 +1,455 @@
import sys
from functools import wraps
from types import coroutine
import inspect
from inspect import (
getcoroutinestate, CORO_CREATED, CORO_CLOSED, CORO_SUSPENDED
)
import collections.abc
class YieldWrapper:
def __init__(self, payload):
self.payload = payload
def _wrap(value):
return YieldWrapper(value)
def _is_wrapped(box):
return isinstance(box, YieldWrapper)
def _unwrap(box):
return box.payload
# This is the magic code that lets you use yield_ and yield_from_ with native
# generators.
#
# The old version worked great on Linux and MacOS, but not on Windows, because
# it depended on _PyAsyncGenValueWrapperNew. The new version segfaults
# everywhere, and I'm not sure why -- probably my lack of understanding
# of ctypes and refcounts.
#
# There are also some commented out tests that should be re-enabled if this is
# fixed:
#
# if sys.version_info >= (3, 6):
# # Use the same box type that the interpreter uses internally. This allows
# # yield_ and (more importantly!) yield_from_ to work in built-in
# # generators.
# import ctypes # mua ha ha.
#
# # We used to call _PyAsyncGenValueWrapperNew to create and set up new
# # wrapper objects, but that symbol isn't available on Windows:
# #
# # https://github.com/python-trio/async_generator/issues/5
# #
# # Fortunately, the type object is available, but it means we have to do
# # this the hard way.
#
# # We don't actually need to access this, but we need to make a ctypes
# # structure so we can call addressof.
# class _ctypes_PyTypeObject(ctypes.Structure):
# pass
# _PyAsyncGenWrappedValue_Type_ptr = ctypes.addressof(
# _ctypes_PyTypeObject.in_dll(
# ctypes.pythonapi, "_PyAsyncGenWrappedValue_Type"))
# _PyObject_GC_New = ctypes.pythonapi._PyObject_GC_New
# _PyObject_GC_New.restype = ctypes.py_object
# _PyObject_GC_New.argtypes = (ctypes.c_void_p,)
#
# _Py_IncRef = ctypes.pythonapi.Py_IncRef
# _Py_IncRef.restype = None
# _Py_IncRef.argtypes = (ctypes.py_object,)
#
# class _ctypes_PyAsyncGenWrappedValue(ctypes.Structure):
# _fields_ = [
# ('PyObject_HEAD', ctypes.c_byte * object().__sizeof__()),
# ('agw_val', ctypes.py_object),
# ]
# def _wrap(value):
# box = _PyObject_GC_New(_PyAsyncGenWrappedValue_Type_ptr)
# raw = ctypes.cast(ctypes.c_void_p(id(box)),
# ctypes.POINTER(_ctypes_PyAsyncGenWrappedValue))
# raw.contents.agw_val = value
# _Py_IncRef(value)
# return box
#
# def _unwrap(box):
# assert _is_wrapped(box)
# raw = ctypes.cast(ctypes.c_void_p(id(box)),
# ctypes.POINTER(_ctypes_PyAsyncGenWrappedValue))
# value = raw.contents.agw_val
# _Py_IncRef(value)
# return value
#
# _PyAsyncGenWrappedValue_Type = type(_wrap(1))
# def _is_wrapped(box):
# return isinstance(box, _PyAsyncGenWrappedValue_Type)
# The magic @coroutine decorator is how you write the bottom level of
# coroutine stacks -- 'async def' can only use 'await' = yield from; but
# eventually we must bottom out in a @coroutine that calls plain 'yield'.
@coroutine
def _yield_(value):
return (yield _wrap(value))
# But we wrap the bare @coroutine version in an async def, because async def
# has the magic feature that users can get warnings messages if they forget to
# use 'await'.
async def yield_(value=None):
return await _yield_(value)
async def yield_from_(delegate):
# Transcribed with adaptations from:
#
# https://www.python.org/dev/peps/pep-0380/#formal-semantics
#
# This takes advantage of a sneaky trick: if an @async_generator-wrapped
# function calls another async function (like yield_from_), and that
# second async function calls yield_, then because of the hack we use to
# implement yield_, the yield_ will actually propagate through yield_from_
# back to the @async_generator wrapper. So even though we're a regular
# function, we can directly yield values out of the calling async
# generator.
def unpack_StopAsyncIteration(e):
if e.args:
return e.args[0]
else:
return None
_i = type(delegate).__aiter__(delegate)
if hasattr(_i, "__await__"):
_i = await _i
try:
_y = await type(_i).__anext__(_i)
except StopAsyncIteration as _e:
_r = unpack_StopAsyncIteration(_e)
else:
while 1:
try:
_s = await yield_(_y)
except GeneratorExit as _e:
try:
_m = _i.aclose
except AttributeError:
pass
else:
await _m()
raise _e
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.athrow
except AttributeError:
raise _e
else:
try:
_y = await _m(*_x)
except StopAsyncIteration as _e:
_r = unpack_StopAsyncIteration(_e)
break
else:
try:
if _s is None:
_y = await type(_i).__anext__(_i)
else:
_y = await _i.asend(_s)
except StopAsyncIteration as _e:
_r = unpack_StopAsyncIteration(_e)
break
return _r
# This is the awaitable / iterator that implements asynciter.__anext__() and
# friends.
#
# Note: we can be sloppy about the distinction between
#
# type(self._it).__next__(self._it)
#
# and
#
# self._it.__next__()
#
# because we happen to know that self._it is not a general iterator object,
# but specifically a coroutine iterator object where these are equivalent.
class ANextIter:
def __init__(self, it, first_fn, *first_args):
self._it = it
self._first_fn = first_fn
self._first_args = first_args
def __await__(self):
return self
def __next__(self):
if self._first_fn is not None:
first_fn = self._first_fn
first_args = self._first_args
self._first_fn = self._first_args = None
return self._invoke(first_fn, *first_args)
else:
return self._invoke(self._it.__next__)
def send(self, value):
return self._invoke(self._it.send, value)
def throw(self, type, value=None, traceback=None):
return self._invoke(self._it.throw, type, value, traceback)
def _invoke(self, fn, *args):
try:
result = fn(*args)
except StopIteration as e:
# The underlying generator returned, so we should signal the end
# of iteration.
raise StopAsyncIteration(e.value)
except StopAsyncIteration as e:
# PEP 479 says: if a generator raises Stop(Async)Iteration, then
# it should be wrapped into a RuntimeError. Python automatically
# enforces this for StopIteration; for StopAsyncIteration we need
# to it ourselves.
raise RuntimeError(
"async_generator raise StopAsyncIteration"
) from e
if _is_wrapped(result):
raise StopIteration(_unwrap(result))
else:
return result
UNSPECIFIED = object()
try:
from sys import get_asyncgen_hooks, set_asyncgen_hooks
except ImportError:
import threading
asyncgen_hooks = collections.namedtuple(
"asyncgen_hooks", ("firstiter", "finalizer")
)
class _hooks_storage(threading.local):
def __init__(self):
self.firstiter = None
self.finalizer = None
_hooks = _hooks_storage()
def get_asyncgen_hooks():
return asyncgen_hooks(
firstiter=_hooks.firstiter, finalizer=_hooks.finalizer
)
def set_asyncgen_hooks(firstiter=UNSPECIFIED, finalizer=UNSPECIFIED):
if firstiter is not UNSPECIFIED:
if firstiter is None or callable(firstiter):
_hooks.firstiter = firstiter
else:
raise TypeError(
"callable firstiter expected, got {}".format(
type(firstiter).__name__
)
)
if finalizer is not UNSPECIFIED:
if finalizer is None or callable(finalizer):
_hooks.finalizer = finalizer
else:
raise TypeError(
"callable finalizer expected, got {}".format(
type(finalizer).__name__
)
)
class AsyncGenerator:
# https://bitbucket.org/pypy/pypy/issues/2786:
# PyPy implements 'await' in a way that requires the frame object
# used to execute a coroutine to keep a weakref to that coroutine.
# During a GC pass, weakrefs to all doomed objects are broken
# before any of the doomed objects' finalizers are invoked.
# If an AsyncGenerator is unreachable, its _coroutine probably
# is too, and the weakref from ag._coroutine.cr_frame to
# ag._coroutine will be broken before ag.__del__ can do its
# one-turn close attempt or can schedule a full aclose() using
# the registered finalization hook. It doesn't look like the
# underlying issue is likely to be fully fixed anytime soon,
# so we work around it by preventing an AsyncGenerator and
# its _coroutine from being considered newly unreachable at
# the same time if the AsyncGenerator's finalizer might want
# to iterate the coroutine some more.
_pypy_issue2786_workaround = set()
def __init__(self, coroutine):
self._coroutine = coroutine
self._it = coroutine.__await__()
self.ag_running = False
self._finalizer = None
self._closed = False
self._hooks_inited = False
# On python 3.5.0 and 3.5.1, __aiter__ must be awaitable.
# Starting in 3.5.2, it should not be awaitable, and if it is, then it
# raises a PendingDeprecationWarning.
# See:
# https://www.python.org/dev/peps/pep-0492/#api-design-and-implementation-revisions
# https://docs.python.org/3/reference/datamodel.html#async-iterators
# https://bugs.python.org/issue27243
if sys.version_info < (3, 5, 2):
async def __aiter__(self):
return self
else:
def __aiter__(self):
return self
################################################################
# Introspection attributes
################################################################
@property
def ag_code(self):
return self._coroutine.cr_code
@property
def ag_frame(self):
return self._coroutine.cr_frame
################################################################
# Core functionality
################################################################
# These need to return awaitables, rather than being async functions,
# to match the native behavior where the firstiter hook is called
# immediately on asend()/etc, even if the coroutine that asend()
# produces isn't awaited for a bit.
def __anext__(self):
return self._do_it(self._it.__next__)
def asend(self, value):
return self._do_it(self._it.send, value)
def athrow(self, type, value=None, traceback=None):
return self._do_it(self._it.throw, type, value, traceback)
def _do_it(self, start_fn, *args):
if not self._hooks_inited:
self._hooks_inited = True
(firstiter, self._finalizer) = get_asyncgen_hooks()
if firstiter is not None:
firstiter(self)
if sys.implementation.name == "pypy":
self._pypy_issue2786_workaround.add(self._coroutine)
# On CPython 3.5.2 (but not 3.5.0), coroutines get cranky if you try
# to iterate them after they're exhausted. Generators OTOH just raise
# StopIteration. We want to convert the one into the other, so we need
# to avoid iterating stopped coroutines.
if getcoroutinestate(self._coroutine) is CORO_CLOSED:
raise StopAsyncIteration()
async def step():
if self.ag_running:
raise ValueError("async generator already executing")
try:
self.ag_running = True
return await ANextIter(self._it, start_fn, *args)
except StopAsyncIteration:
self._pypy_issue2786_workaround.discard(self._coroutine)
raise
finally:
self.ag_running = False
return step()
################################################################
# Cleanup
################################################################
async def aclose(self):
state = getcoroutinestate(self._coroutine)
if state is CORO_CLOSED or self._closed:
return
# Make sure that even if we raise "async_generator ignored
# GeneratorExit", and thus fail to exhaust the coroutine,
# __del__ doesn't complain again.
self._closed = True
if state is CORO_CREATED:
# Make sure that aclose() on an unstarted generator returns
# successfully and prevents future iteration.
self._it.close()
return
try:
await self.athrow(GeneratorExit)
except (GeneratorExit, StopAsyncIteration):
self._pypy_issue2786_workaround.discard(self._coroutine)
else:
raise RuntimeError("async_generator ignored GeneratorExit")
def __del__(self):
self._pypy_issue2786_workaround.discard(self._coroutine)
if getcoroutinestate(self._coroutine) is CORO_CREATED:
# Never started, nothing to clean up, just suppress the "coroutine
# never awaited" message.
self._coroutine.close()
if getcoroutinestate(self._coroutine
) is CORO_SUSPENDED and not self._closed:
if self._finalizer is not None:
self._finalizer(self)
else:
# Mimic the behavior of native generators on GC with no finalizer:
# throw in GeneratorExit, run for one turn, and complain if it didn't
# finish.
thrower = self.athrow(GeneratorExit)
try:
thrower.send(None)
except (GeneratorExit, StopAsyncIteration):
pass
except StopIteration:
raise RuntimeError("async_generator ignored GeneratorExit")
else:
raise RuntimeError(
"async_generator {!r} awaited during finalization; install "
"a finalization hook to support this, or wrap it in "
"'async with aclosing(...):'"
.format(self.ag_code.co_name)
)
finally:
thrower.close()
if hasattr(collections.abc, "AsyncGenerator"):
collections.abc.AsyncGenerator.register(AsyncGenerator)
def async_generator(coroutine_maker):
@wraps(coroutine_maker)
def async_generator_maker(*args, **kwargs):
return AsyncGenerator(coroutine_maker(*args, **kwargs))
async_generator_maker._async_gen_function = id(async_generator_maker)
return async_generator_maker
def isasyncgen(obj):
if hasattr(inspect, "isasyncgen"):
if inspect.isasyncgen(obj):
return True
return isinstance(obj, AsyncGenerator)
def isasyncgenfunction(obj):
if hasattr(inspect, "isasyncgenfunction"):
if inspect.isasyncgenfunction(obj):
return True
return getattr(obj, "_async_gen_function", -1) == id(obj)

@ -0,0 +1,36 @@
import pytest
from functools import wraps, partial
import inspect
import types
@types.coroutine
def mock_sleep():
yield "mock_sleep"
# Wrap any 'async def' tests so that they get automatically iterated.
# We used to use pytest-asyncio as a convenient way to do this, but nowadays
# pytest-asyncio uses us! In addition to it being generally bad for our test
# infrastructure to depend on the code-under-test, this totally messes up
# coverage info because depending on pytest's plugin load order, we might get
# imported before pytest-cov can be loaded and start gathering coverage.
@pytest.hookimpl(tryfirst=True)
def pytest_pyfunc_call(pyfuncitem):
if inspect.iscoroutinefunction(pyfuncitem.obj):
fn = pyfuncitem.obj
@wraps(fn)
def wrapper(**kwargs):
coro = fn(**kwargs)
try:
while True:
value = coro.send(None)
if value != "mock_sleep": # pragma: no cover
raise RuntimeError(
"coroutine runner confused: {!r}".format(value)
)
except StopIteration:
pass
pyfuncitem.obj = wrapper

@ -0,0 +1,227 @@
import pytest
from .. import aclosing, async_generator, yield_, asynccontextmanager
@async_generator
async def async_range(count, closed_slot):
try:
for i in range(count): # pragma: no branch
await yield_(i)
except GeneratorExit:
closed_slot[0] = True
async def test_aclosing():
closed_slot = [False]
async with aclosing(async_range(10, closed_slot)) as gen:
it = iter(range(10))
async for item in gen: # pragma: no branch
assert item == next(it)
if item == 4:
break
assert closed_slot[0]
closed_slot = [False]
try:
async with aclosing(async_range(10, closed_slot)) as gen:
it = iter(range(10))
async for item in gen: # pragma: no branch
assert item == next(it)
if item == 4:
raise ValueError()
except ValueError:
pass
assert closed_slot[0]
async def test_contextmanager_do_not_unchain_non_stopiteration_exceptions():
@asynccontextmanager
@async_generator
async def manager_issue29692():
try:
await yield_()
except Exception as exc:
raise RuntimeError('issue29692:Chained') from exc
with pytest.raises(RuntimeError) as excinfo:
async with manager_issue29692():
raise ZeroDivisionError
assert excinfo.value.args[0] == 'issue29692:Chained'
assert isinstance(excinfo.value.__cause__, ZeroDivisionError)
# This is a little funky because of implementation details in
# async_generator It can all go away once we stop supporting Python3.5
with pytest.raises(RuntimeError) as excinfo:
async with manager_issue29692():
exc = StopIteration('issue29692:Unchained')
raise exc
assert excinfo.value.args[0] == 'issue29692:Chained'
cause = excinfo.value.__cause__
assert cause.args[0] == 'generator raised StopIteration'
assert cause.__cause__ is exc
with pytest.raises(StopAsyncIteration) as excinfo:
async with manager_issue29692():
raise StopAsyncIteration('issue29692:Unchained')
assert excinfo.value.args[0] == 'issue29692:Unchained'
assert excinfo.value.__cause__ is None
@asynccontextmanager
@async_generator
async def noop_async_context_manager():
await yield_()
with pytest.raises(StopIteration):
async with noop_async_context_manager():
raise StopIteration
# Native async generators are only available from Python 3.6 and onwards
nativeasyncgenerators = True
try:
exec(
"""
@asynccontextmanager
async def manager_issue29692_2():
try:
yield
except Exception as exc:
raise RuntimeError('issue29692:Chained') from exc
"""
)
except SyntaxError:
nativeasyncgenerators = False
@pytest.mark.skipif(
not nativeasyncgenerators,
reason="Python < 3.6 doesn't have native async generators"
)
async def test_native_contextmanager_do_not_unchain_non_stopiteration_exceptions(
):
with pytest.raises(RuntimeError) as excinfo:
async with manager_issue29692_2():
raise ZeroDivisionError
assert excinfo.value.args[0] == 'issue29692:Chained'
assert isinstance(excinfo.value.__cause__, ZeroDivisionError)
for cls in [StopIteration, StopAsyncIteration]:
with pytest.raises(cls) as excinfo:
async with manager_issue29692_2():
raise cls('issue29692:Unchained')
assert excinfo.value.args[0] == 'issue29692:Unchained'
assert excinfo.value.__cause__ is None
async def test_asynccontextmanager_exception_passthrough():
# This was the cause of annoying coverage flapping, see gh-140
@asynccontextmanager
@async_generator
async def noop_async_context_manager():
await yield_()
for exc_type in [StopAsyncIteration, RuntimeError, ValueError]:
with pytest.raises(exc_type):
async with noop_async_context_manager():
raise exc_type
# And let's also check a boring nothing pass-through while we're at it
async with noop_async_context_manager():
pass
async def test_asynccontextmanager_catches_exception():
@asynccontextmanager
@async_generator
async def catch_it():
with pytest.raises(ValueError):
await yield_()
async with catch_it():
raise ValueError
async def test_asynccontextmanager_different_exception():
@asynccontextmanager
@async_generator
async def switch_it():
try:
await yield_()
except KeyError:
raise ValueError
with pytest.raises(ValueError):
async with switch_it():
raise KeyError
async def test_asynccontextmanager_nice_message_on_sync_enter():
@asynccontextmanager
@async_generator
async def xxx(): # pragma: no cover
await yield_()
cm = xxx()
with pytest.raises(RuntimeError) as excinfo:
with cm:
pass # pragma: no cover
assert "async with" in str(excinfo.value)
async with cm:
pass
async def test_asynccontextmanager_no_yield():
@asynccontextmanager
@async_generator
async def yeehaw():
pass
with pytest.raises(RuntimeError) as excinfo:
async with yeehaw():
assert False # pragma: no cover
assert "didn't yield" in str(excinfo.value)
async def test_asynccontextmanager_too_many_yields():
closed_count = 0
@asynccontextmanager
@async_generator
async def doubleyield():
try:
await yield_()
except Exception:
pass
try:
await yield_()
finally:
nonlocal closed_count
closed_count += 1
with pytest.raises(RuntimeError) as excinfo:
async with doubleyield():
pass
assert "didn't stop" in str(excinfo.value)
assert closed_count == 1
with pytest.raises(RuntimeError) as excinfo:
async with doubleyield():
raise ValueError
assert "didn't stop after athrow" in str(excinfo.value)
assert closed_count == 2
async def test_asynccontextmanager_requires_asyncgenfunction():
with pytest.raises(TypeError):
@asynccontextmanager
def syncgen(): # pragma: no cover
yield

@ -0,0 +1,110 @@
import sys
from functools import wraps
from ._impl import isasyncgenfunction
class aclosing:
def __init__(self, aiter):
self._aiter = aiter
async def __aenter__(self):
return self._aiter
async def __aexit__(self, *args):
await self._aiter.aclose()
# Very much derived from the one in contextlib, by copy/pasting and then
# asyncifying everything. (Also I dropped the obscure support for using
# context managers as function decorators. It could be re-added; I just
# couldn't be bothered.)
# So this is a derivative work licensed under the PSF License, which requires
# the following notice:
#
# Copyright © 2001-2017 Python Software Foundation; All Rights Reserved
class _AsyncGeneratorContextManager:
def __init__(self, func, args, kwds):
self._func_name = func.__name__
self._agen = func(*args, **kwds).__aiter__()
async def __aenter__(self):
if sys.version_info < (3, 5, 2):
self._agen = await self._agen
try:
return await self._agen.asend(None)
except StopAsyncIteration:
raise RuntimeError("async generator didn't yield") from None
async def __aexit__(self, type, value, traceback):
async with aclosing(self._agen):
if type is None:
try:
await self._agen.asend(None)
except StopAsyncIteration:
return False
else:
raise RuntimeError("async generator didn't stop")
else:
# It used to be possible to have type != None, value == None:
# https://bugs.python.org/issue1705170
# but AFAICT this can't happen anymore.
assert value is not None
try:
await self._agen.athrow(type, value, traceback)
raise RuntimeError(
"async generator didn't stop after athrow()"
)
except StopAsyncIteration as exc:
# Suppress StopIteration *unless* it's the same exception
# that was passed to throw(). This prevents a
# StopIteration raised inside the "with" statement from
# being suppressed.
return (exc is not value)
except RuntimeError as exc:
# Don't re-raise the passed in exception. (issue27112)
if exc is value:
return False
# Likewise, avoid suppressing if a StopIteration exception
# was passed to throw() and later wrapped into a
# RuntimeError (see PEP 479).
if (isinstance(value, (StopIteration, StopAsyncIteration))
and exc.__cause__ is value):
return False
raise
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise an
# exception unless __exit__() itself failed. But throw()
# has to raise the exception to signal propagation, so
# this fixes the impedance mismatch between the throw()
# protocol and the __exit__() protocol.
#
if sys.exc_info()[1] is value:
return False
raise
def __enter__(self):
raise RuntimeError(
"use 'async with {func_name}(...)', not 'with {func_name}(...)'".
format(func_name=self._func_name)
)
def __exit__(self): # pragma: no cover
assert False, """Never called, but should be defined"""
def asynccontextmanager(func):
"""Like @contextmanager, but async."""
if not isasyncgenfunction(func):
raise TypeError(
"must be an async generator (native or from async_generator; "
"if using @async_generator then @acontextmanager must be on top."
)
@wraps(func)
def helper(*args, **kwds):
return _AsyncGeneratorContextManager(func, args, kwds)
# A hint for sphinxcontrib-trio:
helper.__returns_acontextmanager__ = True
return helper

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2016 Nathaniel J. Smith <njs@pobox.com> and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,197 @@
Metadata-Version: 2.1
Name: h11
Version: 0.13.0
Summary: A pure-Python, bring-your-own-I/O implementation of HTTP/1.1
Home-page: https://github.com/python-hyper/h11
Author: Nathaniel J. Smith
Author-email: njs@pobox.com
License: MIT
Platform: UNKNOWN
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: System :: Networking
Requires-Python: >=3.6
License-File: LICENSE.txt
Requires-Dist: dataclasses ; python_version < "3.7"
Requires-Dist: typing-extensions ; python_version < "3.8"
h11
===
.. image:: https://travis-ci.org/python-hyper/h11.svg?branch=master
:target: https://travis-ci.org/python-hyper/h11
:alt: Automated test status
.. image:: https://codecov.io/gh/python-hyper/h11/branch/master/graph/badge.svg
:target: https://codecov.io/gh/python-hyper/h11
:alt: Test coverage
.. image:: https://readthedocs.org/projects/h11/badge/?version=latest
:target: http://h11.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
This is a little HTTP/1.1 library written from scratch in Python,
heavily inspired by `hyper-h2 <https://hyper-h2.readthedocs.io/>`_.
It's a "bring-your-own-I/O" library; h11 contains no IO code
whatsoever. This means you can hook h11 up to your favorite network
API, and that could be anything you want: synchronous, threaded,
asynchronous, or your own implementation of `RFC 6214
<https://tools.ietf.org/html/rfc6214>`_ -- h11 won't judge you.
(Compare this to the current state of the art, where every time a `new
network API <https://trio.readthedocs.io/>`_ comes along then someone
gets to start over reimplementing the entire HTTP protocol from
scratch.) Cory Benfield made an `excellent blog post describing the
benefits of this approach
<https://lukasa.co.uk/2015/10/The_New_Hyper/>`_, or if you like video
then here's his `PyCon 2016 talk on the same theme
<https://www.youtube.com/watch?v=7cC3_jGwl_U>`_.
This also means that h11 is not immediately useful out of the box:
it's a toolkit for building programs that speak HTTP, not something
that could directly replace ``requests`` or ``twisted.web`` or
whatever. But h11 makes it much easier to implement something like
``requests`` or ``twisted.web``.
At a high level, working with h11 goes like this:
1) First, create an ``h11.Connection`` object to track the state of a
single HTTP/1.1 connection.
2) When you read data off the network, pass it to
``conn.receive_data(...)``; you'll get back a list of objects
representing high-level HTTP "events".
3) When you want to send a high-level HTTP event, create the
corresponding "event" object and pass it to ``conn.send(...)``;
this will give you back some bytes that you can then push out
through the network.
For example, a client might instantiate and then send a
``h11.Request`` object, then zero or more ``h11.Data`` objects for the
request body (e.g., if this is a POST), and then a
``h11.EndOfMessage`` to indicate the end of the message. Then the
server would then send back a ``h11.Response``, some ``h11.Data``, and
its own ``h11.EndOfMessage``. If either side violates the protocol,
you'll get a ``h11.ProtocolError`` exception.
h11 is suitable for implementing both servers and clients, and has a
pleasantly symmetric API: the events you send as a client are exactly
the ones that you receive as a server and vice-versa.
`Here's an example of a tiny HTTP client
<https://github.com/python-hyper/h11/blob/master/examples/basic-client.py>`_
It also has `a fine manual <https://h11.readthedocs.io/>`_.
FAQ
---
*Whyyyyy?*
I wanted to play with HTTP in `Curio
<https://curio.readthedocs.io/en/latest/tutorial.html>`__ and `Trio
<https://trio.readthedocs.io>`__, which at the time didn't have any
HTTP libraries. So I thought, no big deal, Python has, like, a dozen
different implementations of HTTP, surely I can find one that's
reusable. I didn't find one, but I did find Cory's call-to-arms
blog-post. So I figured, well, fine, if I have to implement HTTP from
scratch, at least I can make sure no-one *else* has to ever again.
*Should I use it?*
Maybe. You should be aware that it's a very young project. But, it's
feature complete and has an exhaustive test-suite and complete docs,
so the next step is for people to try using it and see how it goes
:-). If you do then please let us know -- if nothing else we'll want
to talk to you before making any incompatible changes!
*What are the features/limitations?*
Roughly speaking, it's trying to be a robust, complete, and non-hacky
implementation of the first "chapter" of the HTTP/1.1 spec: `RFC 7230:
HTTP/1.1 Message Syntax and Routing
<https://tools.ietf.org/html/rfc7230>`_. That is, it mostly focuses on
implementing HTTP at the level of taking bytes on and off the wire,
and the headers related to that, and tries to be anal about spec
conformance. It doesn't know about higher-level concerns like URL
routing, conditional GETs, cross-origin cookie policies, or content
negotiation. But it does know how to take care of framing,
cross-version differences in keep-alive handling, and the "obsolete
line folding" rule, so you can focus your energies on the hard /
interesting parts for your application, and it tries to support the
full specification in the sense that any useful HTTP/1.1 conformant
application should be able to use h11.
It's pure Python, and has no dependencies outside of the standard
library.
It has a test suite with 100.0% coverage for both statements and
branches.
Currently it supports Python 3 (testing on 3.6-3.9) and PyPy 3.
The last Python 2-compatible version was h11 0.11.x.
(Originally it had a Cython wrapper for `http-parser
<https://github.com/nodejs/http-parser>`_ and a beautiful nested state
machine implemented with ``yield from`` to postprocess the output. But
I had to take these out -- the new *parser* needs fewer lines-of-code
than the old *parser wrapper*, is written in pure Python, uses no
exotic language syntax, and has more features. It's sad, really; that
old state machine was really slick. I just need a few sentences here
to mourn that.)
I don't know how fast it is. I haven't benchmarked or profiled it yet,
so it's probably got a few pointless hot spots, and I've been trying
to err on the side of simplicity and robustness instead of
micro-optimization. But at the architectural level I tried hard to
avoid fundamentally bad decisions, e.g., I believe that all the
parsing algorithms remain linear-time even in the face of pathological
input like slowloris, and there are no byte-by-byte loops. (I also
believe that it maintains bounded memory usage in the face of
arbitrary/pathological input.)
The whole library is ~800 lines-of-code. You can read and understand
the whole thing in less than an hour. Most of the energy invested in
this so far has been spent on trying to keep things simple by
minimizing special-cases and ad hoc state manipulation; even though it
is now quite small and simple, I'm still annoyed that I haven't
figured out how to make it even smaller and simpler. (Unfortunately,
HTTP does not lend itself to simplicity.)
The API is ~feature complete and I don't expect the general outlines
to change much, but you can't judge an API's ergonomics until you
actually document and use it, so I'd expect some changes in the
details.
*How do I try it?*
.. code-block:: sh
$ pip install h11
$ git clone git@github.com:python-hyper/h11
$ cd h11/examples
$ python basic-client.py
and go from there.
*License?*
MIT
*Code of conduct?*
Contributors are requested to follow our `code of conduct
<https://github.com/python-hyper/h11/blob/master/CODE_OF_CONDUCT.md>`_ in
all project spaces.

@ -0,0 +1,52 @@
h11-0.13.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
h11-0.13.0.dist-info/LICENSE.txt,sha256=N9tbuFkm2yikJ6JYZ_ELEjIAOuob5pzLhRE4rbjm82E,1124
h11-0.13.0.dist-info/METADATA,sha256=Fd9foEJycn0gUB9YsXul6neMlYnEU0MRQ8IUBsSOHxE,8245
h11-0.13.0.dist-info/RECORD,,
h11-0.13.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
h11-0.13.0.dist-info/top_level.txt,sha256=F7dC4jl3zeh8TGHEPaWJrMbeuoWbS379Gwdi-Yvdcis,4
h11/__init__.py,sha256=iO1KzkSO42yZ6ffg-VMgbx_ZVTWGUY00nRYEWn-s3kY,1507
h11/__pycache__/__init__.cpython-310.pyc,,
h11/__pycache__/_abnf.cpython-310.pyc,,
h11/__pycache__/_connection.cpython-310.pyc,,
h11/__pycache__/_events.cpython-310.pyc,,
h11/__pycache__/_headers.cpython-310.pyc,,
h11/__pycache__/_readers.cpython-310.pyc,,
h11/__pycache__/_receivebuffer.cpython-310.pyc,,
h11/__pycache__/_state.cpython-310.pyc,,
h11/__pycache__/_util.cpython-310.pyc,,
h11/__pycache__/_version.cpython-310.pyc,,
h11/__pycache__/_writers.cpython-310.pyc,,
h11/_abnf.py,sha256=tMKqgOEkTHHp8sPd_gmU9Qowe_yXXrihct63RX2zJsg,4637
h11/_connection.py,sha256=udHjqEO1fOcQUKa3hYIw88DMeoyG4fxiIXKjgE4DwJw,26480
h11/_events.py,sha256=LEfuvg1AbhHaVRwxCd0I-pFn9-ezUOaoL8o2Kvy1PBA,11816
h11/_headers.py,sha256=tRwZuFy5Wj4Yi9VVad_s7EqwCgeN6O3TIbcHd5CN_GI,10230
h11/_readers.py,sha256=TWWoSbLVBfYGzD5dunReTd2QCxz466wjwu-4Fkzk_sQ,8370
h11/_receivebuffer.py,sha256=xrspsdsNgWFxRfQcTXxR8RrdjRXXTK0Io5cQYWpJ1Ws,5252
h11/_state.py,sha256=F8MPHIFMJV3kUPYR3YjrjqjJ1AYp_FZ38UwGr0855lE,13184
h11/_util.py,sha256=LWkkjXyJaFlAy6Lt39w73UStklFT5ovcvo0TkY7RYuk,4888
h11/_version.py,sha256=ye-8iNs3P1TB71VRGlNQe2OxnAe-RupjozAMywAS5z8,686
h11/_writers.py,sha256=7WBTXyJqFAUqqmLl5adGF8_7UVQdOVa2phL8s8csljI,5063
h11/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7
h11/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
h11/tests/__pycache__/__init__.cpython-310.pyc,,
h11/tests/__pycache__/helpers.cpython-310.pyc,,
h11/tests/__pycache__/test_against_stdlib_http.cpython-310.pyc,,
h11/tests/__pycache__/test_connection.cpython-310.pyc,,
h11/tests/__pycache__/test_events.cpython-310.pyc,,
h11/tests/__pycache__/test_headers.cpython-310.pyc,,
h11/tests/__pycache__/test_helpers.cpython-310.pyc,,
h11/tests/__pycache__/test_io.cpython-310.pyc,,
h11/tests/__pycache__/test_receivebuffer.cpython-310.pyc,,
h11/tests/__pycache__/test_state.cpython-310.pyc,,
h11/tests/__pycache__/test_util.cpython-310.pyc,,
h11/tests/data/test-file,sha256=ZJ03Rqs98oJw29OHzJg7LlMzyGQaRAY0r3AqBeM2wVU,65
h11/tests/helpers.py,sha256=a1EVG_p7xU4wRsa3tMPTRxuaKCmretok9sxXWvqfmQA,3355
h11/tests/test_against_stdlib_http.py,sha256=cojCHgHXFQ8gWhNlEEwl3trmOpN-5uDukRoHnElqo3A,3995
h11/tests/test_connection.py,sha256=ZbPLDPclKvjgjAhgk-WlCPBaf17c4XUIV2tpaW08jOI,38720
h11/tests/test_events.py,sha256=LPVLbcV-NvPNK9fW3rraR6Bdpz1hAlsWubMtNaJ5gHg,4657
h11/tests/test_headers.py,sha256=qd8T1Zenuz5GbD6wklSJ5G8VS7trrYgMV0jT-SMvqg8,5612
h11/tests/test_helpers.py,sha256=kAo0CEM4LGqmyyP2ZFmhsyq3UFJqoFfAbzu3hbWreRM,794
h11/tests/test_io.py,sha256=gXFSKpcx6n3-Ld0Y8w5kBkom1LZsCq3uHtqdotQ3S2c,16243
h11/tests/test_receivebuffer.py,sha256=3jGbeJM36Akqg_pAhPb7XzIn2NS6RhPg-Ryg8Eu6ytk,3454
h11/tests/test_state.py,sha256=rqll9WqFsJPE0zSrtCn9LH659mPKsDeXZ-DwXwleuBQ,8928
h11/tests/test_util.py,sha256=ZWdRng_P-JP-cnvmcBhazBxfyWmEKBB0NLrDy5eq3h0,2970

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.37.0)
Root-Is-Purelib: true
Tag: py3-none-any

@ -0,0 +1,62 @@
# A highish-level implementation of the HTTP/1.1 wire protocol (RFC 7230),
# containing no networking code at all, loosely modelled on hyper-h2's generic
# implementation of HTTP/2 (and in particular the h2.connection.H2Connection
# class). There's still a bunch of subtle details you need to get right if you
# want to make this actually useful, because it doesn't implement all the
# semantics to check that what you're asking to write to the wire is sensible,
# but at least it gets you out of dealing with the wire itself.
from h11._connection import Connection, NEED_DATA, PAUSED
from h11._events import (
ConnectionClosed,
Data,
EndOfMessage,
Event,
InformationalResponse,
Request,
Response,
)
from h11._state import (
CLIENT,
CLOSED,
DONE,
ERROR,
IDLE,
MIGHT_SWITCH_PROTOCOL,
MUST_CLOSE,
SEND_BODY,
SEND_RESPONSE,
SERVER,
SWITCHED_PROTOCOL,
)
from h11._util import LocalProtocolError, ProtocolError, RemoteProtocolError
from h11._version import __version__
PRODUCT_ID = "python-h11/" + __version__
__all__ = (
"Connection",
"NEED_DATA",
"PAUSED",
"ConnectionClosed",
"Data",
"EndOfMessage",
"Event",
"InformationalResponse",
"Request",
"Response",
"CLIENT",
"CLOSED",
"DONE",
"ERROR",
"IDLE",
"MUST_CLOSE",
"SEND_BODY",
"SEND_RESPONSE",
"SERVER",
"SWITCHED_PROTOCOL",
"ProtocolError",
"LocalProtocolError",
"RemoteProtocolError",
)

@ -0,0 +1,129 @@
# We use native strings for all the re patterns, to take advantage of string
# formatting, and then convert to bytestrings when compiling the final re
# objects.
# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#whitespace
# OWS = *( SP / HTAB )
# ; optional whitespace
OWS = r"[ \t]*"
# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#rule.token.separators
# token = 1*tchar
#
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
# / DIGIT / ALPHA
# ; any VCHAR, except delimiters
token = r"[-!#$%&'*+.^_`|~0-9a-zA-Z]+"
# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#header.fields
# field-name = token
field_name = token
# The standard says:
#
# field-value = *( field-content / obs-fold )
# field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
# field-vchar = VCHAR / obs-text
# obs-fold = CRLF 1*( SP / HTAB )
# ; obsolete line folding
# ; see Section 3.2.4
#
# https://tools.ietf.org/html/rfc5234#appendix-B.1
#
# VCHAR = %x21-7E
# ; visible (printing) characters
#
# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#rule.quoted-string
# obs-text = %x80-FF
#
# However, the standard definition of field-content is WRONG! It disallows
# fields containing a single visible character surrounded by whitespace,
# e.g. "foo a bar".
#
# See: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
#
# So our definition of field_content attempts to fix it up...
#
# Also, we allow lots of control characters, because apparently people assume
# that they're legal in practice (e.g., google analytics makes cookies with
# \x01 in them!):
# https://github.com/python-hyper/h11/issues/57
# We still don't allow NUL or whitespace, because those are often treated as
# meta-characters and letting them through can lead to nasty issues like SSRF.
vchar = r"[\x21-\x7e]"
vchar_or_obs_text = r"[^\x00\s]"
field_vchar = vchar_or_obs_text
field_content = r"{field_vchar}+(?:[ \t]+{field_vchar}+)*".format(**globals())
# We handle obs-fold at a different level, and our fixed-up field_content
# already grows to swallow the whole value, so ? instead of *
field_value = r"({field_content})?".format(**globals())
# header-field = field-name ":" OWS field-value OWS
header_field = (
r"(?P<field_name>{field_name})"
r":"
r"{OWS}"
r"(?P<field_value>{field_value})"
r"{OWS}".format(**globals())
)
# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#request.line
#
# request-line = method SP request-target SP HTTP-version CRLF
# method = token
# HTTP-version = HTTP-name "/" DIGIT "." DIGIT
# HTTP-name = %x48.54.54.50 ; "HTTP", case-sensitive
#
# request-target is complicated (see RFC 7230 sec 5.3) -- could be path, full
# URL, host+port (for connect), or even "*", but in any case we are guaranteed
# that it contists of the visible printing characters.
method = token
request_target = r"{vchar}+".format(**globals())
http_version = r"HTTP/(?P<http_version>[0-9]\.[0-9])"
request_line = (
r"(?P<method>{method})"
r" "
r"(?P<target>{request_target})"
r" "
r"{http_version}".format(**globals())
)
# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#status.line
#
# status-line = HTTP-version SP status-code SP reason-phrase CRLF
# status-code = 3DIGIT
# reason-phrase = *( HTAB / SP / VCHAR / obs-text )
status_code = r"[0-9]{3}"
reason_phrase = r"([ \t]|{vchar_or_obs_text})*".format(**globals())
status_line = (
r"{http_version}"
r" "
r"(?P<status_code>{status_code})"
# However, there are apparently a few too many servers out there that just
# leave out the reason phrase:
# https://github.com/scrapy/scrapy/issues/345#issuecomment-281756036
# https://github.com/seanmonstar/httparse/issues/29
# so make it optional. ?: is a non-capturing group.
r"(?: (?P<reason>{reason_phrase}))?".format(**globals())
)
HEXDIG = r"[0-9A-Fa-f]"
# Actually
#
# chunk-size = 1*HEXDIG
#
# but we impose an upper-limit to avoid ridiculosity. len(str(2**64)) == 20
chunk_size = r"({HEXDIG}){{1,20}}".format(**globals())
# Actually
#
# chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
#
# but we aren't parsing the things so we don't really care.
chunk_ext = r";.*"
chunk_header = (
r"(?P<chunk_size>{chunk_size})"
r"(?P<chunk_ext>{chunk_ext})?"
r"\r\n".format(**globals())
)

@ -0,0 +1,631 @@
# This contains the main Connection class. Everything in h11 revolves around
# this.
from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Type, Union
from ._events import (
ConnectionClosed,
Data,
EndOfMessage,
Event,
InformationalResponse,
Request,
Response,
)
from ._headers import get_comma_header, has_expect_100_continue, set_comma_header
from ._readers import READERS, ReadersType
from ._receivebuffer import ReceiveBuffer
from ._state import (
_SWITCH_CONNECT,
_SWITCH_UPGRADE,
CLIENT,
ConnectionState,
DONE,
ERROR,
MIGHT_SWITCH_PROTOCOL,
SEND_BODY,
SERVER,
SWITCHED_PROTOCOL,
)
from ._util import ( # Import the internal things we need
LocalProtocolError,
RemoteProtocolError,
Sentinel,
)
from ._writers import WRITERS, WritersType
# Everything in __all__ gets re-exported as part of the h11 public API.
__all__ = ["Connection", "NEED_DATA", "PAUSED"]
class NEED_DATA(Sentinel, metaclass=Sentinel):
pass
class PAUSED(Sentinel, metaclass=Sentinel):
pass
# If we ever have this much buffered without it making a complete parseable
# event, we error out. The only time we really buffer is when reading the
# request/response line + headers together, so this is effectively the limit on
# the size of that.
#
# Some precedents for defaults:
# - node.js: 80 * 1024
# - tomcat: 8 * 1024
# - IIS: 16 * 1024
# - Apache: <8 KiB per line>
DEFAULT_MAX_INCOMPLETE_EVENT_SIZE = 16 * 1024
# RFC 7230's rules for connection lifecycles:
# - If either side says they want to close the connection, then the connection
# must close.
# - HTTP/1.1 defaults to keep-alive unless someone says Connection: close
# - HTTP/1.0 defaults to close unless both sides say Connection: keep-alive
# (and even this is a mess -- e.g. if you're implementing a proxy then
# sending Connection: keep-alive is forbidden).
#
# We simplify life by simply not supporting keep-alive with HTTP/1.0 peers. So
# our rule is:
# - If someone says Connection: close, we will close
# - If someone uses HTTP/1.0, we will close.
def _keep_alive(event: Union[Request, Response]) -> bool:
connection = get_comma_header(event.headers, b"connection")
if b"close" in connection:
return False
if getattr(event, "http_version", b"1.1") < b"1.1":
return False
return True
def _body_framing(
request_method: bytes, event: Union[Request, Response]
) -> Tuple[str, Union[Tuple[()], Tuple[int]]]:
# Called when we enter SEND_BODY to figure out framing information for
# this body.
#
# These are the only two events that can trigger a SEND_BODY state:
assert type(event) in (Request, Response)
# Returns one of:
#
# ("content-length", count)
# ("chunked", ())
# ("http/1.0", ())
#
# which are (lookup key, *args) for constructing body reader/writer
# objects.
#
# Reference: https://tools.ietf.org/html/rfc7230#section-3.3.3
#
# Step 1: some responses always have an empty body, regardless of what the
# headers say.
if type(event) is Response:
if (
event.status_code in (204, 304)
or request_method == b"HEAD"
or (request_method == b"CONNECT" and 200 <= event.status_code < 300)
):
return ("content-length", (0,))
# Section 3.3.3 also lists another case -- responses with status_code
# < 200. For us these are InformationalResponses, not Responses, so
# they can't get into this function in the first place.
assert event.status_code >= 200
# Step 2: check for Transfer-Encoding (T-E beats C-L):
transfer_encodings = get_comma_header(event.headers, b"transfer-encoding")
if transfer_encodings:
assert transfer_encodings == [b"chunked"]
return ("chunked", ())
# Step 3: check for Content-Length
content_lengths = get_comma_header(event.headers, b"content-length")
if content_lengths:
return ("content-length", (int(content_lengths[0]),))
# Step 4: no applicable headers; fallback/default depends on type
if type(event) is Request:
return ("content-length", (0,))
else:
return ("http/1.0", ())
################################################################
#
# The main Connection class
#
################################################################
class Connection:
"""An object encapsulating the state of an HTTP connection.
Args:
our_role: If you're implementing a client, pass :data:`h11.CLIENT`. If
you're implementing a server, pass :data:`h11.SERVER`.
max_incomplete_event_size (int):
The maximum number of bytes we're willing to buffer of an
incomplete event. In practice this mostly sets a limit on the
maximum size of the request/response line + headers. If this is
exceeded, then :meth:`next_event` will raise
:exc:`RemoteProtocolError`.
"""
def __init__(
self,
our_role: Type[Sentinel],
max_incomplete_event_size: int = DEFAULT_MAX_INCOMPLETE_EVENT_SIZE,
) -> None:
self._max_incomplete_event_size = max_incomplete_event_size
# State and role tracking
if our_role not in (CLIENT, SERVER):
raise ValueError("expected CLIENT or SERVER, not {!r}".format(our_role))
self.our_role = our_role
self.their_role: Type[Sentinel]
if our_role is CLIENT:
self.their_role = SERVER
else:
self.their_role = CLIENT
self._cstate = ConnectionState()
# Callables for converting data->events or vice-versa given the
# current state
self._writer = self._get_io_object(self.our_role, None, WRITERS)
self._reader = self._get_io_object(self.their_role, None, READERS)
# Holds any unprocessed received data
self._receive_buffer = ReceiveBuffer()
# If this is true, then it indicates that the incoming connection was
# closed *after* the end of whatever's in self._receive_buffer:
self._receive_buffer_closed = False
# Extra bits of state that don't fit into the state machine.
#
# These two are only used to interpret framing headers for figuring
# out how to read/write response bodies. their_http_version is also
# made available as a convenient public API.
self.their_http_version: Optional[bytes] = None
self._request_method: Optional[bytes] = None
# This is pure flow-control and doesn't at all affect the set of legal
# transitions, so no need to bother ConnectionState with it:
self.client_is_waiting_for_100_continue = False
@property
def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]:
"""A dictionary like::
{CLIENT: <client state>, SERVER: <server state>}
See :ref:`state-machine` for details.
"""
return dict(self._cstate.states)
@property
def our_state(self) -> Type[Sentinel]:
"""The current state of whichever role we are playing. See
:ref:`state-machine` for details.
"""
return self._cstate.states[self.our_role]
@property
def their_state(self) -> Type[Sentinel]:
"""The current state of whichever role we are NOT playing. See
:ref:`state-machine` for details.
"""
return self._cstate.states[self.their_role]
@property
def they_are_waiting_for_100_continue(self) -> bool:
return self.their_role is CLIENT and self.client_is_waiting_for_100_continue
def start_next_cycle(self) -> None:
"""Attempt to reset our connection state for a new request/response
cycle.
If both client and server are in :data:`DONE` state, then resets them
both to :data:`IDLE` state in preparation for a new request/response
cycle on this same connection. Otherwise, raises a
:exc:`LocalProtocolError`.
See :ref:`keepalive-and-pipelining`.
"""
old_states = dict(self._cstate.states)
self._cstate.start_next_cycle()
self._request_method = None
# self.their_http_version gets left alone, since it presumably lasts
# beyond a single request/response cycle
assert not self.client_is_waiting_for_100_continue
self._respond_to_state_changes(old_states)
def _process_error(self, role: Type[Sentinel]) -> None:
old_states = dict(self._cstate.states)
self._cstate.process_error(role)
self._respond_to_state_changes(old_states)
def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]:
if type(event) is InformationalResponse and event.status_code == 101:
return _SWITCH_UPGRADE
if type(event) is Response:
if (
_SWITCH_CONNECT in self._cstate.pending_switch_proposals
and 200 <= event.status_code < 300
):
return _SWITCH_CONNECT
return None
# All events go through here
def _process_event(self, role: Type[Sentinel], event: Event) -> None:
# First, pass the event through the state machine to make sure it
# succeeds.
old_states = dict(self._cstate.states)
if role is CLIENT and type(event) is Request:
if event.method == b"CONNECT":
self._cstate.process_client_switch_proposal(_SWITCH_CONNECT)
if get_comma_header(event.headers, b"upgrade"):
self._cstate.process_client_switch_proposal(_SWITCH_UPGRADE)
server_switch_event = None
if role is SERVER:
server_switch_event = self._server_switch_event(event)
self._cstate.process_event(role, type(event), server_switch_event)
# Then perform the updates triggered by it.
if type(event) is Request:
self._request_method = event.method
if role is self.their_role and type(event) in (
Request,
Response,
InformationalResponse,
):
event = cast(Union[Request, Response, InformationalResponse], event)
self.their_http_version = event.http_version
# Keep alive handling
#
# RFC 7230 doesn't really say what one should do if Connection: close
# shows up on a 1xx InformationalResponse. I think the idea is that
# this is not supposed to happen. In any case, if it does happen, we
# ignore it.
if type(event) in (Request, Response) and not _keep_alive(
cast(Union[Request, Response], event)
):
self._cstate.process_keep_alive_disabled()
# 100-continue
if type(event) is Request and has_expect_100_continue(event):
self.client_is_waiting_for_100_continue = True
if type(event) in (InformationalResponse, Response):
self.client_is_waiting_for_100_continue = False
if role is CLIENT and type(event) in (Data, EndOfMessage):
self.client_is_waiting_for_100_continue = False
self._respond_to_state_changes(old_states, event)
def _get_io_object(
self,
role: Type[Sentinel],
event: Optional[Event],
io_dict: Union[ReadersType, WritersType],
) -> Optional[Callable[..., Any]]:
# event may be None; it's only used when entering SEND_BODY
state = self._cstate.states[role]
if state is SEND_BODY:
# Special case: the io_dict has a dict of reader/writer factories
# that depend on the request/response framing.
framing_type, args = _body_framing(
cast(bytes, self._request_method), cast(Union[Request, Response], event)
)
return io_dict[SEND_BODY][framing_type](*args) # type: ignore[index]
else:
# General case: the io_dict just has the appropriate reader/writer
# for this state
return io_dict.get((role, state)) # type: ignore
# This must be called after any action that might have caused
# self._cstate.states to change.
def _respond_to_state_changes(
self,
old_states: Dict[Type[Sentinel], Type[Sentinel]],
event: Optional[Event] = None,
) -> None:
# Update reader/writer
if self.our_state != old_states[self.our_role]:
self._writer = self._get_io_object(self.our_role, event, WRITERS)
if self.their_state != old_states[self.their_role]:
self._reader = self._get_io_object(self.their_role, event, READERS)
@property
def trailing_data(self) -> Tuple[bytes, bool]:
"""Data that has been received, but not yet processed, represented as
a tuple with two elements, where the first is a byte-string containing
the unprocessed data itself, and the second is a bool that is True if
the receive connection was closed.
See :ref:`switching-protocols` for discussion of why you'd want this.
"""
return (bytes(self._receive_buffer), self._receive_buffer_closed)
def receive_data(self, data: bytes) -> None:
"""Add data to our internal receive buffer.
This does not actually do any processing on the data, just stores
it. To trigger processing, you have to call :meth:`next_event`.
Args:
data (:term:`bytes-like object`):
The new data that was just received.
Special case: If *data* is an empty byte-string like ``b""``,
then this indicates that the remote side has closed the
connection (end of file). Normally this is convenient, because
standard Python APIs like :meth:`file.read` or
:meth:`socket.recv` use ``b""`` to indicate end-of-file, while
other failures to read are indicated using other mechanisms
like raising :exc:`TimeoutError`. When using such an API you
can just blindly pass through whatever you get from ``read``
to :meth:`receive_data`, and everything will work.
But, if you have an API where reading an empty string is a
valid non-EOF condition, then you need to be aware of this and
make sure to check for such strings and avoid passing them to
:meth:`receive_data`.
Returns:
Nothing, but after calling this you should call :meth:`next_event`
to parse the newly received data.
Raises:
RuntimeError:
Raised if you pass an empty *data*, indicating EOF, and then
pass a non-empty *data*, indicating more data that somehow
arrived after the EOF.
(Calling ``receive_data(b"")`` multiple times is fine,
and equivalent to calling it once.)
"""
if data:
if self._receive_buffer_closed:
raise RuntimeError("received close, then received more data?")
self._receive_buffer += data
else:
self._receive_buffer_closed = True
def _extract_next_receive_event(self) -> Union[Event, Type[Sentinel]]:
state = self.their_state
# We don't pause immediately when they enter DONE, because even in
# DONE state we can still process a ConnectionClosed() event. But
# if we have data in our buffer, then we definitely aren't getting
# a ConnectionClosed() immediately and we need to pause.
if state is DONE and self._receive_buffer:
return PAUSED
if state is MIGHT_SWITCH_PROTOCOL or state is SWITCHED_PROTOCOL:
return PAUSED
assert self._reader is not None
event = self._reader(self._receive_buffer)
if event is None:
if not self._receive_buffer and self._receive_buffer_closed:
# In some unusual cases (basically just HTTP/1.0 bodies), EOF
# triggers an actual protocol event; in that case, we want to
# return that event, and then the state will change and we'll
# get called again to generate the actual ConnectionClosed().
if hasattr(self._reader, "read_eof"):
event = self._reader.read_eof() # type: ignore[attr-defined]
else:
event = ConnectionClosed()
if event is None:
event = NEED_DATA
return event # type: ignore[no-any-return]
def next_event(self) -> Union[Event, Type[Sentinel]]:
"""Parse the next event out of our receive buffer, update our internal
state, and return it.
This is a mutating operation -- think of it like calling :func:`next`
on an iterator.
Returns:
: One of three things:
1) An event object -- see :ref:`events`.
2) The special constant :data:`NEED_DATA`, which indicates that
you need to read more data from your socket and pass it to
:meth:`receive_data` before this method will be able to return
any more events.
3) The special constant :data:`PAUSED`, which indicates that we
are not in a state where we can process incoming data (usually
because the peer has finished their part of the current
request/response cycle, and you have not yet called
:meth:`start_next_cycle`). See :ref:`flow-control` for details.
Raises:
RemoteProtocolError:
The peer has misbehaved. You should close the connection
(possibly after sending some kind of 4xx response).
Once this method returns :class:`ConnectionClosed` once, then all
subsequent calls will also return :class:`ConnectionClosed`.
If this method raises any exception besides :exc:`RemoteProtocolError`
then that's a bug -- if it happens please file a bug report!
If this method raises any exception then it also sets
:attr:`Connection.their_state` to :data:`ERROR` -- see
:ref:`error-handling` for discussion.
"""
if self.their_state is ERROR:
raise RemoteProtocolError("Can't receive data when peer state is ERROR")
try:
event = self._extract_next_receive_event()
if event not in [NEED_DATA, PAUSED]:
self._process_event(self.their_role, cast(Event, event))
if event is NEED_DATA:
if len(self._receive_buffer) > self._max_incomplete_event_size:
# 431 is "Request header fields too large" which is pretty
# much the only situation where we can get here
raise RemoteProtocolError(
"Receive buffer too long", error_status_hint=431
)
if self._receive_buffer_closed:
# We're still trying to complete some event, but that's
# never going to happen because no more data is coming
raise RemoteProtocolError("peer unexpectedly closed connection")
return event
except BaseException as exc:
self._process_error(self.their_role)
if isinstance(exc, LocalProtocolError):
exc._reraise_as_remote_protocol_error()
else:
raise
def send(self, event: Event) -> Optional[bytes]:
"""Convert a high-level event into bytes that can be sent to the peer,
while updating our internal state machine.
Args:
event: The :ref:`event <events>` to send.
Returns:
If ``type(event) is ConnectionClosed``, then returns
``None``. Otherwise, returns a :term:`bytes-like object`.
Raises:
LocalProtocolError:
Sending this event at this time would violate our
understanding of the HTTP/1.1 protocol.
If this method raises any exception then it also sets
:attr:`Connection.our_state` to :data:`ERROR` -- see
:ref:`error-handling` for discussion.
"""
data_list = self.send_with_data_passthrough(event)
if data_list is None:
return None
else:
return b"".join(data_list)
def send_with_data_passthrough(self, event: Event) -> Optional[List[bytes]]:
"""Identical to :meth:`send`, except that in situations where
:meth:`send` returns a single :term:`bytes-like object`, this instead
returns a list of them -- and when sending a :class:`Data` event, this
list is guaranteed to contain the exact object you passed in as
:attr:`Data.data`. See :ref:`sendfile` for discussion.
"""
if self.our_state is ERROR:
raise LocalProtocolError("Can't send data when our state is ERROR")
try:
if type(event) is Response:
event = self._clean_up_response_headers_for_sending(event)
# We want to call _process_event before calling the writer,
# because if someone tries to do something invalid then this will
# give a sensible error message, while our writers all just assume
# they will only receive valid events. But, _process_event might
# change self._writer. So we have to do a little dance:
writer = self._writer
self._process_event(self.our_role, event)
if type(event) is ConnectionClosed:
return None
else:
# In any situation where writer is None, process_event should
# have raised ProtocolError
assert writer is not None
data_list: List[bytes] = []
writer(event, data_list.append)
return data_list
except:
self._process_error(self.our_role)
raise
def send_failed(self) -> None:
"""Notify the state machine that we failed to send the data it gave
us.
This causes :attr:`Connection.our_state` to immediately become
:data:`ERROR` -- see :ref:`error-handling` for discussion.
"""
self._process_error(self.our_role)
# When sending a Response, we take responsibility for a few things:
#
# - Sometimes you MUST set Connection: close. We take care of those
# times. (You can also set it yourself if you want, and if you do then
# we'll respect that and close the connection at the right time. But you
# don't have to worry about that unless you want to.)
#
# - The user has to set Content-Length if they want it. Otherwise, for
# responses that have bodies (e.g. not HEAD), then we will automatically
# select the right mechanism for streaming a body of unknown length,
# which depends on depending on the peer's HTTP version.
#
# This function's *only* responsibility is making sure headers are set up
# right -- everything downstream just looks at the headers. There are no
# side channels.
def _clean_up_response_headers_for_sending(self, response: Response) -> Response:
assert type(response) is Response
headers = response.headers
need_close = False
# HEAD requests need some special handling: they always act like they
# have Content-Length: 0, and that's how _body_framing treats
# them. But their headers are supposed to match what we would send if
# the request was a GET. (Technically there is one deviation allowed:
# we're allowed to leave out the framing headers -- see
# https://tools.ietf.org/html/rfc7231#section-4.3.2 . But it's just as
# easy to get them right.)
method_for_choosing_headers = cast(bytes, self._request_method)
if method_for_choosing_headers == b"HEAD":
method_for_choosing_headers = b"GET"
framing_type, _ = _body_framing(method_for_choosing_headers, response)
if framing_type in ("chunked", "http/1.0"):
# This response has a body of unknown length.
# If our peer is HTTP/1.1, we use Transfer-Encoding: chunked
# If our peer is HTTP/1.0, we use no framing headers, and close the
# connection afterwards.
#
# Make sure to clear Content-Length (in principle user could have
# set both and then we ignored Content-Length b/c
# Transfer-Encoding overwrote it -- this would be naughty of them,
# but the HTTP spec says that if our peer does this then we have
# to fix it instead of erroring out, so we'll accord the user the
# same respect).
headers = set_comma_header(headers, b"content-length", [])
if self.their_http_version is None or self.their_http_version < b"1.1":
# Either we never got a valid request and are sending back an
# error (their_http_version is None), so we assume the worst;
# or else we did get a valid HTTP/1.0 request, so we know that
# they don't understand chunked encoding.
headers = set_comma_header(headers, b"transfer-encoding", [])
# This is actually redundant ATM, since currently we
# unconditionally disable keep-alive when talking to HTTP/1.0
# peers. But let's be defensive just in case we add
# Connection: keep-alive support later:
if self._request_method != b"HEAD":
need_close = True
else:
headers = set_comma_header(headers, b"transfer-encoding", [b"chunked"])
if not self._cstate.keep_alive or need_close:
# Make sure Connection: close is set
connection = set(get_comma_header(headers, b"connection"))
connection.discard(b"keep-alive")
connection.add(b"close")
headers = set_comma_header(headers, b"connection", sorted(connection))
return Response(
headers=headers,
status_code=response.status_code,
http_version=response.http_version,
reason=response.reason,
)

@ -0,0 +1,369 @@
# High level events that make up HTTP/1.1 conversations. Loosely inspired by
# the corresponding events in hyper-h2:
#
# http://python-hyper.org/h2/en/stable/api.html#events
#
# Don't subclass these. Stuff will break.
import re
from abc import ABC
from dataclasses import dataclass, field
from typing import Any, cast, Dict, List, Tuple, Union
from ._abnf import method, request_target
from ._headers import Headers, normalize_and_validate
from ._util import bytesify, LocalProtocolError, validate
# Everything in __all__ gets re-exported as part of the h11 public API.
__all__ = [
"Event",
"Request",
"InformationalResponse",
"Response",
"Data",
"EndOfMessage",
"ConnectionClosed",
]
method_re = re.compile(method.encode("ascii"))
request_target_re = re.compile(request_target.encode("ascii"))
class Event(ABC):
"""
Base class for h11 events.
"""
__slots__ = ()
@dataclass(init=False, frozen=True)
class Request(Event):
"""The beginning of an HTTP request.
Fields:
.. attribute:: method
An HTTP method, e.g. ``b"GET"`` or ``b"POST"``. Always a byte
string. :term:`Bytes-like objects <bytes-like object>` and native
strings containing only ascii characters will be automatically
converted to byte strings.
.. attribute:: target
The target of an HTTP request, e.g. ``b"/index.html"``, or one of the
more exotic formats described in `RFC 7320, section 5.3
<https://tools.ietf.org/html/rfc7230#section-5.3>`_. Always a byte
string. :term:`Bytes-like objects <bytes-like object>` and native
strings containing only ascii characters will be automatically
converted to byte strings.
.. attribute:: headers
Request headers, represented as a list of (name, value) pairs. See
:ref:`the header normalization rules <headers-format>` for details.
.. attribute:: http_version
The HTTP protocol version, represented as a byte string like
``b"1.1"``. See :ref:`the HTTP version normalization rules
<http_version-format>` for details.
"""
__slots__ = ("method", "headers", "target", "http_version")
method: bytes
headers: Headers
target: bytes
http_version: bytes
def __init__(
self,
*,
method: Union[bytes, str],
headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]],
target: Union[bytes, str],
http_version: Union[bytes, str] = b"1.1",
_parsed: bool = False,
) -> None:
super().__init__()
if isinstance(headers, Headers):
object.__setattr__(self, "headers", headers)
else:
object.__setattr__(
self, "headers", normalize_and_validate(headers, _parsed=_parsed)
)
if not _parsed:
object.__setattr__(self, "method", bytesify(method))
object.__setattr__(self, "target", bytesify(target))
object.__setattr__(self, "http_version", bytesify(http_version))
else:
object.__setattr__(self, "method", method)
object.__setattr__(self, "target", target)
object.__setattr__(self, "http_version", http_version)
# "A server MUST respond with a 400 (Bad Request) status code to any
# HTTP/1.1 request message that lacks a Host header field and to any
# request message that contains more than one Host header field or a
# Host header field with an invalid field-value."
# -- https://tools.ietf.org/html/rfc7230#section-5.4
host_count = 0
for name, value in self.headers:
if name == b"host":
host_count += 1
if self.http_version == b"1.1" and host_count == 0:
raise LocalProtocolError("Missing mandatory Host: header")
if host_count > 1:
raise LocalProtocolError("Found multiple Host: headers")
validate(method_re, self.method, "Illegal method characters")
validate(request_target_re, self.target, "Illegal target characters")
# This is an unhashable type.
__hash__ = None # type: ignore
@dataclass(init=False, frozen=True)
class _ResponseBase(Event):
__slots__ = ("headers", "http_version", "reason", "status_code")
headers: Headers
http_version: bytes
reason: bytes
status_code: int
def __init__(
self,
*,
headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]],
status_code: int,
http_version: Union[bytes, str] = b"1.1",
reason: Union[bytes, str] = b"",
_parsed: bool = False,
) -> None:
super().__init__()
if isinstance(headers, Headers):
object.__setattr__(self, "headers", headers)
else:
object.__setattr__(
self, "headers", normalize_and_validate(headers, _parsed=_parsed)
)
if not _parsed:
object.__setattr__(self, "reason", bytesify(reason))
object.__setattr__(self, "http_version", bytesify(http_version))
if not isinstance(status_code, int):
raise LocalProtocolError("status code must be integer")
# Because IntEnum objects are instances of int, but aren't
# duck-compatible (sigh), see gh-72.
object.__setattr__(self, "status_code", int(status_code))
else:
object.__setattr__(self, "reason", reason)
object.__setattr__(self, "http_version", http_version)
object.__setattr__(self, "status_code", status_code)
self.__post_init__()
def __post_init__(self) -> None:
pass
# This is an unhashable type.
__hash__ = None # type: ignore
@dataclass(init=False, frozen=True)
class InformationalResponse(_ResponseBase):
"""An HTTP informational response.
Fields:
.. attribute:: status_code
The status code of this response, as an integer. For an
:class:`InformationalResponse`, this is always in the range [100,
200).
.. attribute:: headers
Request headers, represented as a list of (name, value) pairs. See
:ref:`the header normalization rules <headers-format>` for
details.
.. attribute:: http_version
The HTTP protocol version, represented as a byte string like
``b"1.1"``. See :ref:`the HTTP version normalization rules
<http_version-format>` for details.
.. attribute:: reason
The reason phrase of this response, as a byte string. For example:
``b"OK"``, or ``b"Not Found"``.
"""
def __post_init__(self) -> None:
if not (100 <= self.status_code < 200):
raise LocalProtocolError(
"InformationalResponse status_code should be in range "
"[100, 200), not {}".format(self.status_code)
)
# This is an unhashable type.
__hash__ = None # type: ignore
@dataclass(init=False, frozen=True)
class Response(_ResponseBase):
"""The beginning of an HTTP response.
Fields:
.. attribute:: status_code
The status code of this response, as an integer. For an
:class:`Response`, this is always in the range [200,
1000).
.. attribute:: headers
Request headers, represented as a list of (name, value) pairs. See
:ref:`the header normalization rules <headers-format>` for details.
.. attribute:: http_version
The HTTP protocol version, represented as a byte string like
``b"1.1"``. See :ref:`the HTTP version normalization rules
<http_version-format>` for details.
.. attribute:: reason
The reason phrase of this response, as a byte string. For example:
``b"OK"``, or ``b"Not Found"``.
"""
def __post_init__(self) -> None:
if not (200 <= self.status_code < 1000):
raise LocalProtocolError(
"Response status_code should be in range [200, 1000), not {}".format(
self.status_code
)
)
# This is an unhashable type.
__hash__ = None # type: ignore
@dataclass(init=False, frozen=True)
class Data(Event):
"""Part of an HTTP message body.
Fields:
.. attribute:: data
A :term:`bytes-like object` containing part of a message body. Or, if
using the ``combine=False`` argument to :meth:`Connection.send`, then
any object that your socket writing code knows what to do with, and for
which calling :func:`len` returns the number of bytes that will be
written -- see :ref:`sendfile` for details.
.. attribute:: chunk_start
A marker that indicates whether this data object is from the start of a
chunked transfer encoding chunk. This field is ignored when when a Data
event is provided to :meth:`Connection.send`: it is only valid on
events emitted from :meth:`Connection.next_event`. You probably
shouldn't use this attribute at all; see
:ref:`chunk-delimiters-are-bad` for details.
.. attribute:: chunk_end
A marker that indicates whether this data object is the last for a
given chunked transfer encoding chunk. This field is ignored when when
a Data event is provided to :meth:`Connection.send`: it is only valid
on events emitted from :meth:`Connection.next_event`. You probably
shouldn't use this attribute at all; see
:ref:`chunk-delimiters-are-bad` for details.
"""
__slots__ = ("data", "chunk_start", "chunk_end")
data: bytes
chunk_start: bool
chunk_end: bool
def __init__(
self, data: bytes, chunk_start: bool = False, chunk_end: bool = False
) -> None:
object.__setattr__(self, "data", data)
object.__setattr__(self, "chunk_start", chunk_start)
object.__setattr__(self, "chunk_end", chunk_end)
# This is an unhashable type.
__hash__ = None # type: ignore
# XX FIXME: "A recipient MUST ignore (or consider as an error) any fields that
# are forbidden to be sent in a trailer, since processing them as if they were
# present in the header section might bypass external security filters."
# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#chunked.trailer.part
# Unfortunately, the list of forbidden fields is long and vague :-/
@dataclass(init=False, frozen=True)
class EndOfMessage(Event):
"""The end of an HTTP message.
Fields:
.. attribute:: headers
Default value: ``[]``
Any trailing headers attached to this message, represented as a list of
(name, value) pairs. See :ref:`the header normalization rules
<headers-format>` for details.
Must be empty unless ``Transfer-Encoding: chunked`` is in use.
"""
__slots__ = ("headers",)
headers: Headers
def __init__(
self,
*,
headers: Union[
Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]], None
] = None,
_parsed: bool = False,
) -> None:
super().__init__()
if headers is None:
headers = Headers([])
elif not isinstance(headers, Headers):
headers = normalize_and_validate(headers, _parsed=_parsed)
object.__setattr__(self, "headers", headers)
# This is an unhashable type.
__hash__ = None # type: ignore
@dataclass(frozen=True)
class ConnectionClosed(Event):
"""This event indicates that the sender has closed their outgoing
connection.
Note that this does not necessarily mean that they can't *receive* further
data, because TCP connections are composed to two one-way channels which
can be closed independently. See :ref:`closing` for details.
No fields.
"""
pass

@ -0,0 +1,278 @@
import re
from typing import AnyStr, cast, List, overload, Sequence, Tuple, TYPE_CHECKING, Union
from ._abnf import field_name, field_value
from ._util import bytesify, LocalProtocolError, validate
if TYPE_CHECKING:
from ._events import Request
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal # type: ignore
# Facts
# -----
#
# Headers are:
# keys: case-insensitive ascii
# values: mixture of ascii and raw bytes
#
# "Historically, HTTP has allowed field content with text in the ISO-8859-1
# charset [ISO-8859-1], supporting other charsets only through use of
# [RFC2047] encoding. In practice, most HTTP header field values use only a
# subset of the US-ASCII charset [USASCII]. Newly defined header fields SHOULD
# limit their field values to US-ASCII octets. A recipient SHOULD treat other
# octets in field content (obs-text) as opaque data."
# And it deprecates all non-ascii values
#
# Leading/trailing whitespace in header names is forbidden
#
# Values get leading/trailing whitespace stripped
#
# Content-Disposition actually needs to contain unicode semantically; to
# accomplish this it has a terrifically weird way of encoding the filename
# itself as ascii (and even this still has lots of cross-browser
# incompatibilities)
#
# Order is important:
# "a proxy MUST NOT change the order of these field values when forwarding a
# message"
# (and there are several headers where the order indicates a preference)
#
# Multiple occurences of the same header:
# "A sender MUST NOT generate multiple header fields with the same field name
# in a message unless either the entire field value for that header field is
# defined as a comma-separated list [or the header is Set-Cookie which gets a
# special exception]" - RFC 7230. (cookies are in RFC 6265)
#
# So every header aside from Set-Cookie can be merged by b", ".join if it
# occurs repeatedly. But, of course, they can't necessarily be split by
# .split(b","), because quoting.
#
# Given all this mess (case insensitive, duplicates allowed, order is
# important, ...), there doesn't appear to be any standard way to handle
# headers in Python -- they're almost like dicts, but... actually just
# aren't. For now we punt and just use a super simple representation: headers
# are a list of pairs
#
# [(name1, value1), (name2, value2), ...]
#
# where all entries are bytestrings, names are lowercase and have no
# leading/trailing whitespace, and values are bytestrings with no
# leading/trailing whitespace. Searching and updating are done via naive O(n)
# methods.
#
# Maybe a dict-of-lists would be better?
_content_length_re = re.compile(br"[0-9]+")
_field_name_re = re.compile(field_name.encode("ascii"))
_field_value_re = re.compile(field_value.encode("ascii"))
class Headers(Sequence[Tuple[bytes, bytes]]):
"""
A list-like interface that allows iterating over headers as byte-pairs
of (lowercased-name, value).
Internally we actually store the representation as three-tuples,
including both the raw original casing, in order to preserve casing
over-the-wire, and the lowercased name, for case-insensitive comparisions.
r = Request(
method="GET",
target="/",
headers=[("Host", "example.org"), ("Connection", "keep-alive")],
http_version="1.1",
)
assert r.headers == [
(b"host", b"example.org"),
(b"connection", b"keep-alive")
]
assert r.headers.raw_items() == [
(b"Host", b"example.org"),
(b"Connection", b"keep-alive")
]
"""
__slots__ = "_full_items"
def __init__(self, full_items: List[Tuple[bytes, bytes, bytes]]) -> None:
self._full_items = full_items
def __bool__(self) -> bool:
return bool(self._full_items)
def __eq__(self, other: object) -> bool:
return list(self) == list(other) # type: ignore
def __len__(self) -> int:
return len(self._full_items)
def __repr__(self) -> str:
return "<Headers(%s)>" % repr(list(self))
def __getitem__(self, idx: int) -> Tuple[bytes, bytes]: # type: ignore[override]
_, name, value = self._full_items[idx]
return (name, value)
def raw_items(self) -> List[Tuple[bytes, bytes]]:
return [(raw_name, value) for raw_name, _, value in self._full_items]
HeaderTypes = Union[
List[Tuple[bytes, bytes]],
List[Tuple[bytes, str]],
List[Tuple[str, bytes]],
List[Tuple[str, str]],
]
@overload
def normalize_and_validate(headers: Headers, _parsed: Literal[True]) -> Headers:
...
@overload
def normalize_and_validate(headers: HeaderTypes, _parsed: Literal[False]) -> Headers:
...
@overload
def normalize_and_validate(
headers: Union[Headers, HeaderTypes], _parsed: bool = False
) -> Headers:
...
def normalize_and_validate(
headers: Union[Headers, HeaderTypes], _parsed: bool = False
) -> Headers:
new_headers = []
seen_content_length = None
saw_transfer_encoding = False
for name, value in headers:
# For headers coming out of the parser, we can safely skip some steps,
# because it always returns bytes and has already run these regexes
# over the data:
if not _parsed:
name = bytesify(name)
value = bytesify(value)
validate(_field_name_re, name, "Illegal header name {!r}", name)
validate(_field_value_re, value, "Illegal header value {!r}", value)
assert isinstance(name, bytes)
assert isinstance(value, bytes)
raw_name = name
name = name.lower()
if name == b"content-length":
lengths = {length.strip() for length in value.split(b",")}
if len(lengths) != 1:
raise LocalProtocolError("conflicting Content-Length headers")
value = lengths.pop()
validate(_content_length_re, value, "bad Content-Length")
if seen_content_length is None:
seen_content_length = value
new_headers.append((raw_name, name, value))
elif seen_content_length != value:
raise LocalProtocolError("conflicting Content-Length headers")
elif name == b"transfer-encoding":
# "A server that receives a request message with a transfer coding
# it does not understand SHOULD respond with 501 (Not
# Implemented)."
# https://tools.ietf.org/html/rfc7230#section-3.3.1
if saw_transfer_encoding:
raise LocalProtocolError(
"multiple Transfer-Encoding headers", error_status_hint=501
)
# "All transfer-coding names are case-insensitive"
# -- https://tools.ietf.org/html/rfc7230#section-4
value = value.lower()
if value != b"chunked":
raise LocalProtocolError(
"Only Transfer-Encoding: chunked is supported",
error_status_hint=501,
)
saw_transfer_encoding = True
new_headers.append((raw_name, name, value))
else:
new_headers.append((raw_name, name, value))
return Headers(new_headers)
def get_comma_header(headers: Headers, name: bytes) -> List[bytes]:
# Should only be used for headers whose value is a list of
# comma-separated, case-insensitive values.
#
# The header name `name` is expected to be lower-case bytes.
#
# Connection: meets these criteria (including cast insensitivity).
#
# Content-Length: technically is just a single value (1*DIGIT), but the
# standard makes reference to implementations that do multiple values, and
# using this doesn't hurt. Ditto, case insensitivity doesn't things either
# way.
#
# Transfer-Encoding: is more complex (allows for quoted strings), so
# splitting on , is actually wrong. For example, this is legal:
#
# Transfer-Encoding: foo; options="1,2", chunked
#
# and should be parsed as
#
# foo; options="1,2"
# chunked
#
# but this naive function will parse it as
#
# foo; options="1
# 2"
# chunked
#
# However, this is okay because the only thing we are going to do with
# any Transfer-Encoding is reject ones that aren't just "chunked", so
# both of these will be treated the same anyway.
#
# Expect: the only legal value is the literal string
# "100-continue". Splitting on commas is harmless. Case insensitive.
#
out: List[bytes] = []
for _, found_name, found_raw_value in headers._full_items:
if found_name == name:
found_raw_value = found_raw_value.lower()
for found_split_value in found_raw_value.split(b","):
found_split_value = found_split_value.strip()
if found_split_value:
out.append(found_split_value)
return out
def set_comma_header(headers: Headers, name: bytes, new_values: List[bytes]) -> Headers:
# The header name `name` is expected to be lower-case bytes.
#
# Note that when we store the header we use title casing for the header
# names, in order to match the conventional HTTP header style.
#
# Simply calling `.title()` is a blunt approach, but it's correct
# here given the cases where we're using `set_comma_header`...
#
# Connection, Content-Length, Transfer-Encoding.
new_headers: List[Tuple[bytes, bytes]] = []
for found_raw_name, found_name, found_raw_value in headers._full_items:
if found_name != name:
new_headers.append((found_raw_name, found_raw_value))
for new_value in new_values:
new_headers.append((name.title(), new_value))
return normalize_and_validate(new_headers)
def has_expect_100_continue(request: "Request") -> bool:
# https://tools.ietf.org/html/rfc7231#section-5.1.1
# "A server that receives a 100-continue expectation in an HTTP/1.0 request
# MUST ignore that expectation."
if request.http_version < b"1.1":
return False
expect = get_comma_header(request.headers, b"expect")
return b"100-continue" in expect

@ -0,0 +1,249 @@
# Code to read HTTP data
#
# Strategy: each reader is a callable which takes a ReceiveBuffer object, and
# either:
# 1) consumes some of it and returns an Event
# 2) raises a LocalProtocolError (for consistency -- e.g. we call validate()
# and it might raise a LocalProtocolError, so simpler just to always use
# this)
# 3) returns None, meaning "I need more data"
#
# If they have a .read_eof attribute, then this will be called if an EOF is
# received -- but this is optional. Either way, the actual ConnectionClosed
# event will be generated afterwards.
#
# READERS is a dict describing how to pick a reader. It maps states to either:
# - a reader
# - or, for body readers, a dict of per-framing reader factories
import re
from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union
from ._abnf import chunk_header, header_field, request_line, status_line
from ._events import Data, EndOfMessage, InformationalResponse, Request, Response
from ._receivebuffer import ReceiveBuffer
from ._state import (
CLIENT,
CLOSED,
DONE,
IDLE,
MUST_CLOSE,
SEND_BODY,
SEND_RESPONSE,
SERVER,
)
from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate
__all__ = ["READERS"]
header_field_re = re.compile(header_field.encode("ascii"))
# Remember that this has to run in O(n) time -- so e.g. the bytearray cast is
# critical.
obs_fold_re = re.compile(br"[ \t]+")
def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]:
it = iter(lines)
last: Optional[bytes] = None
for line in it:
match = obs_fold_re.match(line)
if match:
if last is None:
raise LocalProtocolError("continuation line at start of headers")
if not isinstance(last, bytearray):
last = bytearray(last)
last += b" "
last += line[match.end() :]
else:
if last is not None:
yield last
last = line
if last is not None:
yield last
def _decode_header_lines(
lines: Iterable[bytes],
) -> Iterable[Tuple[bytes, bytes]]:
for line in _obsolete_line_fold(lines):
matches = validate(header_field_re, line, "illegal header line: {!r}", line)
yield (matches["field_name"], matches["field_value"])
request_line_re = re.compile(request_line.encode("ascii"))
def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]:
lines = buf.maybe_extract_lines()
if lines is None:
if buf.is_next_line_obviously_invalid_request_line():
raise LocalProtocolError("illegal request line")
return None
if not lines:
raise LocalProtocolError("no request line received")
matches = validate(
request_line_re, lines[0], "illegal request line: {!r}", lines[0]
)
return Request(
headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches
)
status_line_re = re.compile(status_line.encode("ascii"))
def maybe_read_from_SEND_RESPONSE_server(
buf: ReceiveBuffer,
) -> Union[InformationalResponse, Response, None]:
lines = buf.maybe_extract_lines()
if lines is None:
if buf.is_next_line_obviously_invalid_request_line():
raise LocalProtocolError("illegal request line")
return None
if not lines:
raise LocalProtocolError("no response line received")
matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0])
http_version = (
b"1.1" if matches["http_version"] is None else matches["http_version"]
)
reason = b"" if matches["reason"] is None else matches["reason"]
status_code = int(matches["status_code"])
class_: Union[Type[InformationalResponse], Type[Response]] = (
InformationalResponse if status_code < 200 else Response
)
return class_(
headers=list(_decode_header_lines(lines[1:])),
_parsed=True,
status_code=status_code,
reason=reason,
http_version=http_version,
)
class ContentLengthReader:
def __init__(self, length: int) -> None:
self._length = length
self._remaining = length
def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
if self._remaining == 0:
return EndOfMessage()
data = buf.maybe_extract_at_most(self._remaining)
if data is None:
return None
self._remaining -= len(data)
return Data(data=data)
def read_eof(self) -> NoReturn:
raise RemoteProtocolError(
"peer closed connection without sending complete message body "
"(received {} bytes, expected {})".format(
self._length - self._remaining, self._length
)
)
chunk_header_re = re.compile(chunk_header.encode("ascii"))
class ChunkedReader:
def __init__(self) -> None:
self._bytes_in_chunk = 0
# After reading a chunk, we have to throw away the trailing \r\n; if
# this is >0 then we discard that many bytes before resuming regular
# de-chunkification.
self._bytes_to_discard = 0
self._reading_trailer = False
def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
if self._reading_trailer:
lines = buf.maybe_extract_lines()
if lines is None:
return None
return EndOfMessage(headers=list(_decode_header_lines(lines)))
if self._bytes_to_discard > 0:
data = buf.maybe_extract_at_most(self._bytes_to_discard)
if data is None:
return None
self._bytes_to_discard -= len(data)
if self._bytes_to_discard > 0:
return None
# else, fall through and read some more
assert self._bytes_to_discard == 0
if self._bytes_in_chunk == 0:
# We need to refill our chunk count
chunk_header = buf.maybe_extract_next_line()
if chunk_header is None:
return None
matches = validate(
chunk_header_re,
chunk_header,
"illegal chunk header: {!r}",
chunk_header,
)
# XX FIXME: we discard chunk extensions. Does anyone care?
self._bytes_in_chunk = int(matches["chunk_size"], base=16)
if self._bytes_in_chunk == 0:
self._reading_trailer = True
return self(buf)
chunk_start = True
else:
chunk_start = False
assert self._bytes_in_chunk > 0
data = buf.maybe_extract_at_most(self._bytes_in_chunk)
if data is None:
return None
self._bytes_in_chunk -= len(data)
if self._bytes_in_chunk == 0:
self._bytes_to_discard = 2
chunk_end = True
else:
chunk_end = False
return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end)
def read_eof(self) -> NoReturn:
raise RemoteProtocolError(
"peer closed connection without sending complete message body "
"(incomplete chunked read)"
)
class Http10Reader:
def __call__(self, buf: ReceiveBuffer) -> Optional[Data]:
data = buf.maybe_extract_at_most(999999999)
if data is None:
return None
return Data(data=data)
def read_eof(self) -> EndOfMessage:
return EndOfMessage()
def expect_nothing(buf: ReceiveBuffer) -> None:
if buf:
raise LocalProtocolError("Got data when expecting EOF")
return None
ReadersType = Dict[
Union[Sentinel, Tuple[Sentinel, Sentinel]],
Union[Callable[..., Any], Dict[str, Callable[..., Any]]],
]
READERS: ReadersType = {
(CLIENT, IDLE): maybe_read_from_IDLE_client,
(SERVER, IDLE): maybe_read_from_SEND_RESPONSE_server,
(SERVER, SEND_RESPONSE): maybe_read_from_SEND_RESPONSE_server,
(CLIENT, DONE): expect_nothing,
(CLIENT, MUST_CLOSE): expect_nothing,
(CLIENT, CLOSED): expect_nothing,
(SERVER, DONE): expect_nothing,
(SERVER, MUST_CLOSE): expect_nothing,
(SERVER, CLOSED): expect_nothing,
SEND_BODY: {
"chunked": ChunkedReader,
"content-length": ContentLengthReader,
"http/1.0": Http10Reader,
},
}

@ -0,0 +1,153 @@
import re
import sys
from typing import List, Optional, Union
__all__ = ["ReceiveBuffer"]
# Operations we want to support:
# - find next \r\n or \r\n\r\n (\n or \n\n are also acceptable),
# or wait until there is one
# - read at-most-N bytes
# Goals:
# - on average, do this fast
# - worst case, do this in O(n) where n is the number of bytes processed
# Plan:
# - store bytearray, offset, how far we've searched for a separator token
# - use the how-far-we've-searched data to avoid rescanning
# - while doing a stream of uninterrupted processing, advance offset instead
# of constantly copying
# WARNING:
# - I haven't benchmarked or profiled any of this yet.
#
# Note that starting in Python 3.4, deleting the initial n bytes from a
# bytearray is amortized O(n), thanks to some excellent work by Antoine
# Martin:
#
# https://bugs.python.org/issue19087
#
# This means that if we only supported 3.4+, we could get rid of the code here
# involving self._start and self.compress, because it's doing exactly the same
# thing that bytearray now does internally.
#
# BUT unfortunately, we still support 2.7, and reading short segments out of a
# long buffer MUST be O(bytes read) to avoid DoS issues, so we can't actually
# delete this code. Yet:
#
# https://pythonclock.org/
#
# (Two things to double-check first though: make sure PyPy also has the
# optimization, and benchmark to make sure it's a win, since we do have a
# slightly clever thing where we delay calling compress() until we've
# processed a whole event, which could in theory be slightly more efficient
# than the internal bytearray support.)
blank_line_regex = re.compile(b"\n\r?\n", re.MULTILINE)
class ReceiveBuffer:
def __init__(self) -> None:
self._data = bytearray()
self._next_line_search = 0
self._multiple_lines_search = 0
def __iadd__(self, byteslike: Union[bytes, bytearray]) -> "ReceiveBuffer":
self._data += byteslike
return self
def __bool__(self) -> bool:
return bool(len(self))
def __len__(self) -> int:
return len(self._data)
# for @property unprocessed_data
def __bytes__(self) -> bytes:
return bytes(self._data)
def _extract(self, count: int) -> bytearray:
# extracting an initial slice of the data buffer and return it
out = self._data[:count]
del self._data[:count]
self._next_line_search = 0
self._multiple_lines_search = 0
return out
def maybe_extract_at_most(self, count: int) -> Optional[bytearray]:
"""
Extract a fixed number of bytes from the buffer.
"""
out = self._data[:count]
if not out:
return None
return self._extract(count)
def maybe_extract_next_line(self) -> Optional[bytearray]:
"""
Extract the first line, if it is completed in the buffer.
"""
# Only search in buffer space that we've not already looked at.
search_start_index = max(0, self._next_line_search - 1)
partial_idx = self._data.find(b"\r\n", search_start_index)
if partial_idx == -1:
self._next_line_search = len(self._data)
return None
# + 2 is to compensate len(b"\r\n")
idx = partial_idx + 2
return self._extract(idx)
def maybe_extract_lines(self) -> Optional[List[bytearray]]:
"""
Extract everything up to the first blank line, and return a list of lines.
"""
# Handle the case where we have an immediate empty line.
if self._data[:1] == b"\n":
self._extract(1)
return []
if self._data[:2] == b"\r\n":
self._extract(2)
return []
# Only search in buffer space that we've not already looked at.
match = blank_line_regex.search(self._data, self._multiple_lines_search)
if match is None:
self._multiple_lines_search = max(0, len(self._data) - 2)
return None
# Truncate the buffer and return it.
idx = match.span(0)[-1]
out = self._extract(idx)
lines = out.split(b"\n")
for line in lines:
if line.endswith(b"\r"):
del line[-1]
assert lines[-2] == lines[-1] == b""
del lines[-2:]
return lines
# In theory we should wait until `\r\n` before starting to validate
# incoming data. However it's interesting to detect (very) invalid data
# early given they might not even contain `\r\n` at all (hence only
# timeout will get rid of them).
# This is not a 100% effective detection but more of a cheap sanity check
# allowing for early abort in some useful cases.
# This is especially interesting when peer is messing up with HTTPS and
# sent us a TLS stream where we were expecting plain HTTP given all
# versions of TLS so far start handshake with a 0x16 message type code.
def is_next_line_obviously_invalid_request_line(self) -> bool:
try:
# HTTP header line must not contain non-printable characters
# and should not start with a space
return self._data[0] < 0x21
except IndexError:
return False

@ -0,0 +1,363 @@
################################################################
# The core state machine
################################################################
#
# Rule 1: everything that affects the state machine and state transitions must
# live here in this file. As much as possible goes into the table-based
# representation, but for the bits that don't quite fit, the actual code and
# state must nonetheless live here.
#
# Rule 2: this file does not know about what role we're playing; it only knows
# about HTTP request/response cycles in the abstract. This ensures that we
# don't cheat and apply different rules to local and remote parties.
#
#
# Theory of operation
# ===================
#
# Possibly the simplest way to think about this is that we actually have 5
# different state machines here. Yes, 5. These are:
#
# 1) The client state, with its complicated automaton (see the docs)
# 2) The server state, with its complicated automaton (see the docs)
# 3) The keep-alive state, with possible states {True, False}
# 4) The SWITCH_CONNECT state, with possible states {False, True}
# 5) The SWITCH_UPGRADE state, with possible states {False, True}
#
# For (3)-(5), the first state listed is the initial state.
#
# (1)-(3) are stored explicitly in member variables. The last
# two are stored implicitly in the pending_switch_proposals set as:
# (state of 4) == (_SWITCH_CONNECT in pending_switch_proposals)
# (state of 5) == (_SWITCH_UPGRADE in pending_switch_proposals)
#
# And each of these machines has two different kinds of transitions:
#
# a) Event-triggered
# b) State-triggered
#
# Event triggered is the obvious thing that you'd think it is: some event
# happens, and if it's the right event at the right time then a transition
# happens. But there are somewhat complicated rules for which machines can
# "see" which events. (As a rule of thumb, if a machine "sees" an event, this
# means two things: the event can affect the machine, and if the machine is
# not in a state where it expects that event then it's an error.) These rules
# are:
#
# 1) The client machine sees all h11.events objects emitted by the client.
#
# 2) The server machine sees all h11.events objects emitted by the server.
#
# It also sees the client's Request event.
#
# And sometimes, server events are annotated with a _SWITCH_* event. For
# example, we can have a (Response, _SWITCH_CONNECT) event, which is
# different from a regular Response event.
#
# 3) The keep-alive machine sees the process_keep_alive_disabled() event
# (which is derived from Request/Response events), and this event
# transitions it from True -> False, or from False -> False. There's no way
# to transition back.
#
# 4&5) The _SWITCH_* machines transition from False->True when we get a
# Request that proposes the relevant type of switch (via
# process_client_switch_proposals), and they go from True->False when we
# get a Response that has no _SWITCH_* annotation.
#
# So that's event-triggered transitions.
#
# State-triggered transitions are less standard. What they do here is couple
# the machines together. The way this works is, when certain *joint*
# configurations of states are achieved, then we automatically transition to a
# new *joint* state. So, for example, if we're ever in a joint state with
#
# client: DONE
# keep-alive: False
#
# then the client state immediately transitions to:
#
# client: MUST_CLOSE
#
# This is fundamentally different from an event-based transition, because it
# doesn't matter how we arrived at the {client: DONE, keep-alive: False} state
# -- maybe the client transitioned SEND_BODY -> DONE, or keep-alive
# transitioned True -> False. Either way, once this precondition is satisfied,
# this transition is immediately triggered.
#
# What if two conflicting state-based transitions get enabled at the same
# time? In practice there's only one case where this arises (client DONE ->
# MIGHT_SWITCH_PROTOCOL versus DONE -> MUST_CLOSE), and we resolve it by
# explicitly prioritizing the DONE -> MIGHT_SWITCH_PROTOCOL transition.
#
# Implementation
# --------------
#
# The event-triggered transitions for the server and client machines are all
# stored explicitly in a table. Ditto for the state-triggered transitions that
# involve just the server and client state.
#
# The transitions for the other machines, and the state-triggered transitions
# that involve the other machines, are written out as explicit Python code.
#
# It'd be nice if there were some cleaner way to do all this. This isn't
# *too* terrible, but I feel like it could probably be better.
#
# WARNING
# -------
#
# The script that generates the state machine diagrams for the docs knows how
# to read out the EVENT_TRIGGERED_TRANSITIONS and STATE_TRIGGERED_TRANSITIONS
# tables. But it can't automatically read the transitions that are written
# directly in Python code. So if you touch those, you need to also update the
# script to keep it in sync!
from typing import cast, Dict, Optional, Set, Tuple, Type, Union
from ._events import *
from ._util import LocalProtocolError, Sentinel
# Everything in __all__ gets re-exported as part of the h11 public API.
__all__ = [
"CLIENT",
"SERVER",
"IDLE",
"SEND_RESPONSE",
"SEND_BODY",
"DONE",
"MUST_CLOSE",
"CLOSED",
"MIGHT_SWITCH_PROTOCOL",
"SWITCHED_PROTOCOL",
"ERROR",
]
class CLIENT(Sentinel, metaclass=Sentinel):
pass
class SERVER(Sentinel, metaclass=Sentinel):
pass
# States
class IDLE(Sentinel, metaclass=Sentinel):
pass
class SEND_RESPONSE(Sentinel, metaclass=Sentinel):
pass
class SEND_BODY(Sentinel, metaclass=Sentinel):
pass
class DONE(Sentinel, metaclass=Sentinel):
pass
class MUST_CLOSE(Sentinel, metaclass=Sentinel):
pass
class CLOSED(Sentinel, metaclass=Sentinel):
pass
class ERROR(Sentinel, metaclass=Sentinel):
pass
# Switch types
class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel):
pass
class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel):
pass
class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel):
pass
class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel):
pass
EventTransitionType = Dict[
Type[Sentinel],
Dict[
Type[Sentinel],
Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]],
],
]
EVENT_TRIGGERED_TRANSITIONS: EventTransitionType = {
CLIENT: {
IDLE: {Request: SEND_BODY, ConnectionClosed: CLOSED},
SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE},
DONE: {ConnectionClosed: CLOSED},
MUST_CLOSE: {ConnectionClosed: CLOSED},
CLOSED: {ConnectionClosed: CLOSED},
MIGHT_SWITCH_PROTOCOL: {},
SWITCHED_PROTOCOL: {},
ERROR: {},
},
SERVER: {
IDLE: {
ConnectionClosed: CLOSED,
Response: SEND_BODY,
# Special case: server sees client Request events, in this form
(Request, CLIENT): SEND_RESPONSE,
},
SEND_RESPONSE: {
InformationalResponse: SEND_RESPONSE,
Response: SEND_BODY,
(InformationalResponse, _SWITCH_UPGRADE): SWITCHED_PROTOCOL,
(Response, _SWITCH_CONNECT): SWITCHED_PROTOCOL,
},
SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE},
DONE: {ConnectionClosed: CLOSED},
MUST_CLOSE: {ConnectionClosed: CLOSED},
CLOSED: {ConnectionClosed: CLOSED},
SWITCHED_PROTOCOL: {},
ERROR: {},
},
}
# NB: there are also some special-case state-triggered transitions hard-coded
# into _fire_state_triggered_transitions below.
STATE_TRIGGERED_TRANSITIONS = {
# (Client state, Server state) -> new states
# Protocol negotiation
(MIGHT_SWITCH_PROTOCOL, SWITCHED_PROTOCOL): {CLIENT: SWITCHED_PROTOCOL},
# Socket shutdown
(CLOSED, DONE): {SERVER: MUST_CLOSE},
(CLOSED, IDLE): {SERVER: MUST_CLOSE},
(ERROR, DONE): {SERVER: MUST_CLOSE},
(DONE, CLOSED): {CLIENT: MUST_CLOSE},
(IDLE, CLOSED): {CLIENT: MUST_CLOSE},
(DONE, ERROR): {CLIENT: MUST_CLOSE},
}
class ConnectionState:
def __init__(self) -> None:
# Extra bits of state that don't quite fit into the state model.
# If this is False then it enables the automatic DONE -> MUST_CLOSE
# transition. Don't set this directly; call .keep_alive_disabled()
self.keep_alive = True
# This is a subset of {UPGRADE, CONNECT}, containing the proposals
# made by the client for switching protocols.
self.pending_switch_proposals: Set[Type[Sentinel]] = set()
self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE}
def process_error(self, role: Type[Sentinel]) -> None:
self.states[role] = ERROR
self._fire_state_triggered_transitions()
def process_keep_alive_disabled(self) -> None:
self.keep_alive = False
self._fire_state_triggered_transitions()
def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None:
self.pending_switch_proposals.add(switch_event)
self._fire_state_triggered_transitions()
def process_event(
self,
role: Type[Sentinel],
event_type: Type[Event],
server_switch_event: Optional[Type[Sentinel]] = None,
) -> None:
_event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type
if server_switch_event is not None:
assert role is SERVER
if server_switch_event not in self.pending_switch_proposals:
raise LocalProtocolError(
"Received server {} event without a pending proposal".format(
server_switch_event
)
)
_event_type = (event_type, server_switch_event)
if server_switch_event is None and _event_type is Response:
self.pending_switch_proposals = set()
self._fire_event_triggered_transitions(role, _event_type)
# Special case: the server state does get to see Request
# events.
if _event_type is Request:
assert role is CLIENT
self._fire_event_triggered_transitions(SERVER, (Request, CLIENT))
self._fire_state_triggered_transitions()
def _fire_event_triggered_transitions(
self,
role: Type[Sentinel],
event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]],
) -> None:
state = self.states[role]
try:
new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
except KeyError:
event_type = cast(Type[Event], event_type)
raise LocalProtocolError(
"can't handle event type {} when role={} and state={}".format(
event_type.__name__, role, self.states[role]
)
) from None
self.states[role] = new_state
def _fire_state_triggered_transitions(self) -> None:
# We apply these rules repeatedly until converging on a fixed point
while True:
start_states = dict(self.states)
# It could happen that both these special-case transitions are
# enabled at the same time:
#
# DONE -> MIGHT_SWITCH_PROTOCOL
# DONE -> MUST_CLOSE
#
# For example, this will always be true of a HTTP/1.0 client
# requesting CONNECT. If this happens, the protocol switch takes
# priority. From there the client will either go to
# SWITCHED_PROTOCOL, in which case it's none of our business when
# they close the connection, or else the server will deny the
# request, in which case the client will go back to DONE and then
# from there to MUST_CLOSE.
if self.pending_switch_proposals:
if self.states[CLIENT] is DONE:
self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL
if not self.pending_switch_proposals:
if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL:
self.states[CLIENT] = DONE
if not self.keep_alive:
for role in (CLIENT, SERVER):
if self.states[role] is DONE:
self.states[role] = MUST_CLOSE
# Tabular state-triggered transitions
joint_state = (self.states[CLIENT], self.states[SERVER])
changes = STATE_TRIGGERED_TRANSITIONS.get(joint_state, {})
self.states.update(changes) # type: ignore
if self.states == start_states:
# Fixed point reached
return
def start_next_cycle(self) -> None:
if self.states != {CLIENT: DONE, SERVER: DONE}:
raise LocalProtocolError(
"not in a reusable state. self.states={}".format(self.states)
)
# Can't reach DONE/DONE with any of these active, but still, let's be
# sure.
assert self.keep_alive
assert not self.pending_switch_proposals
self.states = {CLIENT: IDLE, SERVER: IDLE}

@ -0,0 +1,135 @@
from typing import Any, Dict, NoReturn, Pattern, Tuple, Type, TypeVar, Union
__all__ = [
"ProtocolError",
"LocalProtocolError",
"RemoteProtocolError",
"validate",
"bytesify",
]
class ProtocolError(Exception):
"""Exception indicating a violation of the HTTP/1.1 protocol.
This as an abstract base class, with two concrete base classes:
:exc:`LocalProtocolError`, which indicates that you tried to do something
that HTTP/1.1 says is illegal, and :exc:`RemoteProtocolError`, which
indicates that the remote peer tried to do something that HTTP/1.1 says is
illegal. See :ref:`error-handling` for details.
In addition to the normal :exc:`Exception` features, it has one attribute:
.. attribute:: error_status_hint
This gives a suggestion as to what status code a server might use if
this error occurred as part of a request.
For a :exc:`RemoteProtocolError`, this is useful as a suggestion for
how you might want to respond to a misbehaving peer, if you're
implementing a server.
For a :exc:`LocalProtocolError`, this can be taken as a suggestion for
how your peer might have responded to *you* if h11 had allowed you to
continue.
The default is 400 Bad Request, a generic catch-all for protocol
violations.
"""
def __init__(self, msg: str, error_status_hint: int = 400) -> None:
if type(self) is ProtocolError:
raise TypeError("tried to directly instantiate ProtocolError")
Exception.__init__(self, msg)
self.error_status_hint = error_status_hint
# Strategy: there are a number of public APIs where a LocalProtocolError can
# be raised (send(), all the different event constructors, ...), and only one
# public API where RemoteProtocolError can be raised
# (receive_data()). Therefore we always raise LocalProtocolError internally,
# and then receive_data will translate this into a RemoteProtocolError.
#
# Internally:
# LocalProtocolError is the generic "ProtocolError".
# Externally:
# LocalProtocolError is for local errors and RemoteProtocolError is for
# remote errors.
class LocalProtocolError(ProtocolError):
def _reraise_as_remote_protocol_error(self) -> NoReturn:
# After catching a LocalProtocolError, use this method to re-raise it
# as a RemoteProtocolError. This method must be called from inside an
# except: block.
#
# An easy way to get an equivalent RemoteProtocolError is just to
# modify 'self' in place.
self.__class__ = RemoteProtocolError # type: ignore
# But the re-raising is somewhat non-trivial -- you might think that
# now that we've modified the in-flight exception object, that just
# doing 'raise' to re-raise it would be enough. But it turns out that
# this doesn't work, because Python tracks the exception type
# (exc_info[0]) separately from the exception object (exc_info[1]),
# and we only modified the latter. So we really do need to re-raise
# the new type explicitly.
# On py3, the traceback is part of the exception object, so our
# in-place modification preserved it and we can just re-raise:
raise self
class RemoteProtocolError(ProtocolError):
pass
def validate(
regex: Pattern[bytes], data: bytes, msg: str = "malformed data", *format_args: Any
) -> Dict[str, bytes]:
match = regex.fullmatch(data)
if not match:
if format_args:
msg = msg.format(*format_args)
raise LocalProtocolError(msg)
return match.groupdict()
# Sentinel values
#
# - Inherit identity-based comparison and hashing from object
# - Have a nice repr
# - Have a *bonus property*: type(sentinel) is sentinel
#
# The bonus property is useful if you want to take the return value from
# next_event() and do some sort of dispatch based on type(event).
_T_Sentinel = TypeVar("_T_Sentinel", bound="Sentinel")
class Sentinel(type):
def __new__(
cls: Type[_T_Sentinel],
name: str,
bases: Tuple[type, ...],
namespace: Dict[str, Any],
**kwds: Any
) -> _T_Sentinel:
assert bases == (Sentinel,)
v = super().__new__(cls, name, bases, namespace, **kwds)
v.__class__ = v # type: ignore
return v
def __repr__(self) -> str:
return self.__name__
# Used for methods, request targets, HTTP versions, header names, and header
# values. Accepts ascii-strings, or bytes/bytearray/memoryview/..., and always
# returns bytes.
def bytesify(s: Union[bytes, bytearray, memoryview, int, str]) -> bytes:
# Fast-path:
if type(s) is bytes:
return s
if isinstance(s, str):
s = s.encode("ascii")
if isinstance(s, int):
raise TypeError("expected bytes-like object, not int")
return bytes(s)

@ -0,0 +1,16 @@
# This file must be kept very simple, because it is consumed from several
# places -- it is imported by h11/__init__.py, execfile'd by setup.py, etc.
# We use a simple scheme:
# 1.0.0 -> 1.0.0+dev -> 1.1.0 -> 1.1.0+dev
# where the +dev versions are never released into the wild, they're just what
# we stick into the VCS in between releases.
#
# This is compatible with PEP 440:
# http://legacy.python.org/dev/peps/pep-0440/
# via the use of the "local suffix" "+dev", which is disallowed on index
# servers and causes 1.0.0+dev to sort after plain 1.0.0, which is what we
# want. (Contrast with the special suffix 1.0.0.dev, which sorts *before*
# 1.0.0.)
__version__ = "0.13.0"

@ -0,0 +1,145 @@
# Code to read HTTP data
#
# Strategy: each writer takes an event + a write-some-bytes function, which is
# calls.
#
# WRITERS is a dict describing how to pick a reader. It maps states to either:
# - a writer
# - or, for body writers, a dict of framin-dependent writer factories
from typing import Any, Callable, Dict, List, Tuple, Type, Union
from ._events import Data, EndOfMessage, Event, InformationalResponse, Request, Response
from ._headers import Headers
from ._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER
from ._util import LocalProtocolError, Sentinel
__all__ = ["WRITERS"]
Writer = Callable[[bytes], Any]
def write_headers(headers: Headers, write: Writer) -> None:
# "Since the Host field-value is critical information for handling a
# request, a user agent SHOULD generate Host as the first header field
# following the request-line." - RFC 7230
raw_items = headers._full_items
for raw_name, name, value in raw_items:
if name == b"host":
write(b"%s: %s\r\n" % (raw_name, value))
for raw_name, name, value in raw_items:
if name != b"host":
write(b"%s: %s\r\n" % (raw_name, value))
write(b"\r\n")
def write_request(request: Request, write: Writer) -> None:
if request.http_version != b"1.1":
raise LocalProtocolError("I only send HTTP/1.1")
write(b"%s %s HTTP/1.1\r\n" % (request.method, request.target))
write_headers(request.headers, write)
# Shared between InformationalResponse and Response
def write_any_response(
response: Union[InformationalResponse, Response], write: Writer
) -> None:
if response.http_version != b"1.1":
raise LocalProtocolError("I only send HTTP/1.1")
status_bytes = str(response.status_code).encode("ascii")
# We don't bother sending ascii status messages like "OK"; they're
# optional and ignored by the protocol. (But the space after the numeric
# status code is mandatory.)
#
# XX FIXME: could at least make an effort to pull out the status message
# from stdlib's http.HTTPStatus table. Or maybe just steal their enums
# (either by import or copy/paste). We already accept them as status codes
# since they're of type IntEnum < int.
write(b"HTTP/1.1 %s %s\r\n" % (status_bytes, response.reason))
write_headers(response.headers, write)
class BodyWriter:
def __call__(self, event: Event, write: Writer) -> None:
if type(event) is Data:
self.send_data(event.data, write)
elif type(event) is EndOfMessage:
self.send_eom(event.headers, write)
else: # pragma: no cover
assert False
def send_data(self, data: bytes, write: Writer) -> None:
pass
def send_eom(self, headers: Headers, write: Writer) -> None:
pass
#
# These are all careful not to do anything to 'data' except call len(data) and
# write(data). This allows us to transparently pass-through funny objects,
# like placeholder objects referring to files on disk that will be sent via
# sendfile(2).
#
class ContentLengthWriter(BodyWriter):
def __init__(self, length: int) -> None:
self._length = length
def send_data(self, data: bytes, write: Writer) -> None:
self._length -= len(data)
if self._length < 0:
raise LocalProtocolError("Too much data for declared Content-Length")
write(data)
def send_eom(self, headers: Headers, write: Writer) -> None:
if self._length != 0:
raise LocalProtocolError("Too little data for declared Content-Length")
if headers:
raise LocalProtocolError("Content-Length and trailers don't mix")
class ChunkedWriter(BodyWriter):
def send_data(self, data: bytes, write: Writer) -> None:
# if we encoded 0-length data in the naive way, it would look like an
# end-of-message.
if not data:
return
write(b"%x\r\n" % len(data))
write(data)
write(b"\r\n")
def send_eom(self, headers: Headers, write: Writer) -> None:
write(b"0\r\n")
write_headers(headers, write)
class Http10Writer(BodyWriter):
def send_data(self, data: bytes, write: Writer) -> None:
write(data)
def send_eom(self, headers: Headers, write: Writer) -> None:
if headers:
raise LocalProtocolError("can't send trailers to HTTP/1.0 client")
# no need to close the socket ourselves, that will be taken care of by
# Connection: close machinery
WritersType = Dict[
Union[Tuple[Sentinel, Sentinel], Sentinel],
Union[
Dict[str, Type[BodyWriter]],
Callable[[Union[InformationalResponse, Response], Writer], None],
Callable[[Request, Writer], None],
],
]
WRITERS: WritersType = {
(CLIENT, IDLE): write_request,
(SERVER, IDLE): write_any_response,
(SERVER, SEND_RESPONSE): write_any_response,
SEND_BODY: {
"chunked": ChunkedWriter,
"content-length": ContentLengthWriter,
"http/1.0": Http10Writer,
},
}

@ -0,0 +1 @@
92b12bc045050b55b848d37167a1a63947c364579889ce1d39788e45e9fac9e5

@ -0,0 +1,101 @@
from typing import cast, List, Type, Union, ValuesView
from .._connection import Connection, NEED_DATA, PAUSED
from .._events import (
ConnectionClosed,
Data,
EndOfMessage,
Event,
InformationalResponse,
Request,
Response,
)
from .._state import CLIENT, CLOSED, DONE, MUST_CLOSE, SERVER
from .._util import Sentinel
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal # type: ignore
def get_all_events(conn: Connection) -> List[Event]:
got_events = []
while True:
event = conn.next_event()
if event in (NEED_DATA, PAUSED):
break
event = cast(Event, event)
got_events.append(event)
if type(event) is ConnectionClosed:
break
return got_events
def receive_and_get(conn: Connection, data: bytes) -> List[Event]:
conn.receive_data(data)
return get_all_events(conn)
# Merges adjacent Data events, converts payloads to bytestrings, and removes
# chunk boundaries.
def normalize_data_events(in_events: List[Event]) -> List[Event]:
out_events: List[Event] = []
for event in in_events:
if type(event) is Data:
event = Data(data=bytes(event.data), chunk_start=False, chunk_end=False)
if out_events and type(out_events[-1]) is type(event) is Data:
out_events[-1] = Data(
data=out_events[-1].data + event.data,
chunk_start=out_events[-1].chunk_start,
chunk_end=out_events[-1].chunk_end,
)
else:
out_events.append(event)
return out_events
# Given that we want to write tests that push some events through a Connection
# and check that its state updates appropriately... we might as make a habit
# of pushing them through two Connections with a fake network link in
# between.
class ConnectionPair:
def __init__(self) -> None:
self.conn = {CLIENT: Connection(CLIENT), SERVER: Connection(SERVER)}
self.other = {CLIENT: SERVER, SERVER: CLIENT}
@property
def conns(self) -> ValuesView[Connection]:
return self.conn.values()
# expect="match" if expect=send_events; expect=[...] to say what expected
def send(
self,
role: Type[Sentinel],
send_events: Union[List[Event], Event],
expect: Union[List[Event], Event, Literal["match"]] = "match",
) -> bytes:
if not isinstance(send_events, list):
send_events = [send_events]
data = b""
closed = False
for send_event in send_events:
new_data = self.conn[role].send(send_event)
if new_data is None:
closed = True
else:
data += new_data
# send uses b"" to mean b"", and None to mean closed
# receive uses b"" to mean closed, and None to mean "try again"
# so we have to translate between the two conventions
if data:
self.conn[self.other[role]].receive_data(data)
if closed:
self.conn[self.other[role]].receive_data(b"")
got_events = get_all_events(self.conn[self.other[role]])
if expect == "match":
expect = send_events
if not isinstance(expect, list):
expect = [expect]
assert got_events == expect
return data

@ -0,0 +1,115 @@
import json
import os.path
import socket
import socketserver
import threading
from contextlib import closing, contextmanager
from http.server import SimpleHTTPRequestHandler
from typing import Callable, Generator
from urllib.request import urlopen
import h11
@contextmanager
def socket_server(
handler: Callable[..., socketserver.BaseRequestHandler]
) -> Generator[socketserver.TCPServer, None, None]:
httpd = socketserver.TCPServer(("127.0.0.1", 0), handler)
thread = threading.Thread(
target=httpd.serve_forever, kwargs={"poll_interval": 0.01}
)
thread.daemon = True
try:
thread.start()
yield httpd
finally:
httpd.shutdown()
test_file_path = os.path.join(os.path.dirname(__file__), "data/test-file")
with open(test_file_path, "rb") as f:
test_file_data = f.read()
class SingleMindedRequestHandler(SimpleHTTPRequestHandler):
def translate_path(self, path: str) -> str:
return test_file_path
def test_h11_as_client() -> None:
with socket_server(SingleMindedRequestHandler) as httpd:
with closing(socket.create_connection(httpd.server_address)) as s:
c = h11.Connection(h11.CLIENT)
s.sendall(
c.send( # type: ignore[arg-type]
h11.Request(
method="GET", target="/foo", headers=[("Host", "localhost")]
)
)
)
s.sendall(c.send(h11.EndOfMessage())) # type: ignore[arg-type]
data = bytearray()
while True:
event = c.next_event()
print(event)
if event is h11.NEED_DATA:
# Use a small read buffer to make things more challenging
# and exercise more paths :-)
c.receive_data(s.recv(10))
continue
if type(event) is h11.Response:
assert event.status_code == 200
if type(event) is h11.Data:
data += event.data
if type(event) is h11.EndOfMessage:
break
assert bytes(data) == test_file_data
class H11RequestHandler(socketserver.BaseRequestHandler):
def handle(self) -> None:
with closing(self.request) as s:
c = h11.Connection(h11.SERVER)
request = None
while True:
event = c.next_event()
if event is h11.NEED_DATA:
# Use a small read buffer to make things more challenging
# and exercise more paths :-)
c.receive_data(s.recv(10))
continue
if type(event) is h11.Request:
request = event
if type(event) is h11.EndOfMessage:
break
assert request is not None
info = json.dumps(
{
"method": request.method.decode("ascii"),
"target": request.target.decode("ascii"),
"headers": {
name.decode("ascii"): value.decode("ascii")
for (name, value) in request.headers
},
}
)
s.sendall(c.send(h11.Response(status_code=200, headers=[]))) # type: ignore[arg-type]
s.sendall(c.send(h11.Data(data=info.encode("ascii"))))
s.sendall(c.send(h11.EndOfMessage()))
def test_h11_as_server() -> None:
with socket_server(H11RequestHandler) as httpd:
host, port = httpd.server_address
url = "http://{}:{}/some-path".format(host, port)
with closing(urlopen(url)) as f:
assert f.getcode() == 200
data = f.read()
info = json.loads(data.decode("ascii"))
print(info)
assert info["method"] == "GET"
assert info["target"] == "/some-path"
assert "urllib" in info["headers"]["user-agent"]

@ -0,0 +1,150 @@
from http import HTTPStatus
import pytest
from .. import _events
from .._events import (
ConnectionClosed,
Data,
EndOfMessage,
Event,
InformationalResponse,
Request,
Response,
)
from .._util import LocalProtocolError
def test_events() -> None:
with pytest.raises(LocalProtocolError):
# Missing Host:
req = Request(
method="GET", target="/", headers=[("a", "b")], http_version="1.1"
)
# But this is okay (HTTP/1.0)
req = Request(method="GET", target="/", headers=[("a", "b")], http_version="1.0")
# fields are normalized
assert req.method == b"GET"
assert req.target == b"/"
assert req.headers == [(b"a", b"b")]
assert req.http_version == b"1.0"
# This is also okay -- has a Host (with weird capitalization, which is ok)
req = Request(
method="GET",
target="/",
headers=[("a", "b"), ("hOSt", "example.com")],
http_version="1.1",
)
# we normalize header capitalization
assert req.headers == [(b"a", b"b"), (b"host", b"example.com")]
# Multiple host is bad too
with pytest.raises(LocalProtocolError):
req = Request(
method="GET",
target="/",
headers=[("Host", "a"), ("Host", "a")],
http_version="1.1",
)
# Even for HTTP/1.0
with pytest.raises(LocalProtocolError):
req = Request(
method="GET",
target="/",
headers=[("Host", "a"), ("Host", "a")],
http_version="1.0",
)
# Header values are validated
for bad_char in "\x00\r\n\f\v":
with pytest.raises(LocalProtocolError):
req = Request(
method="GET",
target="/",
headers=[("Host", "a"), ("Foo", "asd" + bad_char)],
http_version="1.0",
)
# But for compatibility we allow non-whitespace control characters, even
# though they're forbidden by the spec.
Request(
method="GET",
target="/",
headers=[("Host", "a"), ("Foo", "asd\x01\x02\x7f")],
http_version="1.0",
)
# Request target is validated
for bad_byte in b"\x00\x20\x7f\xee":
target = bytearray(b"/")
target.append(bad_byte)
with pytest.raises(LocalProtocolError):
Request(
method="GET", target=target, headers=[("Host", "a")], http_version="1.1"
)
# Request method is validated
with pytest.raises(LocalProtocolError):
Request(
method="GET / HTTP/1.1",
target=target,
headers=[("Host", "a")],
http_version="1.1",
)
ir = InformationalResponse(status_code=100, headers=[("Host", "a")])
assert ir.status_code == 100
assert ir.headers == [(b"host", b"a")]
assert ir.http_version == b"1.1"
with pytest.raises(LocalProtocolError):
InformationalResponse(status_code=200, headers=[("Host", "a")])
resp = Response(status_code=204, headers=[], http_version="1.0") # type: ignore[arg-type]
assert resp.status_code == 204
assert resp.headers == []
assert resp.http_version == b"1.0"
with pytest.raises(LocalProtocolError):
resp = Response(status_code=100, headers=[], http_version="1.0") # type: ignore[arg-type]
with pytest.raises(LocalProtocolError):
Response(status_code="100", headers=[], http_version="1.0") # type: ignore[arg-type]
with pytest.raises(LocalProtocolError):
InformationalResponse(status_code=b"100", headers=[], http_version="1.0") # type: ignore[arg-type]
d = Data(data=b"asdf")
assert d.data == b"asdf"
eom = EndOfMessage()
assert eom.headers == []
cc = ConnectionClosed()
assert repr(cc) == "ConnectionClosed()"
def test_intenum_status_code() -> None:
# https://github.com/python-hyper/h11/issues/72
r = Response(status_code=HTTPStatus.OK, headers=[], http_version="1.0") # type: ignore[arg-type]
assert r.status_code == HTTPStatus.OK
assert type(r.status_code) is not type(HTTPStatus.OK)
assert type(r.status_code) is int
def test_header_casing() -> None:
r = Request(
method="GET",
target="/",
headers=[("Host", "example.org"), ("Connection", "keep-alive")],
http_version="1.1",
)
assert len(r.headers) == 2
assert r.headers[0] == (b"host", b"example.org")
assert r.headers == [(b"host", b"example.org"), (b"connection", b"keep-alive")]
assert r.headers.raw_items() == [
(b"Host", b"example.org"),
(b"Connection", b"keep-alive"),
]

@ -0,0 +1,157 @@
import pytest
from .._events import Request
from .._headers import (
get_comma_header,
has_expect_100_continue,
Headers,
normalize_and_validate,
set_comma_header,
)
from .._util import LocalProtocolError
def test_normalize_and_validate() -> None:
assert normalize_and_validate([("foo", "bar")]) == [(b"foo", b"bar")]
assert normalize_and_validate([(b"foo", b"bar")]) == [(b"foo", b"bar")]
# no leading/trailing whitespace in names
with pytest.raises(LocalProtocolError):
normalize_and_validate([(b"foo ", "bar")])
with pytest.raises(LocalProtocolError):
normalize_and_validate([(b" foo", "bar")])
# no weird characters in names
with pytest.raises(LocalProtocolError) as excinfo:
normalize_and_validate([(b"foo bar", b"baz")])
assert "foo bar" in str(excinfo.value)
with pytest.raises(LocalProtocolError):
normalize_and_validate([(b"foo\x00bar", b"baz")])
# Not even 8-bit characters:
with pytest.raises(LocalProtocolError):
normalize_and_validate([(b"foo\xffbar", b"baz")])
# And not even the control characters we allow in values:
with pytest.raises(LocalProtocolError):
normalize_and_validate([(b"foo\x01bar", b"baz")])
# no return or NUL characters in values
with pytest.raises(LocalProtocolError) as excinfo:
normalize_and_validate([("foo", "bar\rbaz")])
assert "bar\\rbaz" in str(excinfo.value)
with pytest.raises(LocalProtocolError):
normalize_and_validate([("foo", "bar\nbaz")])
with pytest.raises(LocalProtocolError):
normalize_and_validate([("foo", "bar\x00baz")])
# no leading/trailing whitespace
with pytest.raises(LocalProtocolError):
normalize_and_validate([("foo", "barbaz ")])
with pytest.raises(LocalProtocolError):
normalize_and_validate([("foo", " barbaz")])
with pytest.raises(LocalProtocolError):
normalize_and_validate([("foo", "barbaz\t")])
with pytest.raises(LocalProtocolError):
normalize_and_validate([("foo", "\tbarbaz")])
# content-length
assert normalize_and_validate([("Content-Length", "1")]) == [
(b"content-length", b"1")
]
with pytest.raises(LocalProtocolError):
normalize_and_validate([("Content-Length", "asdf")])
with pytest.raises(LocalProtocolError):
normalize_and_validate([("Content-Length", "1x")])
with pytest.raises(LocalProtocolError):
normalize_and_validate([("Content-Length", "1"), ("Content-Length", "2")])
assert normalize_and_validate(
[("Content-Length", "0"), ("Content-Length", "0")]
) == [(b"content-length", b"0")]
assert normalize_and_validate([("Content-Length", "0 , 0")]) == [
(b"content-length", b"0")
]
with pytest.raises(LocalProtocolError):
normalize_and_validate(
[("Content-Length", "1"), ("Content-Length", "1"), ("Content-Length", "2")]
)
with pytest.raises(LocalProtocolError):
normalize_and_validate([("Content-Length", "1 , 1,2")])
# transfer-encoding
assert normalize_and_validate([("Transfer-Encoding", "chunked")]) == [
(b"transfer-encoding", b"chunked")
]
assert normalize_and_validate([("Transfer-Encoding", "cHuNkEd")]) == [
(b"transfer-encoding", b"chunked")
]
with pytest.raises(LocalProtocolError) as excinfo:
normalize_and_validate([("Transfer-Encoding", "gzip")])
assert excinfo.value.error_status_hint == 501 # Not Implemented
with pytest.raises(LocalProtocolError) as excinfo:
normalize_and_validate(
[("Transfer-Encoding", "chunked"), ("Transfer-Encoding", "gzip")]
)
assert excinfo.value.error_status_hint == 501 # Not Implemented
def test_get_set_comma_header() -> None:
headers = normalize_and_validate(
[
("Connection", "close"),
("whatever", "something"),
("connectiON", "fOo,, , BAR"),
]
)
assert get_comma_header(headers, b"connection") == [b"close", b"foo", b"bar"]
headers = set_comma_header(headers, b"newthing", ["a", "b"]) # type: ignore
with pytest.raises(LocalProtocolError):
set_comma_header(headers, b"newthing", [" a", "b"]) # type: ignore
assert headers == [
(b"connection", b"close"),
(b"whatever", b"something"),
(b"connection", b"fOo,, , BAR"),
(b"newthing", b"a"),
(b"newthing", b"b"),
]
headers = set_comma_header(headers, b"whatever", ["different thing"]) # type: ignore
assert headers == [
(b"connection", b"close"),
(b"connection", b"fOo,, , BAR"),
(b"newthing", b"a"),
(b"newthing", b"b"),
(b"whatever", b"different thing"),
]
def test_has_100_continue() -> None:
assert has_expect_100_continue(
Request(
method="GET",
target="/",
headers=[("Host", "example.com"), ("Expect", "100-continue")],
)
)
assert not has_expect_100_continue(
Request(method="GET", target="/", headers=[("Host", "example.com")])
)
# Case insensitive
assert has_expect_100_continue(
Request(
method="GET",
target="/",
headers=[("Host", "example.com"), ("Expect", "100-Continue")],
)
)
# Doesn't work in HTTP/1.0
assert not has_expect_100_continue(
Request(
method="GET",
target="/",
headers=[("Host", "example.com"), ("Expect", "100-continue")],
http_version="1.0",
)
)

@ -0,0 +1,32 @@
from .._events import (
ConnectionClosed,
Data,
EndOfMessage,
Event,
InformationalResponse,
Request,
Response,
)
from .helpers import normalize_data_events
def test_normalize_data_events() -> None:
assert normalize_data_events(
[
Data(data=bytearray(b"1")),
Data(data=b"2"),
Response(status_code=200, headers=[]), # type: ignore[arg-type]
Data(data=b"3"),
Data(data=b"4"),
EndOfMessage(),
Data(data=b"5"),
Data(data=b"6"),
Data(data=b"7"),
]
) == [
Data(data=b"12"),
Response(status_code=200, headers=[]), # type: ignore[arg-type]
Data(data=b"34"),
EndOfMessage(),
Data(data=b"567"),
]

@ -0,0 +1,566 @@
from typing import Any, Callable, Generator, List
import pytest
from .._events import (
ConnectionClosed,
Data,
EndOfMessage,
Event,
InformationalResponse,
Request,
Response,
)
from .._headers import Headers, normalize_and_validate
from .._readers import (
_obsolete_line_fold,
ChunkedReader,
ContentLengthReader,
Http10Reader,
READERS,
)
from .._receivebuffer import ReceiveBuffer
from .._state import (
CLIENT,
CLOSED,
DONE,
IDLE,
MIGHT_SWITCH_PROTOCOL,
MUST_CLOSE,
SEND_BODY,
SEND_RESPONSE,
SERVER,
SWITCHED_PROTOCOL,
)
from .._util import LocalProtocolError
from .._writers import (
ChunkedWriter,
ContentLengthWriter,
Http10Writer,
write_any_response,
write_headers,
write_request,
WRITERS,
)
from .helpers import normalize_data_events
SIMPLE_CASES = [
(
(CLIENT, IDLE),
Request(
method="GET",
target="/a",
headers=[("Host", "foo"), ("Connection", "close")],
),
b"GET /a HTTP/1.1\r\nHost: foo\r\nConnection: close\r\n\r\n",
),
(
(SERVER, SEND_RESPONSE),
Response(status_code=200, headers=[("Connection", "close")], reason=b"OK"),
b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n",
),
(
(SERVER, SEND_RESPONSE),
Response(status_code=200, headers=[], reason=b"OK"), # type: ignore[arg-type]
b"HTTP/1.1 200 OK\r\n\r\n",
),
(
(SERVER, SEND_RESPONSE),
InformationalResponse(
status_code=101, headers=[("Upgrade", "websocket")], reason=b"Upgrade"
),
b"HTTP/1.1 101 Upgrade\r\nUpgrade: websocket\r\n\r\n",
),
(
(SERVER, SEND_RESPONSE),
InformationalResponse(status_code=101, headers=[], reason=b"Upgrade"), # type: ignore[arg-type]
b"HTTP/1.1 101 Upgrade\r\n\r\n",
),
]
def dowrite(writer: Callable[..., None], obj: Any) -> bytes:
got_list: List[bytes] = []
writer(obj, got_list.append)
return b"".join(got_list)
def tw(writer: Any, obj: Any, expected: Any) -> None:
got = dowrite(writer, obj)
assert got == expected
def makebuf(data: bytes) -> ReceiveBuffer:
buf = ReceiveBuffer()
buf += data
return buf
def tr(reader: Any, data: bytes, expected: Any) -> None:
def check(got: Any) -> None:
assert got == expected
# Headers should always be returned as bytes, not e.g. bytearray
# https://github.com/python-hyper/wsproto/pull/54#issuecomment-377709478
for name, value in getattr(got, "headers", []):
assert type(name) is bytes
assert type(value) is bytes
# Simple: consume whole thing
buf = makebuf(data)
check(reader(buf))
assert not buf
# Incrementally growing buffer
buf = ReceiveBuffer()
for i in range(len(data)):
assert reader(buf) is None
buf += data[i : i + 1]
check(reader(buf))
# Trailing data
buf = makebuf(data)
buf += b"trailing"
check(reader(buf))
assert bytes(buf) == b"trailing"
def test_writers_simple() -> None:
for ((role, state), event, binary) in SIMPLE_CASES:
tw(WRITERS[role, state], event, binary)
def test_readers_simple() -> None:
for ((role, state), event, binary) in SIMPLE_CASES:
tr(READERS[role, state], binary, event)
def test_writers_unusual() -> None:
# Simple test of the write_headers utility routine
tw(
write_headers,
normalize_and_validate([("foo", "bar"), ("baz", "quux")]),
b"foo: bar\r\nbaz: quux\r\n\r\n",
)
tw(write_headers, Headers([]), b"\r\n")
# We understand HTTP/1.0, but we don't speak it
with pytest.raises(LocalProtocolError):
tw(
write_request,
Request(
method="GET",
target="/",
headers=[("Host", "foo"), ("Connection", "close")],
http_version="1.0",
),
None,
)
with pytest.raises(LocalProtocolError):
tw(
write_any_response,
Response(
status_code=200, headers=[("Connection", "close")], http_version="1.0"
),
None,
)
def test_readers_unusual() -> None:
# Reading HTTP/1.0
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.0\r\nSome: header\r\n\r\n",
Request(
method="HEAD",
target="/foo",
headers=[("Some", "header")],
http_version="1.0",
),
)
# check no-headers, since it's only legal with HTTP/1.0
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.0\r\n\r\n",
Request(method="HEAD", target="/foo", headers=[], http_version="1.0"), # type: ignore[arg-type]
)
tr(
READERS[SERVER, SEND_RESPONSE],
b"HTTP/1.0 200 OK\r\nSome: header\r\n\r\n",
Response(
status_code=200,
headers=[("Some", "header")],
http_version="1.0",
reason=b"OK",
),
)
# single-character header values (actually disallowed by the ABNF in RFC
# 7230 -- this is a bug in the standard that we originally copied...)
tr(
READERS[SERVER, SEND_RESPONSE],
b"HTTP/1.0 200 OK\r\n" b"Foo: a a a a a \r\n\r\n",
Response(
status_code=200,
headers=[("Foo", "a a a a a")],
http_version="1.0",
reason=b"OK",
),
)
# Empty headers -- also legal
tr(
READERS[SERVER, SEND_RESPONSE],
b"HTTP/1.0 200 OK\r\n" b"Foo:\r\n\r\n",
Response(
status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK"
),
)
tr(
READERS[SERVER, SEND_RESPONSE],
b"HTTP/1.0 200 OK\r\n" b"Foo: \t \t \r\n\r\n",
Response(
status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK"
),
)
# Tolerate broken servers that leave off the response code
tr(
READERS[SERVER, SEND_RESPONSE],
b"HTTP/1.0 200\r\n" b"Foo: bar\r\n\r\n",
Response(
status_code=200, headers=[("Foo", "bar")], http_version="1.0", reason=b""
),
)
# Tolerate headers line endings (\r\n and \n)
# \n\r\b between headers and body
tr(
READERS[SERVER, SEND_RESPONSE],
b"HTTP/1.1 200 OK\r\nSomeHeader: val\n\r\n",
Response(
status_code=200,
headers=[("SomeHeader", "val")],
http_version="1.1",
reason="OK",
),
)
# delimited only with \n
tr(
READERS[SERVER, SEND_RESPONSE],
b"HTTP/1.1 200 OK\nSomeHeader1: val1\nSomeHeader2: val2\n\n",
Response(
status_code=200,
headers=[("SomeHeader1", "val1"), ("SomeHeader2", "val2")],
http_version="1.1",
reason="OK",
),
)
# mixed \r\n and \n
tr(
READERS[SERVER, SEND_RESPONSE],
b"HTTP/1.1 200 OK\r\nSomeHeader1: val1\nSomeHeader2: val2\n\r\n",
Response(
status_code=200,
headers=[("SomeHeader1", "val1"), ("SomeHeader2", "val2")],
http_version="1.1",
reason="OK",
),
)
# obsolete line folding
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.1\r\n"
b"Host: example.com\r\n"
b"Some: multi-line\r\n"
b" header\r\n"
b"\tnonsense\r\n"
b" \t \t\tI guess\r\n"
b"Connection: close\r\n"
b"More-nonsense: in the\r\n"
b" last header \r\n\r\n",
Request(
method="HEAD",
target="/foo",
headers=[
("Host", "example.com"),
("Some", "multi-line header nonsense I guess"),
("Connection", "close"),
("More-nonsense", "in the last header"),
],
),
)
with pytest.raises(LocalProtocolError):
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.1\r\n" b" folded: line\r\n\r\n",
None,
)
with pytest.raises(LocalProtocolError):
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.1\r\n" b"foo : line\r\n\r\n",
None,
)
with pytest.raises(LocalProtocolError):
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n",
None,
)
with pytest.raises(LocalProtocolError):
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n",
None,
)
with pytest.raises(LocalProtocolError):
tr(READERS[CLIENT, IDLE], b"HEAD /foo HTTP/1.1\r\n" b": line\r\n\r\n", None)
def test__obsolete_line_fold_bytes() -> None:
# _obsolete_line_fold has a defensive cast to bytearray, which is
# necessary to protect against O(n^2) behavior in case anyone ever passes
# in regular bytestrings... but right now we never pass in regular
# bytestrings. so this test just exists to get some coverage on that
# defensive cast.
assert list(_obsolete_line_fold([b"aaa", b"bbb", b" ccc", b"ddd"])) == [
b"aaa",
bytearray(b"bbb ccc"),
b"ddd",
]
def _run_reader_iter(
reader: Any, buf: bytes, do_eof: bool
) -> Generator[Any, None, None]:
while True:
event = reader(buf)
if event is None:
break
yield event
# body readers have undefined behavior after returning EndOfMessage,
# because this changes the state so they don't get called again
if type(event) is EndOfMessage:
break
if do_eof:
assert not buf
yield reader.read_eof()
def _run_reader(*args: Any) -> List[Event]:
events = list(_run_reader_iter(*args))
return normalize_data_events(events)
def t_body_reader(thunk: Any, data: bytes, expected: Any, do_eof: bool = False) -> None:
# Simple: consume whole thing
print("Test 1")
buf = makebuf(data)
assert _run_reader(thunk(), buf, do_eof) == expected
# Incrementally growing buffer
print("Test 2")
reader = thunk()
buf = ReceiveBuffer()
events = []
for i in range(len(data)):
events += _run_reader(reader, buf, False)
buf += data[i : i + 1]
events += _run_reader(reader, buf, do_eof)
assert normalize_data_events(events) == expected
is_complete = any(type(event) is EndOfMessage for event in expected)
if is_complete and not do_eof:
buf = makebuf(data + b"trailing")
assert _run_reader(thunk(), buf, False) == expected
def test_ContentLengthReader() -> None:
t_body_reader(lambda: ContentLengthReader(0), b"", [EndOfMessage()])
t_body_reader(
lambda: ContentLengthReader(10),
b"0123456789",
[Data(data=b"0123456789"), EndOfMessage()],
)
def test_Http10Reader() -> None:
t_body_reader(Http10Reader, b"", [EndOfMessage()], do_eof=True)
t_body_reader(Http10Reader, b"asdf", [Data(data=b"asdf")], do_eof=False)
t_body_reader(
Http10Reader, b"asdf", [Data(data=b"asdf"), EndOfMessage()], do_eof=True
)
def test_ChunkedReader() -> None:
t_body_reader(ChunkedReader, b"0\r\n\r\n", [EndOfMessage()])
t_body_reader(
ChunkedReader,
b"0\r\nSome: header\r\n\r\n",
[EndOfMessage(headers=[("Some", "header")])],
)
t_body_reader(
ChunkedReader,
b"5\r\n01234\r\n"
+ b"10\r\n0123456789abcdef\r\n"
+ b"0\r\n"
+ b"Some: header\r\n\r\n",
[
Data(data=b"012340123456789abcdef"),
EndOfMessage(headers=[("Some", "header")]),
],
)
t_body_reader(
ChunkedReader,
b"5\r\n01234\r\n" + b"10\r\n0123456789abcdef\r\n" + b"0\r\n\r\n",
[Data(data=b"012340123456789abcdef"), EndOfMessage()],
)
# handles upper and lowercase hex
t_body_reader(
ChunkedReader,
b"aA\r\n" + b"x" * 0xAA + b"\r\n" + b"0\r\n\r\n",
[Data(data=b"x" * 0xAA), EndOfMessage()],
)
# refuses arbitrarily long chunk integers
with pytest.raises(LocalProtocolError):
# Technically this is legal HTTP/1.1, but we refuse to process chunk
# sizes that don't fit into 20 characters of hex
t_body_reader(ChunkedReader, b"9" * 100 + b"\r\nxxx", [Data(data=b"xxx")])
# refuses garbage in the chunk count
with pytest.raises(LocalProtocolError):
t_body_reader(ChunkedReader, b"10\x00\r\nxxx", None)
# handles (and discards) "chunk extensions" omg wtf
t_body_reader(
ChunkedReader,
b"5; hello=there\r\n"
+ b"xxxxx"
+ b"\r\n"
+ b'0; random="junk"; some=more; canbe=lonnnnngg\r\n\r\n',
[Data(data=b"xxxxx"), EndOfMessage()],
)
def test_ContentLengthWriter() -> None:
w = ContentLengthWriter(5)
assert dowrite(w, Data(data=b"123")) == b"123"
assert dowrite(w, Data(data=b"45")) == b"45"
assert dowrite(w, EndOfMessage()) == b""
w = ContentLengthWriter(5)
with pytest.raises(LocalProtocolError):
dowrite(w, Data(data=b"123456"))
w = ContentLengthWriter(5)
dowrite(w, Data(data=b"123"))
with pytest.raises(LocalProtocolError):
dowrite(w, Data(data=b"456"))
w = ContentLengthWriter(5)
dowrite(w, Data(data=b"123"))
with pytest.raises(LocalProtocolError):
dowrite(w, EndOfMessage())
w = ContentLengthWriter(5)
dowrite(w, Data(data=b"123")) == b"123"
dowrite(w, Data(data=b"45")) == b"45"
with pytest.raises(LocalProtocolError):
dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
def test_ChunkedWriter() -> None:
w = ChunkedWriter()
assert dowrite(w, Data(data=b"aaa")) == b"3\r\naaa\r\n"
assert dowrite(w, Data(data=b"a" * 20)) == b"14\r\n" + b"a" * 20 + b"\r\n"
assert dowrite(w, Data(data=b"")) == b""
assert dowrite(w, EndOfMessage()) == b"0\r\n\r\n"
assert (
dowrite(w, EndOfMessage(headers=[("Etag", "asdf"), ("a", "b")]))
== b"0\r\nEtag: asdf\r\na: b\r\n\r\n"
)
def test_Http10Writer() -> None:
w = Http10Writer()
assert dowrite(w, Data(data=b"1234")) == b"1234"
assert dowrite(w, EndOfMessage()) == b""
with pytest.raises(LocalProtocolError):
dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
def test_reject_garbage_after_request_line() -> None:
with pytest.raises(LocalProtocolError):
tr(READERS[SERVER, SEND_RESPONSE], b"HTTP/1.0 200 OK\x00xxxx\r\n\r\n", None)
def test_reject_garbage_after_response_line() -> None:
with pytest.raises(LocalProtocolError):
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.1 xxxxxx\r\n" b"Host: a\r\n\r\n",
None,
)
def test_reject_garbage_in_header_line() -> None:
with pytest.raises(LocalProtocolError):
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.1\r\n" b"Host: foo\x00bar\r\n\r\n",
None,
)
def test_reject_non_vchar_in_path() -> None:
for bad_char in b"\x00\x20\x7f\xee":
message = bytearray(b"HEAD /")
message.append(bad_char)
message.extend(b" HTTP/1.1\r\nHost: foobar\r\n\r\n")
with pytest.raises(LocalProtocolError):
tr(READERS[CLIENT, IDLE], message, None)
# https://github.com/python-hyper/h11/issues/57
def test_allow_some_garbage_in_cookies() -> None:
tr(
READERS[CLIENT, IDLE],
b"HEAD /foo HTTP/1.1\r\n"
b"Host: foo\r\n"
b"Set-Cookie: ___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900\r\n"
b"\r\n",
Request(
method="HEAD",
target="/foo",
headers=[
("Host", "foo"),
("Set-Cookie", "___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900"),
],
),
)
def test_host_comes_first() -> None:
tw(
write_headers,
normalize_and_validate([("foo", "bar"), ("Host", "example.com")]),
b"Host: example.com\r\nfoo: bar\r\n\r\n",
)

@ -0,0 +1,135 @@
import re
from typing import Tuple
import pytest
from .._receivebuffer import ReceiveBuffer
def test_receivebuffer() -> None:
b = ReceiveBuffer()
assert not b
assert len(b) == 0
assert bytes(b) == b""
b += b"123"
assert b
assert len(b) == 3
assert bytes(b) == b"123"
assert bytes(b) == b"123"
assert b.maybe_extract_at_most(2) == b"12"
assert b
assert len(b) == 1
assert bytes(b) == b"3"
assert bytes(b) == b"3"
assert b.maybe_extract_at_most(10) == b"3"
assert bytes(b) == b""
assert b.maybe_extract_at_most(10) is None
assert not b
################################################################
# maybe_extract_until_next
################################################################
b += b"123\n456\r\n789\r\n"
assert b.maybe_extract_next_line() == b"123\n456\r\n"
assert bytes(b) == b"789\r\n"
assert b.maybe_extract_next_line() == b"789\r\n"
assert bytes(b) == b""
b += b"12\r"
assert b.maybe_extract_next_line() is None
assert bytes(b) == b"12\r"
b += b"345\n\r"
assert b.maybe_extract_next_line() is None
assert bytes(b) == b"12\r345\n\r"
# here we stopped at the middle of b"\r\n" delimiter
b += b"\n6789aaa123\r\n"
assert b.maybe_extract_next_line() == b"12\r345\n\r\n"
assert b.maybe_extract_next_line() == b"6789aaa123\r\n"
assert b.maybe_extract_next_line() is None
assert bytes(b) == b""
################################################################
# maybe_extract_lines
################################################################
b += b"123\r\na: b\r\nfoo:bar\r\n\r\ntrailing"
lines = b.maybe_extract_lines()
assert lines == [b"123", b"a: b", b"foo:bar"]
assert bytes(b) == b"trailing"
assert b.maybe_extract_lines() is None
b += b"\r\n\r"
assert b.maybe_extract_lines() is None
assert b.maybe_extract_at_most(100) == b"trailing\r\n\r"
assert not b
# Empty body case (as happens at the end of chunked encoding if there are
# no trailing headers, e.g.)
b += b"\r\ntrailing"
assert b.maybe_extract_lines() == []
assert bytes(b) == b"trailing"
@pytest.mark.parametrize(
"data",
[
pytest.param(
(
b"HTTP/1.1 200 OK\r\n",
b"Content-type: text/plain\r\n",
b"Connection: close\r\n",
b"\r\n",
b"Some body",
),
id="with_crlf_delimiter",
),
pytest.param(
(
b"HTTP/1.1 200 OK\n",
b"Content-type: text/plain\n",
b"Connection: close\n",
b"\n",
b"Some body",
),
id="with_lf_only_delimiter",
),
pytest.param(
(
b"HTTP/1.1 200 OK\n",
b"Content-type: text/plain\r\n",
b"Connection: close\n",
b"\n",
b"Some body",
),
id="with_mixed_crlf_and_lf",
),
],
)
def test_receivebuffer_for_invalid_delimiter(data: Tuple[bytes]) -> None:
b = ReceiveBuffer()
for line in data:
b += line
lines = b.maybe_extract_lines()
assert lines == [
b"HTTP/1.1 200 OK",
b"Content-type: text/plain",
b"Connection: close",
]
assert bytes(b) == b"Some body"

@ -0,0 +1,271 @@
import pytest
from .._events import (
ConnectionClosed,
Data,
EndOfMessage,
Event,
InformationalResponse,
Request,
Response,
)
from .._state import (
_SWITCH_CONNECT,
_SWITCH_UPGRADE,
CLIENT,
CLOSED,
ConnectionState,
DONE,
IDLE,
MIGHT_SWITCH_PROTOCOL,
MUST_CLOSE,
SEND_BODY,
SEND_RESPONSE,
SERVER,
SWITCHED_PROTOCOL,
)
from .._util import LocalProtocolError
def test_ConnectionState() -> None:
cs = ConnectionState()
# Basic event-triggered transitions
assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
cs.process_event(CLIENT, Request)
# The SERVER-Request special case:
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
# Illegal transitions raise an error and nothing happens
with pytest.raises(LocalProtocolError):
cs.process_event(CLIENT, Request)
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
cs.process_event(SERVER, InformationalResponse)
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
cs.process_event(SERVER, Response)
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_BODY}
cs.process_event(CLIENT, EndOfMessage)
cs.process_event(SERVER, EndOfMessage)
assert cs.states == {CLIENT: DONE, SERVER: DONE}
# State-triggered transition
cs.process_event(SERVER, ConnectionClosed)
assert cs.states == {CLIENT: MUST_CLOSE, SERVER: CLOSED}
def test_ConnectionState_keep_alive() -> None:
# keep_alive = False
cs = ConnectionState()
cs.process_event(CLIENT, Request)
cs.process_keep_alive_disabled()
cs.process_event(CLIENT, EndOfMessage)
assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_RESPONSE}
cs.process_event(SERVER, Response)
cs.process_event(SERVER, EndOfMessage)
assert cs.states == {CLIENT: MUST_CLOSE, SERVER: MUST_CLOSE}
def test_ConnectionState_keep_alive_in_DONE() -> None:
# Check that if keep_alive is disabled when the CLIENT is already in DONE,
# then this is sufficient to immediately trigger the DONE -> MUST_CLOSE
# transition
cs = ConnectionState()
cs.process_event(CLIENT, Request)
cs.process_event(CLIENT, EndOfMessage)
assert cs.states[CLIENT] is DONE
cs.process_keep_alive_disabled()
assert cs.states[CLIENT] is MUST_CLOSE
def test_ConnectionState_switch_denied() -> None:
for switch_type in (_SWITCH_CONNECT, _SWITCH_UPGRADE):
for deny_early in (True, False):
cs = ConnectionState()
cs.process_client_switch_proposal(switch_type)
cs.process_event(CLIENT, Request)
cs.process_event(CLIENT, Data)
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
assert switch_type in cs.pending_switch_proposals
if deny_early:
# before client reaches DONE
cs.process_event(SERVER, Response)
assert not cs.pending_switch_proposals
cs.process_event(CLIENT, EndOfMessage)
if deny_early:
assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
else:
assert cs.states == {
CLIENT: MIGHT_SWITCH_PROTOCOL,
SERVER: SEND_RESPONSE,
}
cs.process_event(SERVER, InformationalResponse)
assert cs.states == {
CLIENT: MIGHT_SWITCH_PROTOCOL,
SERVER: SEND_RESPONSE,
}
cs.process_event(SERVER, Response)
assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
assert not cs.pending_switch_proposals
_response_type_for_switch = {
_SWITCH_UPGRADE: InformationalResponse,
_SWITCH_CONNECT: Response,
None: Response,
}
def test_ConnectionState_protocol_switch_accepted() -> None:
for switch_event in [_SWITCH_UPGRADE, _SWITCH_CONNECT]:
cs = ConnectionState()
cs.process_client_switch_proposal(switch_event)
cs.process_event(CLIENT, Request)
cs.process_event(CLIENT, Data)
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
cs.process_event(CLIENT, EndOfMessage)
assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
cs.process_event(SERVER, InformationalResponse)
assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
cs.process_event(SERVER, _response_type_for_switch[switch_event], switch_event)
assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL}
def test_ConnectionState_double_protocol_switch() -> None:
# CONNECT + Upgrade is legal! Very silly, but legal. So we support
# it. Because sometimes doing the silly thing is easier than not.
for server_switch in [None, _SWITCH_UPGRADE, _SWITCH_CONNECT]:
cs = ConnectionState()
cs.process_client_switch_proposal(_SWITCH_UPGRADE)
cs.process_client_switch_proposal(_SWITCH_CONNECT)
cs.process_event(CLIENT, Request)
cs.process_event(CLIENT, EndOfMessage)
assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
cs.process_event(
SERVER, _response_type_for_switch[server_switch], server_switch
)
if server_switch is None:
assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
else:
assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL}
def test_ConnectionState_inconsistent_protocol_switch() -> None:
for client_switches, server_switch in [
([], _SWITCH_CONNECT),
([], _SWITCH_UPGRADE),
([_SWITCH_UPGRADE], _SWITCH_CONNECT),
([_SWITCH_CONNECT], _SWITCH_UPGRADE),
]:
cs = ConnectionState()
for client_switch in client_switches: # type: ignore[attr-defined]
cs.process_client_switch_proposal(client_switch)
cs.process_event(CLIENT, Request)
with pytest.raises(LocalProtocolError):
cs.process_event(SERVER, Response, server_switch)
def test_ConnectionState_keepalive_protocol_switch_interaction() -> None:
# keep_alive=False + pending_switch_proposals
cs = ConnectionState()
cs.process_client_switch_proposal(_SWITCH_UPGRADE)
cs.process_event(CLIENT, Request)
cs.process_keep_alive_disabled()
cs.process_event(CLIENT, Data)
assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
# the protocol switch "wins"
cs.process_event(CLIENT, EndOfMessage)
assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
# but when the server denies the request, keep_alive comes back into play
cs.process_event(SERVER, Response)
assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_BODY}
def test_ConnectionState_reuse() -> None:
cs = ConnectionState()
with pytest.raises(LocalProtocolError):
cs.start_next_cycle()
cs.process_event(CLIENT, Request)
cs.process_event(CLIENT, EndOfMessage)
with pytest.raises(LocalProtocolError):
cs.start_next_cycle()
cs.process_event(SERVER, Response)
cs.process_event(SERVER, EndOfMessage)
cs.start_next_cycle()
assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
# No keepalive
cs.process_event(CLIENT, Request)
cs.process_keep_alive_disabled()
cs.process_event(CLIENT, EndOfMessage)
cs.process_event(SERVER, Response)
cs.process_event(SERVER, EndOfMessage)
with pytest.raises(LocalProtocolError):
cs.start_next_cycle()
# One side closed
cs = ConnectionState()
cs.process_event(CLIENT, Request)
cs.process_event(CLIENT, EndOfMessage)
cs.process_event(CLIENT, ConnectionClosed)
cs.process_event(SERVER, Response)
cs.process_event(SERVER, EndOfMessage)
with pytest.raises(LocalProtocolError):
cs.start_next_cycle()
# Succesful protocol switch
cs = ConnectionState()
cs.process_client_switch_proposal(_SWITCH_UPGRADE)
cs.process_event(CLIENT, Request)
cs.process_event(CLIENT, EndOfMessage)
cs.process_event(SERVER, InformationalResponse, _SWITCH_UPGRADE)
with pytest.raises(LocalProtocolError):
cs.start_next_cycle()
# Failed protocol switch
cs = ConnectionState()
cs.process_client_switch_proposal(_SWITCH_UPGRADE)
cs.process_event(CLIENT, Request)
cs.process_event(CLIENT, EndOfMessage)
cs.process_event(SERVER, Response)
cs.process_event(SERVER, EndOfMessage)
cs.start_next_cycle()
assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
def test_server_request_is_illegal() -> None:
# There used to be a bug in how we handled the Request special case that
# made this allowed...
cs = ConnectionState()
with pytest.raises(LocalProtocolError):
cs.process_event(SERVER, Request)

@ -0,0 +1,112 @@
import re
import sys
import traceback
from typing import NoReturn
import pytest
from .._util import (
bytesify,
LocalProtocolError,
ProtocolError,
RemoteProtocolError,
Sentinel,
validate,
)
def test_ProtocolError() -> None:
with pytest.raises(TypeError):
ProtocolError("abstract base class")
def test_LocalProtocolError() -> None:
try:
raise LocalProtocolError("foo")
except LocalProtocolError as e:
assert str(e) == "foo"
assert e.error_status_hint == 400
try:
raise LocalProtocolError("foo", error_status_hint=418)
except LocalProtocolError as e:
assert str(e) == "foo"
assert e.error_status_hint == 418
def thunk() -> NoReturn:
raise LocalProtocolError("a", error_status_hint=420)
try:
try:
thunk()
except LocalProtocolError as exc1:
orig_traceback = "".join(traceback.format_tb(sys.exc_info()[2]))
exc1._reraise_as_remote_protocol_error()
except RemoteProtocolError as exc2:
assert type(exc2) is RemoteProtocolError
assert exc2.args == ("a",)
assert exc2.error_status_hint == 420
new_traceback = "".join(traceback.format_tb(sys.exc_info()[2]))
assert new_traceback.endswith(orig_traceback)
def test_validate() -> None:
my_re = re.compile(br"(?P<group1>[0-9]+)\.(?P<group2>[0-9]+)")
with pytest.raises(LocalProtocolError):
validate(my_re, b"0.")
groups = validate(my_re, b"0.1")
assert groups == {"group1": b"0", "group2": b"1"}
# successful partial matches are an error - must match whole string
with pytest.raises(LocalProtocolError):
validate(my_re, b"0.1xx")
with pytest.raises(LocalProtocolError):
validate(my_re, b"0.1\n")
def test_validate_formatting() -> None:
my_re = re.compile(br"foo")
with pytest.raises(LocalProtocolError) as excinfo:
validate(my_re, b"", "oops")
assert "oops" in str(excinfo.value)
with pytest.raises(LocalProtocolError) as excinfo:
validate(my_re, b"", "oops {}")
assert "oops {}" in str(excinfo.value)
with pytest.raises(LocalProtocolError) as excinfo:
validate(my_re, b"", "oops {} xx", 10)
assert "oops 10 xx" in str(excinfo.value)
def test_make_sentinel() -> None:
class S(Sentinel, metaclass=Sentinel):
pass
assert repr(S) == "S"
assert S == S
assert type(S).__name__ == "S"
assert S in {S}
assert type(S) is S
class S2(Sentinel, metaclass=Sentinel):
pass
assert repr(S2) == "S2"
assert S != S2
assert S not in {S2}
assert type(S) is not type(S2)
def test_bytesify() -> None:
assert bytesify(b"123") == b"123"
assert bytesify(bytearray(b"123")) == b"123"
assert bytesify("123") == b"123"
with pytest.raises(UnicodeEncodeError):
bytesify("\u1234")
with pytest.raises(TypeError):
bytesify(10)

@ -0,0 +1,3 @@
This software is made available under the terms of *either* of the
licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to are
made under the terms of *both* these licenses.

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,20 @@
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,61 @@
Metadata-Version: 2.1
Name: outcome
Version: 1.2.0
Summary: Capture the outcome of Python function calls.
Home-page: https://github.com/python-trio/outcome
Author: Frazer McLean
Author-email: frazer@frazermclean.co.uk
License: MIT OR Apache-2.0
Project-URL: Documentation, https://outcome.readthedocs.io/en/latest/
Project-URL: Chat, https://gitter.im/python-trio/general
Keywords: result
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Trio
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.7
Description-Content-Type: text/x-rst
Requires-Dist: attrs (>=19.2.0)
.. image:: https://img.shields.io/badge/chat-join%20now-blue.svg
:target: https://gitter.im/python-trio/general
:alt: Join chatroom
.. image:: https://img.shields.io/badge/docs-read%20now-blue.svg
:target: https://outcome.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://travis-ci.org/python-trio/trio.svg?branch=master
:target: https://travis-ci.org/python-trio/outcome
:alt: Automated test status (Linux and MacOS)
.. image:: https://ci.appveyor.com/api/projects/status/c54uu4rxlgs2usmj/branch/master?svg=true
:target: https://ci.appveyor.com/project/RazerM/outcome/history
:alt: Automated test status (Windows)
.. image:: https://codecov.io/gh/python-trio/trio/branch/master/graph/badge.svg
:target: https://codecov.io/gh/python-trio/outcome
:alt: Test coverage
outcome
=======
Welcome to `outcome <https://github.com/python-trio/outcome>`__!
Capture the outcome of Python function calls. Extracted from the
`Trio <https://github.com/python-trio/trio>`__ project.
License: Your choice of MIT or Apache License 2.0

@ -0,0 +1,16 @@
outcome-1.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
outcome-1.2.0.dist-info/LICENSE,sha256=ZSyHhIjRRWNh4Iw_hgf9e6WYkqFBA9Fczk_5PIW1zIs,185
outcome-1.2.0.dist-info/LICENSE.APACHE2,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
outcome-1.2.0.dist-info/LICENSE.MIT,sha256=Pm2uVV65J4f8gtHUg1Vnf0VMf2Wus40_nnK_mj2vA0s,1046
outcome-1.2.0.dist-info/METADATA,sha256=WXghBvotvGpqqYeW3QDS0sxYwhMiBlZhpXBMIl9ejPc,2328
outcome-1.2.0.dist-info/RECORD,,
outcome-1.2.0.dist-info/WHEEL,sha256=HX-v9-noUkyUoxyZ1PMSuS7auUxDAR4VBdoYLqD0xws,110
outcome-1.2.0.dist-info/top_level.txt,sha256=gyOUMosVXcIYHnlvxHt2jeNDnEIt62nwCk1PKatrcNg,8
outcome/__init__.py,sha256=KvpS6PDeFBkv8x-JOzRcpRs_YIlpuD8qjkiDzB62X0Y,351
outcome/__pycache__/__init__.cpython-310.pyc,,
outcome/__pycache__/_impl.cpython-310.pyc,,
outcome/__pycache__/_util.cpython-310.pyc,,
outcome/__pycache__/_version.cpython-310.pyc,,
outcome/_impl.py,sha256=0LXAi1K8M8HDxIMyRpc1PD1hqFUXdR_0rwp64t6MGL0,4705
outcome/_util.py,sha256=DBavKx74818Za3QhmHTkIs64iLjHJ8PH4HHYwujr7DI,689
outcome/_version.py,sha256=codhqWKYFbR_gKABvhb2_aWOKwuRo4YQlLcENJOvFkQ,89

@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.33.1)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

@ -0,0 +1,12 @@
"""Top-level package for outcome."""
from ._impl import Error, Outcome, Value, acapture, capture
from ._util import AlreadyUsedError, fixup_module_metadata
from ._version import __version__
__all__ = (
'Error', 'Outcome', 'Value', 'acapture', 'capture', 'AlreadyUsedError'
)
fixup_module_metadata(__name__, globals())
del fixup_module_metadata

@ -0,0 +1,160 @@
import abc
import attr
from ._util import AlreadyUsedError, remove_tb_frames
__all__ = ['Error', 'Outcome', 'Value', 'acapture', 'capture']
def capture(sync_fn, *args, **kwargs):
"""Run ``sync_fn(*args, **kwargs)`` and capture the result.
Returns:
Either a :class:`Value` or :class:`Error` as appropriate.
"""
try:
return Value(sync_fn(*args, **kwargs))
except BaseException as exc:
exc = remove_tb_frames(exc, 1)
return Error(exc)
async def acapture(async_fn, *args, **kwargs):
"""Run ``await async_fn(*args, **kwargs)`` and capture the result.
Returns:
Either a :class:`Value` or :class:`Error` as appropriate.
"""
try:
return Value(await async_fn(*args, **kwargs))
except BaseException as exc:
exc = remove_tb_frames(exc, 1)
return Error(exc)
@attr.s(repr=False, init=False, slots=True)
class Outcome(abc.ABC):
"""An abstract class representing the result of a Python computation.
This class has two concrete subclasses: :class:`Value` representing a
value, and :class:`Error` representing an exception.
In addition to the methods described below, comparison operators on
:class:`Value` and :class:`Error` objects (``==``, ``<``, etc.) check that
the other object is also a :class:`Value` or :class:`Error` object
respectively, and then compare the contained objects.
:class:`Outcome` objects are hashable if the contained objects are
hashable.
"""
_unwrapped = attr.ib(default=False, eq=False, init=False)
def _set_unwrapped(self):
if self._unwrapped:
raise AlreadyUsedError
object.__setattr__(self, '_unwrapped', True)
@abc.abstractmethod
def unwrap(self):
"""Return or raise the contained value or exception.
These two lines of code are equivalent::
x = fn(*args)
x = outcome.capture(fn, *args).unwrap()
"""
@abc.abstractmethod
def send(self, gen):
"""Send or throw the contained value or exception into the given
generator object.
Args:
gen: A generator object supporting ``.send()`` and ``.throw()``
methods.
"""
@abc.abstractmethod
async def asend(self, agen):
"""Send or throw the contained value or exception into the given async
generator object.
Args:
agen: An async generator object supporting ``.asend()`` and
``.athrow()`` methods.
"""
@attr.s(frozen=True, repr=False, slots=True)
class Value(Outcome):
"""Concrete :class:`Outcome` subclass representing a regular value.
"""
value = attr.ib()
"""The contained value."""
def __repr__(self):
return f'Value({self.value!r})'
def unwrap(self):
self._set_unwrapped()
return self.value
def send(self, gen):
self._set_unwrapped()
return gen.send(self.value)
async def asend(self, agen):
self._set_unwrapped()
return await agen.asend(self.value)
@attr.s(frozen=True, repr=False, slots=True)
class Error(Outcome):
"""Concrete :class:`Outcome` subclass representing a raised exception.
"""
error = attr.ib(validator=attr.validators.instance_of(BaseException))
"""The contained exception object."""
def __repr__(self):
return f'Error({self.error!r})'
def unwrap(self):
self._set_unwrapped()
# Tracebacks show the 'raise' line below out of context, so let's give
# this variable a name that makes sense out of context.
captured_error = self.error
try:
raise captured_error
finally:
# We want to avoid creating a reference cycle here. Python does
# collect cycles just fine, so it wouldn't be the end of the world
# if we did create a cycle, but the cyclic garbage collector adds
# latency to Python programs, and the more cycles you create, the
# more often it runs, so it's nicer to avoid creating them in the
# first place. For more details see:
#
# https://github.com/python-trio/trio/issues/1770
#
# In particuar, by deleting this local variables from the 'unwrap'
# methods frame, we avoid the 'captured_error' object's
# __traceback__ from indirectly referencing 'captured_error'.
del captured_error, self
def send(self, it):
self._set_unwrapped()
return it.throw(self.error)
async def asend(self, agen):
self._set_unwrapped()
return await agen.athrow(self.error)

@ -0,0 +1,24 @@
class AlreadyUsedError(RuntimeError):
"""An Outcome can only be unwrapped once."""
pass
def fixup_module_metadata(module_name, namespace):
def fix_one(obj):
mod = getattr(obj, "__module__", None)
if mod is not None and mod.startswith("outcome."):
obj.__module__ = module_name
if isinstance(obj, type):
for attr_value in obj.__dict__.values():
fix_one(attr_value)
for objname in namespace["__all__"]:
obj = namespace[objname]
fix_one(obj)
def remove_tb_frames(exc, n):
tb = exc.__traceback__
for _ in range(n):
tb = tb.tb_next
return exc.with_traceback(tb)

@ -0,0 +1,3 @@
# This file is imported from __init__.py and exec'd from setup.py
__version__ = "1.2.0"

@ -0,0 +1,170 @@
Metadata-Version: 2.1
Name: selenium
Version: 4.4.3
Home-page: https://www.selenium.dev
License: Apache 2.0
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Software Development :: Libraries
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Requires-Python: ~=3.7
Requires-Dist: urllib3[socks]~=1.26
Requires-Dist: trio~=0.17
Requires-Dist: trio-websocket~=0.9
Requires-Dist: certifi>=2021.10.8
======================
Selenium Client Driver
======================
Introduction
============
Python language bindings for Selenium WebDriver.
The `selenium` package is used to automate web browser interaction from Python.
+-----------+--------------------------------------------------------------------------------------+
| **Home**: | https://selenium.dev |
+-----------+--------------------------------------------------------------------------------------+
| **Docs**: | `selenium package API <https://seleniumhq.github.io/selenium/docs/api/py/api.html>`_ |
+-----------+--------------------------------------------------------------------------------------+
| **Dev**: | https://github.com/SeleniumHQ/Selenium |
+-----------+--------------------------------------------------------------------------------------+
| **PyPI**: | https://pypi.org/project/selenium/ |
+-----------+--------------------------------------------------------------------------------------+
| **IRC**: | **#selenium** channel on LiberaChat |
+-----------+--------------------------------------------------------------------------------------+
Several browsers/drivers are supported (Firefox, Chrome, Internet Explorer), as well as the Remote protocol.
Supported Python Versions
=========================
* Python 3.7+
Installing
==========
If you have `pip <https://pip.pypa.io/>`_ on your system, you can simply install or upgrade the Python bindings::
pip install -U selenium
Alternately, you can download the source distribution from `PyPI <https://pypi.org/project/selenium/#files>`_ (e.g. selenium-4.4.0.tar.gz), unarchive it, and run::
python setup.py install
Note: You may want to consider using `virtualenv <http://www.virtualenv.org/>`_ to create isolated Python environments.
Drivers
=======
Selenium requires a driver to interface with the chosen browser. Firefox,
for example, requires `geckodriver <https://github.com/mozilla/geckodriver/releases>`_, which needs to be installed before the below examples can be run. Make sure it's in your `PATH`, e. g., place it in `/usr/bin` or `/usr/local/bin`.
Failure to observe this step will give you an error `selenium.common.exceptions.WebDriverException: Message: 'geckodriver' executable needs to be in PATH.`
Other supported browsers will have their own drivers available. Links to some of the more popular browser drivers follow.
+--------------+-----------------------------------------------------------------------+
| **Chrome**: | https://chromedriver.chromium.org/downloads |
+--------------+-----------------------------------------------------------------------+
| **Edge**: | https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ |
+--------------+-----------------------------------------------------------------------+
| **Firefox**: | https://github.com/mozilla/geckodriver/releases |
+--------------+-----------------------------------------------------------------------+
| **Safari**: | https://webkit.org/blog/6900/webdriver-support-in-safari-10/ |
+--------------+-----------------------------------------------------------------------+
Example 0:
==========
* open a new Firefox browser
* load the page at the given URL
.. code-block:: python
from selenium import webdriver
browser = webdriver.Firefox()
browser.get('http://selenium.dev/')
Example 1:
==========
* open a new Firefox browser
* load the Yahoo homepage
* search for "seleniumhq"
* close the browser
.. code-block:: python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
browser = webdriver.Firefox()
browser.get('http://www.yahoo.com')
assert 'Yahoo' in browser.title
elem = browser.find_element(By.NAME, 'p') # Find the search box
elem.send_keys('seleniumhq' + Keys.RETURN)
browser.quit()
Example 2:
==========
Selenium WebDriver is often used as a basis for testing web applications. Here is a simple example using Python's standard `unittest <http://docs.python.org/3/library/unittest.html>`_ library:
.. code-block:: python
import unittest
from selenium import webdriver
class GoogleTestCase(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.addCleanup(self.browser.quit)
def test_page_title(self):
self.browser.get('http://www.google.com')
self.assertIn('Google', self.browser.title)
if __name__ == '__main__':
unittest.main(verbosity=2)
Selenium Server (optional)
==========================
For normal WebDriver scripts (non-Remote), the Java server is not needed.
However, to use Selenium Webdriver Remote or the legacy Selenium API (Selenium-RC), you need to also run the Selenium server. The server requires a Java Runtime Environment (JRE).
Download the server separately, from: https://www.selenium.dev/downloads/
Run the server from the command line::
java -jar selenium-server-4.4.0.jar
Then run your Python client scripts.
Use The Source Luke!
====================
View source code online:
+-----------+------------------------------------------------------+
| official: | https://github.com/SeleniumHQ/selenium/tree/trunk/py |
+-----------+------------------------------------------------------+

@ -0,0 +1,593 @@
selenium-4.4.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
selenium-4.4.3.dist-info/METADATA,sha256=mzBSI1y4B3bx1qj_MdV8vPZ1ibxb5R8rwJUJxt3xdhU,6492
selenium-4.4.3.dist-info/RECORD,,
selenium-4.4.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
selenium-4.4.3.dist-info/WHEEL,sha256=sobxWSyDDkdg_rinUth-jxhXHqoNqlmNMJY3aTZn2Us,91
selenium/__init__.py,sha256=8M8-hYeYtvWoEk_tBAQKI_81W9z8SpYUPzxPOI7M0yQ,811
selenium/__pycache__/__init__.cpython-310.pyc,,
selenium/__pycache__/types.cpython-310.pyc,,
selenium/common/__init__.py,sha256=rMFs1jA8osI6lOInhLSPJN8mFITZ6odszfSXYVZKZ6M,3770
selenium/common/__pycache__/__init__.cpython-310.pyc,,
selenium/common/__pycache__/exceptions.cpython-310.pyc,,
selenium/common/exceptions.py,sha256=1NQNvtC1806G8lNA1hd5pSNckO1ABaBeSHMl2a2DhDo,9225
selenium/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
selenium/types.py,sha256=HW9deIkVeCbWUh2Nnyy2iZF9_D4admi3uCnbBwtGutI,932
selenium/webdriver/__init__.py,sha256=I8OSHq6FbbBGeVLXYyEaqxSaKmVUfXzR73uTAgKUnHs,2428
selenium/webdriver/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/chrome/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/chrome/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/chrome/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/chrome/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/chrome/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/chrome/options.py,sha256=2I-RKskaWPdRdh-fwK8Pc23EwMVlyvkfzJHJWZ054Ss,1430
selenium/webdriver/chrome/service.py,sha256=kyMJsnnMGLbyinPBJMHglchp0n19Vm01PdVy-rCr3Rg,1750
selenium/webdriver/chrome/webdriver.py,sha256=yGKRf7A6it65t1n-Qn71B2m3O4KgtkXtBGCtW1-0nP8,3655
selenium/webdriver/chromium/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/chromium/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/chromium/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/chromium/__pycache__/remote_connection.cpython-310.pyc,,
selenium/webdriver/chromium/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/chromium/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/chromium/options.py,sha256=1txeAQNXuXB9DvZn23-HgtrcvDFTkAEz8PlfWJPVhho,6769
selenium/webdriver/chromium/remote_connection.py,sha256=7HR_VWVFiQd8muuUPW0kHUoxP0E-5z0PBeaAkuyhzSo,2591
selenium/webdriver/chromium/service.py,sha256=1fri0BZlwmSP5dH8RIF-0QvD_PK-EqgorFkf-Nnwsr8,1977
selenium/webdriver/chromium/webdriver.py,sha256=SBQB_4jWZcoOkkbQBaUgYpzAHIwTRmsa8CxqQxeazmg,9393
selenium/webdriver/common/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/common/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/action_chains.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/alert.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/by.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/desired_capabilities.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/keys.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/log.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/print_page_options.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/proxy.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/timeouts.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/utils.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/virtual_authenticator.cpython-310.pyc,,
selenium/webdriver/common/__pycache__/window.cpython-310.pyc,,
selenium/webdriver/common/action_chains.py,sha256=-wV6GhFrdya5B8mFNhdNPWN7dy3sTWGXevOovMTS5l0,13338
selenium/webdriver/common/actions/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/common/actions/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/action_builder.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/input_device.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/interaction.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/key_actions.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/key_input.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/mouse_button.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/pointer_actions.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/pointer_input.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/wheel_actions.cpython-310.pyc,,
selenium/webdriver/common/actions/__pycache__/wheel_input.cpython-310.pyc,,
selenium/webdriver/common/actions/action_builder.py,sha256=uifc2dblDrys1xb6tCsrc0fKzQMxoLSenLj87pRs-wI,3493
selenium/webdriver/common/actions/input_device.py,sha256=AxG-1shD8VygMj4RogJpRflfZ72tqCGSOa09zUiRfvE,1268
selenium/webdriver/common/actions/interaction.py,sha256=2qPGW5QDNQgmfQLMTJ4DWp2bDh02orx2nQ1jMk7I2zQ,1432
selenium/webdriver/common/actions/key_actions.py,sha256=hor8gxQF9plf7PLRAJc3NcNuypEwFyFdwlND1dtyHa4,1713
selenium/webdriver/common/actions/key_input.py,sha256=QPGoZVWDSCaFbTVPG702D_c7zsxEbdt7I3iiBmbXSOY,1801
selenium/webdriver/common/actions/mouse_button.py,sha256=NG6DaAf86EZH6_RsnFq5l-6FU0QFkN_G8kdYmw1oA4o,880
selenium/webdriver/common/actions/pointer_actions.py,sha256=NXOcN13bx4k-BNvw4DxrUmTAqzoYbW6K9iAFaLuIjPA,5505
selenium/webdriver/common/actions/pointer_input.py,sha256=RDd4PeONJTuCUiIpFJTSmq14MKTRnQGOOeYJtjribcs,2951
selenium/webdriver/common/actions/wheel_actions.py,sha256=MsyTSRptR5OjIZPT1fXi7UaM0JctBE9UQpOoZS1mUXE,1323
selenium/webdriver/common/actions/wheel_input.py,sha256=RceuhSmYnY6CrUM4LzFGED_QxXydnp1soTFkb6f0yDA,2610
selenium/webdriver/common/alert.py,sha256=KI_Uuyc2vb74bx8m7vg2ZWlZvCpG2XNeYWD2AlMoDqM,2583
selenium/webdriver/common/bidi/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/common/bidi/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/common/bidi/__pycache__/cdp.cpython-310.pyc,,
selenium/webdriver/common/bidi/__pycache__/console.cpython-310.pyc,,
selenium/webdriver/common/bidi/cdp.py,sha256=-jwilUynZTEQwjnsyFT4w7mAvpGwiHKbVwhBRrOIK_4,18318
selenium/webdriver/common/bidi/console.py,sha256=0yPaIH4MC0tb87Ab1NwEuMn1_RKb-dvkT-AqLbTYRZM,886
selenium/webdriver/common/by.py,sha256=4h4wg0c9BR29CNDKcQsO9RSVCuH_1ERRZ1oCXjSysG0,1103
selenium/webdriver/common/desired_capabilities.py,sha256=oZp8rrcfkn9zP_R7SWbVUo60fe0vHNe7gF-SAnKHTC0,2933
selenium/webdriver/common/devtools/v102/__init__.py,sha256=odgx0kjP8gxKQiplaFASHw9Ly1Nt-M6dlIJr0KPtzAE,1293
selenium/webdriver/common/devtools/v102/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/accessibility.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/animation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/audits.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/background_service.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/browser.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/cache_storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/cast.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/console.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/css.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/database.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/debugger.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/device_orientation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/dom.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/dom_debugger.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/dom_snapshot.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/dom_storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/emulation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/event_breakpoints.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/fetch.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/headless_experimental.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/heap_profiler.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/indexed_db.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/input_.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/inspector.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/io.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/layer_tree.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/log.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/media.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/memory.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/network.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/overlay.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/page.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/performance.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/performance_timeline.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/profiler.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/runtime.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/schema.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/security.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/service_worker.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/system_info.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/target.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/tethering.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/tracing.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/util.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/web_audio.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/__pycache__/web_authn.cpython-310.pyc,,
selenium/webdriver/common/devtools/v102/accessibility.py,sha256=JOBaqnUeqPgG0nm5BYdNVmQtKbFRCsPz4zzOoLiHcQc,21911
selenium/webdriver/common/devtools/v102/animation.py,sha256=uyeJ8qZu7U6RfPgHaS0d4loPBnyz0lbH9RoTwn8oBSw,11112
selenium/webdriver/common/devtools/v102/audits.py,sha256=CGlGCy3hFRMZo0rDbBRRYHGLZaJ9l0yFOMUxjlXtqCI,45099
selenium/webdriver/common/devtools/v102/background_service.py,sha256=IrsZjfMaZLbAsSq6yn0vlIpyKIykuJGI_RSgmv4SPsM,5760
selenium/webdriver/common/devtools/v102/browser.py,sha256=9jDo4SupO17Up33-FMX3SF88LODzz3zGRxulN5mjS2E,20683
selenium/webdriver/common/devtools/v102/cache_storage.py,sha256=XpnSE40sg1LNifK2kYe51F9ZV3jW8vzE46hbzVi1v8Y,7810
selenium/webdriver/common/devtools/v102/cast.py,sha256=32wNaJwk_0S5X-UdjazzLauMyXx6JPkyJHUdsxm8scs,4382
selenium/webdriver/common/devtools/v102/console.py,sha256=_mGKNlgtple6r9rJ_GxEfhkoQsMT7D1V6IeF_lAOBJ4,2765
selenium/webdriver/common/devtools/v102/css.py,sha256=cEq_4pCMnB7-Siv8dk2LedpXRSMNEk8NJkPZXKpJAnE,55616
selenium/webdriver/common/devtools/v102/database.py,sha256=ucK8g37lBtFgYWgzM5go6YJxQcQ2sTqPy9T2wymGxrA,3925
selenium/webdriver/common/devtools/v102/debugger.py,sha256=co3VP-G7aM4hb6_mVB6Yvjalf3uHZkBEgfP8rAfgiac,43952
selenium/webdriver/common/devtools/v102/device_orientation.py,sha256=iTyh5DWm7zRYzmq6VBe3_XakjFCC8kuuXc_NVIeNb2k,1209
selenium/webdriver/common/devtools/v102/dom.py,sha256=I2ETt27WCevNzDkPkX32FabdX1xQSaYS94_HioVk7Dc,59373
selenium/webdriver/common/devtools/v102/dom_debugger.py,sha256=HgKs0cpnWhtViWsPQt6tBBsHsdkLQbh2hizyFtyqRFc,9459
selenium/webdriver/common/devtools/v102/dom_snapshot.py,sha256=KdPjwnF0lGudJz3oUtJF_y4prGJJTdOs3L9ps3zfyow,36328
selenium/webdriver/common/devtools/v102/dom_storage.py,sha256=35ho_LUl6SiRfVL6HckRpswt0vb7KGmij7A_hiTQy8k,5026
selenium/webdriver/common/devtools/v102/emulation.py,sha256=zDNal5RXrrNg4iUdiDfiFDgTpVH99YtPhVp8IkxKo6s,24981
selenium/webdriver/common/devtools/v102/event_breakpoints.py,sha256=ogM8_6wu8gz52-4f394gydOSXyXIiN-QDGluLUZ9ro4,1289
selenium/webdriver/common/devtools/v102/fetch.py,sha256=nO6DG5xqVTRbg5-U92ULuctTh3bEuH-afm9TluWkt-w,18611
selenium/webdriver/common/devtools/v102/headless_experimental.py,sha256=FfGXBeUm7sewWPeLtJ9x0bLJrXc1aUV_vE8dguO1iYY,4791
selenium/webdriver/common/devtools/v102/heap_profiler.py,sha256=18nUpu0Jq6lMZwAmdi-LOxhsAYixZmRgIqZ7EWsEN-g,11742
selenium/webdriver/common/devtools/v102/indexed_db.py,sha256=iA91BkxRAmGC15Oyc54gy690E79f7REqysCSgRxKqdE,12762
selenium/webdriver/common/devtools/v102/input_.py,sha256=ZZpGjItj5BhYWWz966XSvKIRshO23P1qeZaVBEEDwug,27841
selenium/webdriver/common/devtools/v102/inspector.py,sha256=oroH9p3tWRYh5poAM48tUmAU4_V8mJBXCGGjQt9ATOY,1718
selenium/webdriver/common/devtools/v102/io.py,sha256=IWzLKRqziXr-qZ8_TJhgWbi-Z-Yo3mfAGDTjgYwMR50,3036
selenium/webdriver/common/devtools/v102/layer_tree.py,sha256=EZzeGIvsAMOzqY9BTzx2FWMKcML5MiMvu_wALaG5PiQ,15049
selenium/webdriver/common/devtools/v102/log.py,sha256=kBm2ACgcTbC5T6nGROJnMfofA9zjLtO9K2hUwFywqS0,5264
selenium/webdriver/common/devtools/v102/media.py,sha256=Mmj2eFGMZMLcPrv_Mi-MgRJHe6mpFAa-GdMXdILFrd8,7627
selenium/webdriver/common/devtools/v102/memory.py,sha256=KdyZgvqzFAtKmHRxcDihqpZnRvRTsk0VQK8L5dGKc7A,6808
selenium/webdriver/common/devtools/v102/network.py,sha256=jLOsF5yriAoU4LDUp6PXNXcFj_bPOmlrpx0XusAYfCU,123743
selenium/webdriver/common/devtools/v102/overlay.py,sha256=UOyDsHVzPrng48FbQX9K6pcfj05Hj_Pi6qlU8izoWdw,50256
selenium/webdriver/common/devtools/v102/page.py,sha256=9WIasNqGXvL4UjXZygfsoZaJL8H53hmehq8i8z-u17Y,102102
selenium/webdriver/common/devtools/v102/performance.py,sha256=473SXp3oVXk295P4aiYuoB-wkm0VPgOGHrl7ZedfCo0,2927
selenium/webdriver/common/devtools/v102/performance_timeline.py,sha256=_mRYyRzB_9NbbE3b_ZWxIkebwK01y1yMzCrYb0TBMlo,6623
selenium/webdriver/common/devtools/v102/profiler.py,sha256=1m972dA0zAnrZiS3KuvT4EpWUmDfvAmMAJ5_pHI2CJY,15772
selenium/webdriver/common/devtools/v102/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
selenium/webdriver/common/devtools/v102/runtime.py,sha256=LwAlY3rGKWGGxmUAlFIHKLaHGmxpaftrGSrP765M-k4,57860
selenium/webdriver/common/devtools/v102/schema.py,sha256=fbNSqooayAk-upeq4OoLeWfgLYudVhFoZPEhQ7OKJdw,1111
selenium/webdriver/common/devtools/v102/security.py,sha256=vMTeGe52TzLhoa2R36s3xFD8PnZqHdE5bi7aevdbcgE,16861
selenium/webdriver/common/devtools/v102/service_worker.py,sha256=LlhRw5Z613K8I96wTUU79VaWEiYURNOSMJBHxkC2KdU,11066
selenium/webdriver/common/devtools/v102/storage.py,sha256=c0gpIoarcXyrL9qbgashtxm0HPZaWbJWqW4hl46lW00,16331
selenium/webdriver/common/devtools/v102/system_info.py,sha256=2PaY1DGaJVaWDQxBGTon9PuZZ0KgzOrz7dpIaJkEEcg,11049
selenium/webdriver/common/devtools/v102/target.py,sha256=vb-HM8x-b5-S9PnGFyxnQ6WK6zFiDfcE7_yB3xhJFQ8,20915
selenium/webdriver/common/devtools/v102/tethering.py,sha256=brZK0Ex_pp1RZI-6m3BgoruXDOC_wkNpOH-S50-Dbso,1538
selenium/webdriver/common/devtools/v102/tracing.py,sha256=mnoSmhvemJMaJTKbqXo4djQMuFfJGlCy_b5mnuARy-I,12458
selenium/webdriver/common/devtools/v102/util.py,sha256=Kr37bDnAdqbom-MSZicf17z_xFQj5JEQwmGlAGu1shQ,455
selenium/webdriver/common/devtools/v102/web_audio.py,sha256=AZt0TNgQXBWGf5ep4by_o9ChKLxL7Cw9LRu9TP-qn4w,16895
selenium/webdriver/common/devtools/v102/web_authn.py,sha256=x5gxPUYJBkANHdxmC-tgQHdgr-oZSdA2sZwBDFkGwNg,12345
selenium/webdriver/common/devtools/v103/__init__.py,sha256=odgx0kjP8gxKQiplaFASHw9Ly1Nt-M6dlIJr0KPtzAE,1293
selenium/webdriver/common/devtools/v103/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/accessibility.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/animation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/audits.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/background_service.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/browser.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/cache_storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/cast.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/console.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/css.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/database.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/debugger.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/device_orientation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/dom.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/dom_debugger.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/dom_snapshot.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/dom_storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/emulation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/event_breakpoints.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/fetch.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/headless_experimental.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/heap_profiler.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/indexed_db.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/input_.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/inspector.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/io.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/layer_tree.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/log.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/media.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/memory.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/network.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/overlay.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/page.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/performance.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/performance_timeline.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/profiler.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/runtime.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/schema.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/security.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/service_worker.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/system_info.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/target.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/tethering.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/tracing.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/util.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/web_audio.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/__pycache__/web_authn.cpython-310.pyc,,
selenium/webdriver/common/devtools/v103/accessibility.py,sha256=JOBaqnUeqPgG0nm5BYdNVmQtKbFRCsPz4zzOoLiHcQc,21911
selenium/webdriver/common/devtools/v103/animation.py,sha256=uyeJ8qZu7U6RfPgHaS0d4loPBnyz0lbH9RoTwn8oBSw,11112
selenium/webdriver/common/devtools/v103/audits.py,sha256=SbIsL-C3l7Jyvj7X9cEJ8NIEtACuttSx2SroNq06oVc,47666
selenium/webdriver/common/devtools/v103/background_service.py,sha256=IrsZjfMaZLbAsSq6yn0vlIpyKIykuJGI_RSgmv4SPsM,5760
selenium/webdriver/common/devtools/v103/browser.py,sha256=9jDo4SupO17Up33-FMX3SF88LODzz3zGRxulN5mjS2E,20683
selenium/webdriver/common/devtools/v103/cache_storage.py,sha256=XpnSE40sg1LNifK2kYe51F9ZV3jW8vzE46hbzVi1v8Y,7810
selenium/webdriver/common/devtools/v103/cast.py,sha256=32wNaJwk_0S5X-UdjazzLauMyXx6JPkyJHUdsxm8scs,4382
selenium/webdriver/common/devtools/v103/console.py,sha256=_mGKNlgtple6r9rJ_GxEfhkoQsMT7D1V6IeF_lAOBJ4,2765
selenium/webdriver/common/devtools/v103/css.py,sha256=cEq_4pCMnB7-Siv8dk2LedpXRSMNEk8NJkPZXKpJAnE,55616
selenium/webdriver/common/devtools/v103/database.py,sha256=ucK8g37lBtFgYWgzM5go6YJxQcQ2sTqPy9T2wymGxrA,3925
selenium/webdriver/common/devtools/v103/debugger.py,sha256=m0GkNkNwvOgADMbLAMCUOYMM9vmfMBbvY8WX25898G0,44487
selenium/webdriver/common/devtools/v103/device_orientation.py,sha256=iTyh5DWm7zRYzmq6VBe3_XakjFCC8kuuXc_NVIeNb2k,1209
selenium/webdriver/common/devtools/v103/dom.py,sha256=I2ETt27WCevNzDkPkX32FabdX1xQSaYS94_HioVk7Dc,59373
selenium/webdriver/common/devtools/v103/dom_debugger.py,sha256=HgKs0cpnWhtViWsPQt6tBBsHsdkLQbh2hizyFtyqRFc,9459
selenium/webdriver/common/devtools/v103/dom_snapshot.py,sha256=KdPjwnF0lGudJz3oUtJF_y4prGJJTdOs3L9ps3zfyow,36328
selenium/webdriver/common/devtools/v103/dom_storage.py,sha256=YXm-9sGNr-aaI4-LoH95kxD-kK5G0AEz4sMykr1o1Lo,6254
selenium/webdriver/common/devtools/v103/emulation.py,sha256=7hHgn-nXYZSryjJYwUiLPNQ5P8DkKhBH5PV14fKTyyk,25359
selenium/webdriver/common/devtools/v103/event_breakpoints.py,sha256=ogM8_6wu8gz52-4f394gydOSXyXIiN-QDGluLUZ9ro4,1289
selenium/webdriver/common/devtools/v103/fetch.py,sha256=nO6DG5xqVTRbg5-U92ULuctTh3bEuH-afm9TluWkt-w,18611
selenium/webdriver/common/devtools/v103/headless_experimental.py,sha256=TjJfwR6MosnJ1Lh-MKXwYFF6sK6O-FDCdXgNc-EEico,4811
selenium/webdriver/common/devtools/v103/heap_profiler.py,sha256=0Mc5udNeFX6bf-DVRhZ-GmDwU2lf_AbzmPO-RxqJ5Oc,12337
selenium/webdriver/common/devtools/v103/indexed_db.py,sha256=iA91BkxRAmGC15Oyc54gy690E79f7REqysCSgRxKqdE,12762
selenium/webdriver/common/devtools/v103/input_.py,sha256=ZZpGjItj5BhYWWz966XSvKIRshO23P1qeZaVBEEDwug,27841
selenium/webdriver/common/devtools/v103/inspector.py,sha256=oroH9p3tWRYh5poAM48tUmAU4_V8mJBXCGGjQt9ATOY,1718
selenium/webdriver/common/devtools/v103/io.py,sha256=IWzLKRqziXr-qZ8_TJhgWbi-Z-Yo3mfAGDTjgYwMR50,3036
selenium/webdriver/common/devtools/v103/layer_tree.py,sha256=EZzeGIvsAMOzqY9BTzx2FWMKcML5MiMvu_wALaG5PiQ,15049
selenium/webdriver/common/devtools/v103/log.py,sha256=kBm2ACgcTbC5T6nGROJnMfofA9zjLtO9K2hUwFywqS0,5264
selenium/webdriver/common/devtools/v103/media.py,sha256=Mmj2eFGMZMLcPrv_Mi-MgRJHe6mpFAa-GdMXdILFrd8,7627
selenium/webdriver/common/devtools/v103/memory.py,sha256=KdyZgvqzFAtKmHRxcDihqpZnRvRTsk0VQK8L5dGKc7A,6808
selenium/webdriver/common/devtools/v103/network.py,sha256=jLOsF5yriAoU4LDUp6PXNXcFj_bPOmlrpx0XusAYfCU,123743
selenium/webdriver/common/devtools/v103/overlay.py,sha256=UOyDsHVzPrng48FbQX9K6pcfj05Hj_Pi6qlU8izoWdw,50256
selenium/webdriver/common/devtools/v103/page.py,sha256=IL8KPY-ViS_KFKEXf29iEWCeAhfnOPAyiJ1fa9C3NNk,104004
selenium/webdriver/common/devtools/v103/performance.py,sha256=473SXp3oVXk295P4aiYuoB-wkm0VPgOGHrl7ZedfCo0,2927
selenium/webdriver/common/devtools/v103/performance_timeline.py,sha256=_mRYyRzB_9NbbE3b_ZWxIkebwK01y1yMzCrYb0TBMlo,6623
selenium/webdriver/common/devtools/v103/profiler.py,sha256=1m972dA0zAnrZiS3KuvT4EpWUmDfvAmMAJ5_pHI2CJY,15772
selenium/webdriver/common/devtools/v103/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
selenium/webdriver/common/devtools/v103/runtime.py,sha256=um55mIkHYbPoIlLARZp72Et-xCkDwdpxh-Uj18z_OzI,57994
selenium/webdriver/common/devtools/v103/schema.py,sha256=fbNSqooayAk-upeq4OoLeWfgLYudVhFoZPEhQ7OKJdw,1111
selenium/webdriver/common/devtools/v103/security.py,sha256=vMTeGe52TzLhoa2R36s3xFD8PnZqHdE5bi7aevdbcgE,16861
selenium/webdriver/common/devtools/v103/service_worker.py,sha256=LlhRw5Z613K8I96wTUU79VaWEiYURNOSMJBHxkC2KdU,11066
selenium/webdriver/common/devtools/v103/storage.py,sha256=bqihOMY5dKxx_I5TvTDh4_YZ8_ugIgnf_uvLTr056w4,16612
selenium/webdriver/common/devtools/v103/system_info.py,sha256=2PaY1DGaJVaWDQxBGTon9PuZZ0KgzOrz7dpIaJkEEcg,11049
selenium/webdriver/common/devtools/v103/target.py,sha256=vb-HM8x-b5-S9PnGFyxnQ6WK6zFiDfcE7_yB3xhJFQ8,20915
selenium/webdriver/common/devtools/v103/tethering.py,sha256=brZK0Ex_pp1RZI-6m3BgoruXDOC_wkNpOH-S50-Dbso,1538
selenium/webdriver/common/devtools/v103/tracing.py,sha256=mnoSmhvemJMaJTKbqXo4djQMuFfJGlCy_b5mnuARy-I,12458
selenium/webdriver/common/devtools/v103/util.py,sha256=Kr37bDnAdqbom-MSZicf17z_xFQj5JEQwmGlAGu1shQ,455
selenium/webdriver/common/devtools/v103/web_audio.py,sha256=AZt0TNgQXBWGf5ep4by_o9ChKLxL7Cw9LRu9TP-qn4w,16895
selenium/webdriver/common/devtools/v103/web_authn.py,sha256=XPSXCFVNE9877HzDlEh__Mgf5MWo-j4PwfTfOTDxGWk,12846
selenium/webdriver/common/devtools/v104/__init__.py,sha256=odgx0kjP8gxKQiplaFASHw9Ly1Nt-M6dlIJr0KPtzAE,1293
selenium/webdriver/common/devtools/v104/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/accessibility.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/animation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/audits.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/background_service.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/browser.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/cache_storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/cast.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/console.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/css.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/database.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/debugger.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/device_orientation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/dom.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/dom_debugger.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/dom_snapshot.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/dom_storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/emulation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/event_breakpoints.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/fetch.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/headless_experimental.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/heap_profiler.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/indexed_db.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/input_.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/inspector.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/io.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/layer_tree.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/log.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/media.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/memory.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/network.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/overlay.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/page.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/performance.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/performance_timeline.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/profiler.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/runtime.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/schema.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/security.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/service_worker.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/system_info.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/target.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/tethering.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/tracing.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/util.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/web_audio.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/__pycache__/web_authn.cpython-310.pyc,,
selenium/webdriver/common/devtools/v104/accessibility.py,sha256=JOBaqnUeqPgG0nm5BYdNVmQtKbFRCsPz4zzOoLiHcQc,21911
selenium/webdriver/common/devtools/v104/animation.py,sha256=uyeJ8qZu7U6RfPgHaS0d4loPBnyz0lbH9RoTwn8oBSw,11112
selenium/webdriver/common/devtools/v104/audits.py,sha256=DirkZwq6rEV71VahHeuOPRsRJNYPWitgCoitduZ-Neg,47493
selenium/webdriver/common/devtools/v104/background_service.py,sha256=IrsZjfMaZLbAsSq6yn0vlIpyKIykuJGI_RSgmv4SPsM,5760
selenium/webdriver/common/devtools/v104/browser.py,sha256=9jDo4SupO17Up33-FMX3SF88LODzz3zGRxulN5mjS2E,20683
selenium/webdriver/common/devtools/v104/cache_storage.py,sha256=XpnSE40sg1LNifK2kYe51F9ZV3jW8vzE46hbzVi1v8Y,7810
selenium/webdriver/common/devtools/v104/cast.py,sha256=32wNaJwk_0S5X-UdjazzLauMyXx6JPkyJHUdsxm8scs,4382
selenium/webdriver/common/devtools/v104/console.py,sha256=_mGKNlgtple6r9rJ_GxEfhkoQsMT7D1V6IeF_lAOBJ4,2765
selenium/webdriver/common/devtools/v104/css.py,sha256=M12NV99Vxz5W2imPWTYZuESJ70GmiZY6RLs32iLHyuE,56061
selenium/webdriver/common/devtools/v104/database.py,sha256=ucK8g37lBtFgYWgzM5go6YJxQcQ2sTqPy9T2wymGxrA,3925
selenium/webdriver/common/devtools/v104/debugger.py,sha256=cXA4-J6eIv0PUtA6r2fb_gTy5Rrll-TcayFbPhH8qVw,45453
selenium/webdriver/common/devtools/v104/device_orientation.py,sha256=iTyh5DWm7zRYzmq6VBe3_XakjFCC8kuuXc_NVIeNb2k,1209
selenium/webdriver/common/devtools/v104/dom.py,sha256=vqJS2ckBGxPI7EPsXfPwtNC4ljHcp3R5oKX4KVSW2-A,60005
selenium/webdriver/common/devtools/v104/dom_debugger.py,sha256=HgKs0cpnWhtViWsPQt6tBBsHsdkLQbh2hizyFtyqRFc,9459
selenium/webdriver/common/devtools/v104/dom_snapshot.py,sha256=vQc7m2npPm9mrQqSDZWD0NrQY_NVuUWjxW1y2LI-OjE,36732
selenium/webdriver/common/devtools/v104/dom_storage.py,sha256=5BqIxbqu_wCmiZ3tX59AxgbGuBKY6TX_RCT7you5lTs,5765
selenium/webdriver/common/devtools/v104/emulation.py,sha256=FOMnFQVsNr2XEm8Gq5puC853XspBSeDVc11eb3xvVXk,25835
selenium/webdriver/common/devtools/v104/event_breakpoints.py,sha256=ogM8_6wu8gz52-4f394gydOSXyXIiN-QDGluLUZ9ro4,1289
selenium/webdriver/common/devtools/v104/fetch.py,sha256=nO6DG5xqVTRbg5-U92ULuctTh3bEuH-afm9TluWkt-w,18611
selenium/webdriver/common/devtools/v104/headless_experimental.py,sha256=TjJfwR6MosnJ1Lh-MKXwYFF6sK6O-FDCdXgNc-EEico,4811
selenium/webdriver/common/devtools/v104/heap_profiler.py,sha256=0Mc5udNeFX6bf-DVRhZ-GmDwU2lf_AbzmPO-RxqJ5Oc,12337
selenium/webdriver/common/devtools/v104/indexed_db.py,sha256=iA91BkxRAmGC15Oyc54gy690E79f7REqysCSgRxKqdE,12762
selenium/webdriver/common/devtools/v104/input_.py,sha256=ZZpGjItj5BhYWWz966XSvKIRshO23P1qeZaVBEEDwug,27841
selenium/webdriver/common/devtools/v104/inspector.py,sha256=oroH9p3tWRYh5poAM48tUmAU4_V8mJBXCGGjQt9ATOY,1718
selenium/webdriver/common/devtools/v104/io.py,sha256=IWzLKRqziXr-qZ8_TJhgWbi-Z-Yo3mfAGDTjgYwMR50,3036
selenium/webdriver/common/devtools/v104/layer_tree.py,sha256=EZzeGIvsAMOzqY9BTzx2FWMKcML5MiMvu_wALaG5PiQ,15049
selenium/webdriver/common/devtools/v104/log.py,sha256=kBm2ACgcTbC5T6nGROJnMfofA9zjLtO9K2hUwFywqS0,5264
selenium/webdriver/common/devtools/v104/media.py,sha256=Mmj2eFGMZMLcPrv_Mi-MgRJHe6mpFAa-GdMXdILFrd8,7627
selenium/webdriver/common/devtools/v104/memory.py,sha256=KdyZgvqzFAtKmHRxcDihqpZnRvRTsk0VQK8L5dGKc7A,6808
selenium/webdriver/common/devtools/v104/network.py,sha256=SEIkbRy0OVYyRpe2GtT6RcJTZjmXgWM7aDJWXL6HoeM,123782
selenium/webdriver/common/devtools/v104/overlay.py,sha256=UOyDsHVzPrng48FbQX9K6pcfj05Hj_Pi6qlU8izoWdw,50256
selenium/webdriver/common/devtools/v104/page.py,sha256=BmYT32-EGLLP4wLme2woVHDuq9ZVm4qWRF8H5aUx1xY,105032
selenium/webdriver/common/devtools/v104/performance.py,sha256=473SXp3oVXk295P4aiYuoB-wkm0VPgOGHrl7ZedfCo0,2927
selenium/webdriver/common/devtools/v104/performance_timeline.py,sha256=_mRYyRzB_9NbbE3b_ZWxIkebwK01y1yMzCrYb0TBMlo,6623
selenium/webdriver/common/devtools/v104/profiler.py,sha256=1m972dA0zAnrZiS3KuvT4EpWUmDfvAmMAJ5_pHI2CJY,15772
selenium/webdriver/common/devtools/v104/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
selenium/webdriver/common/devtools/v104/runtime.py,sha256=um55mIkHYbPoIlLARZp72Et-xCkDwdpxh-Uj18z_OzI,57994
selenium/webdriver/common/devtools/v104/schema.py,sha256=fbNSqooayAk-upeq4OoLeWfgLYudVhFoZPEhQ7OKJdw,1111
selenium/webdriver/common/devtools/v104/security.py,sha256=vMTeGe52TzLhoa2R36s3xFD8PnZqHdE5bi7aevdbcgE,16861
selenium/webdriver/common/devtools/v104/service_worker.py,sha256=LlhRw5Z613K8I96wTUU79VaWEiYURNOSMJBHxkC2KdU,11066
selenium/webdriver/common/devtools/v104/storage.py,sha256=F8gav59qPCVTKcgtZbw4NHlzcEPrrcFqKM7a1tqz7gk,17142
selenium/webdriver/common/devtools/v104/system_info.py,sha256=2PaY1DGaJVaWDQxBGTon9PuZZ0KgzOrz7dpIaJkEEcg,11049
selenium/webdriver/common/devtools/v104/target.py,sha256=vb-HM8x-b5-S9PnGFyxnQ6WK6zFiDfcE7_yB3xhJFQ8,20915
selenium/webdriver/common/devtools/v104/tethering.py,sha256=brZK0Ex_pp1RZI-6m3BgoruXDOC_wkNpOH-S50-Dbso,1538
selenium/webdriver/common/devtools/v104/tracing.py,sha256=mnoSmhvemJMaJTKbqXo4djQMuFfJGlCy_b5mnuARy-I,12458
selenium/webdriver/common/devtools/v104/util.py,sha256=Kr37bDnAdqbom-MSZicf17z_xFQj5JEQwmGlAGu1shQ,455
selenium/webdriver/common/devtools/v104/web_audio.py,sha256=AZt0TNgQXBWGf5ep4by_o9ChKLxL7Cw9LRu9TP-qn4w,16895
selenium/webdriver/common/devtools/v104/web_authn.py,sha256=XPSXCFVNE9877HzDlEh__Mgf5MWo-j4PwfTfOTDxGWk,12846
selenium/webdriver/common/devtools/v85/__init__.py,sha256=6mggdTvoA7p_XVUR8gaSMKf3YgW9pN589OCWM1sUT9U,1258
selenium/webdriver/common/devtools/v85/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/accessibility.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/animation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/application_cache.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/audits.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/background_service.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/browser.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/cache_storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/cast.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/console.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/css.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/database.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/debugger.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/device_orientation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/dom.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/dom_debugger.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/dom_snapshot.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/dom_storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/emulation.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/fetch.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/headless_experimental.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/heap_profiler.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/indexed_db.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/input_.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/inspector.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/io.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/layer_tree.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/log.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/media.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/memory.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/network.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/overlay.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/page.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/performance.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/profiler.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/runtime.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/schema.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/security.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/service_worker.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/storage.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/system_info.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/target.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/tethering.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/tracing.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/util.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/web_audio.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/__pycache__/web_authn.cpython-310.pyc,,
selenium/webdriver/common/devtools/v85/accessibility.py,sha256=2kYomCXGOMOqbuiRxE22QTyNlwMJ4bVRzYEek2YQv94,15011
selenium/webdriver/common/devtools/v85/animation.py,sha256=uyeJ8qZu7U6RfPgHaS0d4loPBnyz0lbH9RoTwn8oBSw,11112
selenium/webdriver/common/devtools/v85/application_cache.py,sha256=RYyqqIVwx-L6Z-FXqPc5tD7S3poMOAHkX9zHECP94aA,5735
selenium/webdriver/common/devtools/v85/audits.py,sha256=c77RGRFYjtwXAXQMM_Tby979ClMkFSgexU5Wz-ln1hs,17067
selenium/webdriver/common/devtools/v85/background_service.py,sha256=IrsZjfMaZLbAsSq6yn0vlIpyKIykuJGI_RSgmv4SPsM,5760
selenium/webdriver/common/devtools/v85/browser.py,sha256=Wa5yoNzjZIB99c5bTscRPCmhpHilBBtIuO4I2zBorDI,17298
selenium/webdriver/common/devtools/v85/cache_storage.py,sha256=XpnSE40sg1LNifK2kYe51F9ZV3jW8vzE46hbzVi1v8Y,7810
selenium/webdriver/common/devtools/v85/cast.py,sha256=fqpo07lEYuCNX7NlXI_wMCfcCiL7j0euWXZPaZ44Qm8,3982
selenium/webdriver/common/devtools/v85/console.py,sha256=_mGKNlgtple6r9rJ_GxEfhkoQsMT7D1V6IeF_lAOBJ4,2765
selenium/webdriver/common/devtools/v85/css.py,sha256=Ao0AAFMNPwbZZWtz6yYo9cAjNIEjwRF3hrb-6i41ee8,42907
selenium/webdriver/common/devtools/v85/database.py,sha256=ucK8g37lBtFgYWgzM5go6YJxQcQ2sTqPy9T2wymGxrA,3925
selenium/webdriver/common/devtools/v85/debugger.py,sha256=Tyxui6Bv1bhdwJ2rsbJNeuW8amk354lEOtWFgLiwnY4,43473
selenium/webdriver/common/devtools/v85/device_orientation.py,sha256=iTyh5DWm7zRYzmq6VBe3_XakjFCC8kuuXc_NVIeNb2k,1209
selenium/webdriver/common/devtools/v85/dom.py,sha256=GeHSe0rQBrqWW2KbbJ4na90tjFFzoR8woYpWMgt7efM,54390
selenium/webdriver/common/devtools/v85/dom_debugger.py,sha256=wv1gMUxBfj9XK6haKFm1PsHH7oUUWxf2U_hL1YzAquI,8592
selenium/webdriver/common/devtools/v85/dom_snapshot.py,sha256=ZOEOEDYqQrIauzN-4BaQzj2qilCH0UEObd-DAfKCXeg,34069
selenium/webdriver/common/devtools/v85/dom_storage.py,sha256=35ho_LUl6SiRfVL6HckRpswt0vb7KGmij7A_hiTQy8k,5026
selenium/webdriver/common/devtools/v85/emulation.py,sha256=haxuxzQEqfLbpySsWZmNhSmLSqBdSXzf6UqYr5YUmdI,20772
selenium/webdriver/common/devtools/v85/fetch.py,sha256=pjm5WPNwN3WA-WEDvROZd6Dk0maifqt_FP5uNP9upFI,16053
selenium/webdriver/common/devtools/v85/headless_experimental.py,sha256=FfGXBeUm7sewWPeLtJ9x0bLJrXc1aUV_vE8dguO1iYY,4791
selenium/webdriver/common/devtools/v85/heap_profiler.py,sha256=GuGWWPrOJOU0lZko-TvjQasogv6lpdghLO9AE_gZECQ,11207
selenium/webdriver/common/devtools/v85/indexed_db.py,sha256=iA91BkxRAmGC15Oyc54gy690E79f7REqysCSgRxKqdE,12762
selenium/webdriver/common/devtools/v85/input_.py,sha256=8eF82VtOLYjNI6Kq5NzqdTxgsqkdmBXCU1GrocJNKN0,19701
selenium/webdriver/common/devtools/v85/inspector.py,sha256=oroH9p3tWRYh5poAM48tUmAU4_V8mJBXCGGjQt9ATOY,1718
selenium/webdriver/common/devtools/v85/io.py,sha256=Mn76wIWyZF5XyX6I94GU1sPp-fsVC26FIiS6MoD77MI,3034
selenium/webdriver/common/devtools/v85/layer_tree.py,sha256=EZzeGIvsAMOzqY9BTzx2FWMKcML5MiMvu_wALaG5PiQ,15049
selenium/webdriver/common/devtools/v85/log.py,sha256=v8YxsMi_UUFTirdCZbwhjBy-QRsruhe8pqGXU6wKffE,5062
selenium/webdriver/common/devtools/v85/media.py,sha256=jMi5rcJrLvoWDTkUS_KcjkOSm5ezh_u807VEaHSwr9A,6605
selenium/webdriver/common/devtools/v85/memory.py,sha256=KdyZgvqzFAtKmHRxcDihqpZnRvRTsk0VQK8L5dGKc7A,6808
selenium/webdriver/common/devtools/v85/network.py,sha256=R3hB7eL_G6GvOlrQV9oVfT30UzzAzusW5qvIjEbBHis,86888
selenium/webdriver/common/devtools/v85/overlay.py,sha256=A8ZJvmcq5D0AufGfJ7olNrACQ2Qd8eCkACPjnj6GLb8,24823
selenium/webdriver/common/devtools/v85/page.py,sha256=v4tQVvWPjhQKd7qVZmlLGrXaXoQda6-3K40N52frmbI,70795
selenium/webdriver/common/devtools/v85/performance.py,sha256=473SXp3oVXk295P4aiYuoB-wkm0VPgOGHrl7ZedfCo0,2927
selenium/webdriver/common/devtools/v85/profiler.py,sha256=K2ZO0DDGg0mmK4soQ6asNhcSYedHXgDtH9MiAV0zYB4,17177
selenium/webdriver/common/devtools/v85/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
selenium/webdriver/common/devtools/v85/runtime.py,sha256=uX-02-XMVhLNnZjqUkvMxGYhlS7D5PD0fBtbhoeWy9Y,51695
selenium/webdriver/common/devtools/v85/schema.py,sha256=fbNSqooayAk-upeq4OoLeWfgLYudVhFoZPEhQ7OKJdw,1111
selenium/webdriver/common/devtools/v85/security.py,sha256=MBXW2msUqqwtP2-tubVIu0kh6Zyqud9TZ8Q8ysIv8D8,16913
selenium/webdriver/common/devtools/v85/service_worker.py,sha256=LlhRw5Z613K8I96wTUU79VaWEiYURNOSMJBHxkC2KdU,11066
selenium/webdriver/common/devtools/v85/storage.py,sha256=e07wEq1t5vsy2kxsDyq9Xo50NCfJNtekQK1lBun8q7c,8277
selenium/webdriver/common/devtools/v85/system_info.py,sha256=2PaY1DGaJVaWDQxBGTon9PuZZ0KgzOrz7dpIaJkEEcg,11049
selenium/webdriver/common/devtools/v85/target.py,sha256=4sRfceZReSELAuFhnh6_iFfe6H9sP5ZynssrRw3hNG4,18516
selenium/webdriver/common/devtools/v85/tethering.py,sha256=brZK0Ex_pp1RZI-6m3BgoruXDOC_wkNpOH-S50-Dbso,1538
selenium/webdriver/common/devtools/v85/tracing.py,sha256=pINJ5eJyMEYwnewjQUQjR185yVUJ_fws2BuBjZHGAh0,10556
selenium/webdriver/common/devtools/v85/util.py,sha256=Kr37bDnAdqbom-MSZicf17z_xFQj5JEQwmGlAGu1shQ,455
selenium/webdriver/common/devtools/v85/web_audio.py,sha256=7zP6tg9l4vSQXqnpnOKrm49y2XK8gPsEmpRZshP-rMw,16894
selenium/webdriver/common/devtools/v85/web_authn.py,sha256=w5vnpzOhvMOYHUszzuBu2IwkseVGbxSuHCsaffgQ-Lc,9424
selenium/webdriver/common/html5/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/common/html5/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/common/html5/__pycache__/application_cache.cpython-310.pyc,,
selenium/webdriver/common/html5/application_cache.py,sha256=j-4cPdYV9rXYZng_1vdNMvssAt_XEyIvSL8YOsE-oN8,1631
selenium/webdriver/common/keys.py,sha256=aBnTFIQIrzptEGIsMtSZMq9twNAXIt_wWYDQ0hmmPlk,2329
selenium/webdriver/common/log.py,sha256=wajr6qantpt2Lvj69ssXt1-q2sL6HjpPOvTF-c3xJXw,5967
selenium/webdriver/common/mutation-listener.js,sha256=LCCDyaSfZcUQ1o02IKV9Tf7cjcD8wyUkwcyxHGMp6gc,1944
selenium/webdriver/common/options.py,sha256=T4tVj43JvPJBG8EqG4nyewzZq9Y5KQ0pdnmUUAtHwXs,8996
selenium/webdriver/common/print_page_options.py,sha256=BU8vu438Hp0sBa8nM4179WtxT3tAc8mG_nP-PAioraw,8500
selenium/webdriver/common/proxy.py,sha256=ABbuTZQ6p9j7rUgOV3A2VpDVvAMkoIpTpJko_Bmrmt0,10769
selenium/webdriver/common/service.py,sha256=3zEAuUNAiCeRcHf6tPWzqjpl_CEgXVdfG3HXmNLGCMM,5795
selenium/webdriver/common/timeouts.py,sha256=Bj9e0BkNmgVm_oVVmTocdaU9ccAtrv2TcWXGA_-fYoo,3831
selenium/webdriver/common/utils.py,sha256=aV043qj5iVjtyu8M31YuL3vnd-dk_kAmVGGSmHyP39c,4474
selenium/webdriver/common/virtual_authenticator.py,sha256=jnKgWPuR_VvlYnMFuv8K5gBBlH40J-2CUb8h7GTNaHU,8855
selenium/webdriver/common/window.py,sha256=jF7AFNY71X6b7zrltgKb6258gGtoikVZ9sAvNLFaTkg,929
selenium/webdriver/edge/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/edge/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/edge/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/edge/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/edge/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/edge/options.py,sha256=eSKgk369G7tUKJ8qME4Cg50aDrnA4rc5UDU_2Vzhkgk,1695
selenium/webdriver/edge/service.py,sha256=_KW7uymnpOpRsOWrWhHF_aVJaJ4m9POMyqa0XL5LeF4,2258
selenium/webdriver/edge/webdriver.py,sha256=c5cdIzqOoKYvY3yvoHyGOWsGzeR1iqob--VGLPNZomo,3307
selenium/webdriver/firefox/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/firefox/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/firefox/__pycache__/extension_connection.cpython-310.pyc,,
selenium/webdriver/firefox/__pycache__/firefox_binary.cpython-310.pyc,,
selenium/webdriver/firefox/__pycache__/firefox_profile.cpython-310.pyc,,
selenium/webdriver/firefox/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/firefox/__pycache__/remote_connection.cpython-310.pyc,,
selenium/webdriver/firefox/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/firefox/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/firefox/extension_connection.py,sha256=9WUldw-x9VaFbkcg1jL-SszDgKCRvK47k76aIGwFRM0,2840
selenium/webdriver/firefox/firefox_binary.py,sha256=MWfbYkJDI5-9hl1lDZz_kcfMjH0KA1-7B0oiP0LB08Y,8965
selenium/webdriver/firefox/firefox_profile.py,sha256=ss1rzT-1A61Ue4PB_guSO_DZzT68YLOj_lm_QpigYgE,14482
selenium/webdriver/firefox/options.py,sha256=LvrH0SOVnyh0-yblYArt7_LgwN8UgxsVW8mGGqTTA3k,5376
selenium/webdriver/firefox/remote_connection.py,sha256=ZcttASPljczTU_IFw_rwSPVHxzOfzHdg9sdy6UlMuWc,1718
selenium/webdriver/firefox/service.py,sha256=p6PBXUYeTqeyMHvffPt5bziAi9YAmXldxIPXgCXho9o,2679
selenium/webdriver/firefox/webdriver.py,sha256=HiY0r7uKPuJMn6dPd3S1iW-yWGLodA8Ld-9iMq6Ma_E,13471
selenium/webdriver/firefox/webdriver_prefs.json,sha256=lGrdKYpeI0bj1T0cvorXwz5JlBMFEfbYt5JovlC3o0w,2826
selenium/webdriver/ie/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/ie/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/ie/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/ie/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/ie/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/ie/options.py,sha256=BrjlqEOGOayscfFS0M1L2OvPMNqAZkcSig2rvOXwfrY,11534
selenium/webdriver/ie/service.py,sha256=yOqkXedxcWqSK9PAslxnqgfHrjl_ro8swfRb8E_CNo8,2334
selenium/webdriver/ie/webdriver.py,sha256=DdAk2xjUU654Qq8NC3QPmL8O7Doy7hJjRiMxLQ8b084,5504
selenium/webdriver/remote/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/remote/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/bidi_connection.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/command.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/errorhandler.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/file_detector.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/mobile.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/remote_connection.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/script_key.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/shadowroot.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/switch_to.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/utils.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/remote/__pycache__/webelement.cpython-310.pyc,,
selenium/webdriver/remote/bidi_connection.py,sha256=kSXSKjc0LUDPLyiYhdFYrppe3cgE5AJS95O8_U4-hG0,968
selenium/webdriver/remote/command.py,sha256=bgXHEB1YEtXCjdTtNMsvHCHlKGV21RBjnZeNH7wbLt0,5003
selenium/webdriver/remote/errorhandler.py,sha256=-uN2awrlSyADOlu-vaXHeDddud4ZBrAV4kZM1tzjMOA,11723
selenium/webdriver/remote/file_detector.py,sha256=hl32mi-e9BPcHtkq1OJPq6DmSHjsnmz0zSpmPISI_Fw,1817
selenium/webdriver/remote/findElements.js,sha256=eccGqSMLFWow7lMIA8_YfArAa6X-z-0iQ9HWBSnBETo,53824
selenium/webdriver/remote/getAttribute.js,sha256=IYbqcAcsY920rYnyMVp5Cam0qX9SpplXx02nJkHNrmo,43157
selenium/webdriver/remote/isDisplayed.js,sha256=69pAM_qjITC_ykt6Cz30FWWpkwHfkzEFSxj3kys0w4g,43996
selenium/webdriver/remote/mobile.py,sha256=epNOBAh3oZ4StWkIHgFFlWYvWn8iAhF7OvEW5Tz1Gxs,2673
selenium/webdriver/remote/remote_connection.py,sha256=XxgDz7QziXTOOY11MOp4u5d6VBjn7OxOyV6F7sN-PwM,18014
selenium/webdriver/remote/script_key.py,sha256=a-ZTYElQUjXLU-6QVKm3irw4IdDi267jAPXsmCcQfhI,1083
selenium/webdriver/remote/shadowroot.py,sha256=XCNFRS98jo7mmdn0DOHy8ueHHlH3I34DfomwS6pEhXo,3006
selenium/webdriver/remote/switch_to.py,sha256=JQpbz000R_TCLKEY9lLGnkGKNb9U4qG7JY8jb3kf-eY,5081
selenium/webdriver/remote/utils.py,sha256=SBLoZTqTBNqAxjawprIgB1QeQegHWVbkQyazgSKPLKQ,978
selenium/webdriver/remote/webdriver.py,sha256=xRe3ZHaCuC3ZBy6j6QJ09KcqShwmoHwcfwCc88GwQUc,43675
selenium/webdriver/remote/webelement.py,sha256=zvLZZ546XaKTkr2sZtWFXcXmdXwnAyZ6xVsCCQ2GlDY,17156
selenium/webdriver/safari/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/safari/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/safari/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/safari/__pycache__/permissions.cpython-310.pyc,,
selenium/webdriver/safari/__pycache__/remote_connection.cpython-310.pyc,,
selenium/webdriver/safari/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/safari/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/safari/options.py,sha256=L-QXvOaGvrdM4XU_NjSuWGjFMs041U63SaS6IkCdMkU,4243
selenium/webdriver/safari/permissions.py,sha256=Fmb0cr3QL8fYnvpRpKADNhZLBrQAiR4hUAMwhpf8r8U,934
selenium/webdriver/safari/remote_connection.py,sha256=TVl2J44_FYO3SnkxcMDai8KSxaL3lAxj3s_d1rqQKLY,1503
selenium/webdriver/safari/service.py,sha256=SpdZuKMNWhxaoCjj9KzbLnK-JFOJ1R-zvBdP_fqKFts,2500
selenium/webdriver/safari/webdriver.py,sha256=lZF92BtV1EIE8Am9ur8E3cxrVuxbXn5eh1bqQ6P0MAw,6268
selenium/webdriver/support/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/support/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/abstract_event_listener.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/color.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/event_firing_webdriver.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/events.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/expected_conditions.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/relative_locator.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/select.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/ui.cpython-310.pyc,,
selenium/webdriver/support/__pycache__/wait.cpython-310.pyc,,
selenium/webdriver/support/abstract_event_listener.py,sha256=9eYWmxdTCOUw7bOs8Ke55VVpSWw8VOcvbicFb442Gi0,2025
selenium/webdriver/support/color.py,sha256=cPNdHYgDwUyMbAme2bVVw2xRrlm3YtmyzfXcQjagHiY,12301
selenium/webdriver/support/event_firing_webdriver.py,sha256=thUIObC_SI2g3q307iu91dv_pd26LNPMU__iAVxsFIg,8998
selenium/webdriver/support/events.py,sha256=amakwBWfJ37pInruIx1Jzev3-ocb6jCl9lc82TaD5Vw,920
selenium/webdriver/support/expected_conditions.py,sha256=eGB-xHOhkSaQiFd_pchl9KvuXpZzj1sHlBspCUoZQGw,14637
selenium/webdriver/support/relative_locator.py,sha256=QuiuGROfGRXKLPCqZSIXiYE1ILZl7S1I9cVr1dtOlsk,6033
selenium/webdriver/support/select.py,sha256=d5nY3ch3hiKHMAFUa6lEbKXCjdhmmmrrzo2bjEx8uAI,9254
selenium/webdriver/support/ui.py,sha256=q6QHalMLPmpmPV8PfmN5K3LCS3JLpygOG6JmYUd3C5k,863
selenium/webdriver/support/wait.py,sha256=hxltF_uAOuC0o7eYOhhZSYbd4gMQqI44X7hBGv2XYjk,4873
selenium/webdriver/webkitgtk/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/webkitgtk/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/webkitgtk/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/webkitgtk/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/webkitgtk/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/webkitgtk/options.py,sha256=yktZ2pVl24yFIgrj2FWf5wRyiNhwUQNHYw7Vgs8E28Q,2676
selenium/webdriver/webkitgtk/service.py,sha256=HrPyA28sU5liTJbew6XeD4D-IfXo_8U4yuzLk5r0mPY,1629
selenium/webdriver/webkitgtk/webdriver.py,sha256=DzAfn72iBrTZfewgZSaYkWdckoa0FU-cYsoHhJYAJx4,2970
selenium/webdriver/wpewebkit/__init__.py,sha256=3TKaBBK08eiCsGGFFcZlZwwjHHcmj2YO0xImghpJk38,787
selenium/webdriver/wpewebkit/__pycache__/__init__.cpython-310.pyc,,
selenium/webdriver/wpewebkit/__pycache__/options.cpython-310.pyc,,
selenium/webdriver/wpewebkit/__pycache__/service.cpython-310.pyc,,
selenium/webdriver/wpewebkit/__pycache__/webdriver.cpython-310.pyc,,
selenium/webdriver/wpewebkit/options.py,sha256=BC1_b84HdZJO4KhPxgcTjEK-e0j9ZR9DOWvrMHlh7Bk,2210
selenium/webdriver/wpewebkit/service.py,sha256=Itu0hLj0j2Q-r5C4wxw8KBeTl70wz8uPwsbPslaFFG8,1626
selenium/webdriver/wpewebkit/webdriver.py,sha256=18sXgc5BK2oZnemiTUK9GJ3TRQ7sy3yB_HSPZKiHKAk,2756

@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: bazel-wheelmaker 1.0
Root-Is-Purelib: true
Tag: py3-none-any

@ -0,0 +1,19 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
__version__ = "4.4.3"

@ -0,0 +1,85 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from .exceptions import ElementClickInterceptedException
from .exceptions import ElementNotInteractableException
from .exceptions import ElementNotSelectableException
from .exceptions import ElementNotVisibleException
from .exceptions import ImeActivationFailedException
from .exceptions import ImeNotAvailableException
from .exceptions import InsecureCertificateException
from .exceptions import InvalidArgumentException
from .exceptions import InvalidCookieDomainException
from .exceptions import InvalidCoordinatesException
from .exceptions import InvalidElementStateException
from .exceptions import InvalidSelectorException
from .exceptions import InvalidSessionIdException
from .exceptions import InvalidSwitchToTargetException
from .exceptions import JavascriptException
from .exceptions import MoveTargetOutOfBoundsException
from .exceptions import NoAlertPresentException
from .exceptions import NoSuchAttributeException
from .exceptions import NoSuchCookieException
from .exceptions import NoSuchElementException
from .exceptions import NoSuchFrameException
from .exceptions import NoSuchShadowRootException
from .exceptions import NoSuchWindowException
from .exceptions import RemoteDriverServerException
from .exceptions import ScreenshotException
from .exceptions import SessionNotCreatedException
from .exceptions import StaleElementReferenceException
from .exceptions import TimeoutException
from .exceptions import UnableToSetCookieException
from .exceptions import UnexpectedAlertPresentException
from .exceptions import UnexpectedTagNameException
from .exceptions import UnknownMethodException
from .exceptions import WebDriverException
__all__ = ["WebDriverException",
"InvalidSwitchToTargetException",
"NoSuchFrameException",
"NoSuchWindowException",
"NoSuchElementException",
"NoSuchAttributeException",
"NoSuchShadowRootException",
"StaleElementReferenceException",
"InvalidElementStateException",
"UnexpectedAlertPresentException",
"NoAlertPresentException",
"ElementNotVisibleException",
"ElementNotInteractableException",
"ElementNotSelectableException",
"InvalidCookieDomainException",
"UnableToSetCookieException",
"RemoteDriverServerException",
"TimeoutException",
"MoveTargetOutOfBoundsException",
"UnexpectedTagNameException",
"InvalidSelectorException",
"ImeNotAvailableException",
"ImeActivationFailedException",
"InvalidArgumentException",
"JavascriptException",
"NoSuchCookieException",
"ScreenshotException",
"ElementClickInterceptedException",
"InsecureCertificateException",
"InvalidCoordinatesException",
"InvalidSessionIdException",
"SessionNotCreatedException",
"UnknownMethodException"]

@ -0,0 +1,326 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Exceptions that may happen in all the webdriver code.
"""
from typing import Optional, Sequence
class WebDriverException(Exception):
"""
Base webdriver exception.
"""
def __init__(self, msg: Optional[str] = None, screen: Optional[str] = None, stacktrace: Optional[Sequence[str]] = None) -> None:
self.msg = msg
self.screen = screen
self.stacktrace = stacktrace
def __str__(self) -> str:
exception_msg = "Message: %s\n" % self.msg
if self.screen:
exception_msg += "Screenshot: available via screen\n"
if self.stacktrace:
stacktrace = "\n".join(self.stacktrace)
exception_msg += "Stacktrace:\n%s" % stacktrace
return exception_msg
class InvalidSwitchToTargetException(WebDriverException):
"""
Thrown when frame or window target to be switched doesn't exist.
"""
pass
class NoSuchFrameException(InvalidSwitchToTargetException):
"""
Thrown when frame target to be switched doesn't exist.
"""
pass
class NoSuchWindowException(InvalidSwitchToTargetException):
"""
Thrown when window target to be switched doesn't exist.
To find the current set of active window handles, you can get a list
of the active window handles in the following way::
print driver.window_handles
"""
pass
class NoSuchElementException(WebDriverException):
"""
Thrown when element could not be found.
If you encounter this exception, you may want to check the following:
* Check your selector used in your find_by...
* Element may not yet be on the screen at the time of the find operation,
(webpage is still loading) see selenium.webdriver.support.wait.WebDriverWait()
for how to write a wait wrapper to wait for an element to appear.
"""
pass
class NoSuchAttributeException(WebDriverException):
"""
Thrown when the attribute of element could not be found.
You may want to check if the attribute exists in the particular browser you are
testing against. Some browsers may have different property names for the same
property. (IE8's .innerText vs. Firefox .textContent)
"""
pass
class NoSuchShadowRootException(WebDriverException):
"""
Thrown when trying to access the shadow root of an element when it does not
have a shadow root attached.
"""
pass
class StaleElementReferenceException(WebDriverException):
"""
Thrown when a reference to an element is now "stale".
Stale means the element no longer appears on the DOM of the page.
Possible causes of StaleElementReferenceException include, but not limited to:
* You are no longer on the same page, or the page may have refreshed since the element
was located.
* The element may have been removed and re-added to the screen, since it was located.
Such as an element being relocated.
This can happen typically with a javascript framework when values are updated and the
node is rebuilt.
* Element may have been inside an iframe or another context which was refreshed.
"""
pass
class InvalidElementStateException(WebDriverException):
"""
Thrown when a command could not be completed because the element is in an invalid state.
This can be caused by attempting to clear an element that isn't both editable and resettable.
"""
pass
class UnexpectedAlertPresentException(WebDriverException):
"""
Thrown when an unexpected alert has appeared.
Usually raised when an unexpected modal is blocking the webdriver from executing
commands.
"""
def __init__(self, msg: Optional[str] = None, screen: Optional[str] = None, stacktrace: Optional[Sequence[str]] = None, alert_text: Optional[str] = None) -> None:
super().__init__(msg, screen, stacktrace)
self.alert_text = alert_text
def __str__(self) -> str:
return f"Alert Text: {self.alert_text}\n{super().__str__()}"
class NoAlertPresentException(WebDriverException):
"""
Thrown when switching to no presented alert.
This can be caused by calling an operation on the Alert() class when an alert is
not yet on the screen.
"""
pass
class ElementNotVisibleException(InvalidElementStateException):
"""
Thrown when an element is present on the DOM, but
it is not visible, and so is not able to be interacted with.
Most commonly encountered when trying to click or read text
of an element that is hidden from view.
"""
pass
class ElementNotInteractableException(InvalidElementStateException):
"""
Thrown when an element is present in the DOM but interactions
with that element will hit another element due to paint order
"""
pass
class ElementNotSelectableException(InvalidElementStateException):
"""
Thrown when trying to select an unselectable element.
For example, selecting a 'script' element.
"""
pass
class InvalidCookieDomainException(WebDriverException):
"""
Thrown when attempting to add a cookie under a different domain
than the current URL.
"""
pass
class UnableToSetCookieException(WebDriverException):
"""
Thrown when a driver fails to set a cookie.
"""
pass
class RemoteDriverServerException(WebDriverException):
"""
"""
pass
class TimeoutException(WebDriverException):
"""
Thrown when a command does not complete in enough time.
"""
pass
class MoveTargetOutOfBoundsException(WebDriverException):
"""
Thrown when the target provided to the `ActionsChains` move()
method is invalid, i.e. out of document.
"""
pass
class UnexpectedTagNameException(WebDriverException):
"""
Thrown when a support class did not get an expected web element.
"""
pass
class InvalidSelectorException(WebDriverException):
"""
Thrown when the selector which is used to find an element does not return
a WebElement. Currently this only happens when the selector is an xpath
expression and it is either syntactically invalid (i.e. it is not a
xpath expression) or the expression does not select WebElements
(e.g. "count(//input)").
"""
pass
class ImeNotAvailableException(WebDriverException):
"""
Thrown when IME support is not available. This exception is thrown for every IME-related
method call if IME support is not available on the machine.
"""
pass
class ImeActivationFailedException(WebDriverException):
"""
Thrown when activating an IME engine has failed.
"""
pass
class InvalidArgumentException(WebDriverException):
"""
The arguments passed to a command are either invalid or malformed.
"""
pass
class JavascriptException(WebDriverException):
"""
An error occurred while executing JavaScript supplied by the user.
"""
pass
class NoSuchCookieException(WebDriverException):
"""
No cookie matching the given path name was found amongst the associated cookies of the
current browsing context's active document.
"""
pass
class ScreenshotException(WebDriverException):
"""
A screen capture was made impossible.
"""
pass
class ElementClickInterceptedException(WebDriverException):
"""
The Element Click command could not be completed because the element receiving the events
is obscuring the element that was requested to be clicked.
"""
pass
class InsecureCertificateException(WebDriverException):
"""
Navigation caused the user agent to hit a certificate warning, which is usually the result
of an expired or invalid TLS certificate.
"""
pass
class InvalidCoordinatesException(WebDriverException):
"""
The coordinates provided to an interaction's operation are invalid.
"""
pass
class InvalidSessionIdException(WebDriverException):
"""
Occurs if the given session id is not in the list of active sessions, meaning the session
either does not exist or that it's not active.
"""
pass
class SessionNotCreatedException(WebDriverException):
"""
A new session could not be created.
"""
pass
class UnknownMethodException(WebDriverException):
"""
The requested command matched a known URL but did not match any methods for that URL.
"""
pass

@ -0,0 +1,24 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Selenium type definitions."""
import typing
AnyKey = typing.Union[str, int, float]
WaitExcTypes = typing.Iterable[typing.Type[Exception]]

@ -0,0 +1,64 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from .firefox.webdriver import WebDriver as Firefox # noqa
from .firefox.firefox_profile import FirefoxProfile # noqa
from .firefox.options import Options as FirefoxOptions # noqa
from .chrome.webdriver import WebDriver as Chrome # noqa
from .chrome.options import Options as ChromeOptions # noqa
from .ie.webdriver import WebDriver as Ie # noqa
from .ie.options import Options as IeOptions # noqa
from .edge.webdriver import WebDriver as Edge # noqa
from .edge.webdriver import WebDriver as ChromiumEdge # noqa
from .edge.options import Options as EdgeOptions # noqa
from .safari.webdriver import WebDriver as Safari # noqa
from .webkitgtk.webdriver import WebDriver as WebKitGTK # noqa
from .webkitgtk.options import Options as WebKitGTKOptions # noqa
from .wpewebkit.webdriver import WebDriver as WPEWebKit # noqa
from .wpewebkit.options import Options as WPEWebKitOptions # noqa
from .remote.webdriver import WebDriver as Remote # noqa
from .common.desired_capabilities import DesiredCapabilities # noqa
from .common.action_chains import ActionChains # noqa
from .common.proxy import Proxy # noqa
from .common.keys import Keys # noqa
__version__ = '4.4.3'
# We need an explicit __all__ because the above won't otherwise be exported.
__all__ = [
"Firefox",
"FirefoxProfile",
"FirefoxOptions",
"Chrome",
"ChromeOptions",
"Ie",
"IeOptions",
"Edge",
"ChromiumEdge",
"EdgeOptions",
"Opera",
"Safari",
"WebKitGTK",
"WebKitGTKOptions",
"WPEWebKit",
"WPEWebKitOptions",
"Remote",
"DesiredCapabilities",
"ActionChains",
"Proxy",
"Keys",
]

@ -0,0 +1,16 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

@ -0,0 +1,34 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.chromium.options import ChromiumOptions
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from typing import Optional
class Options(ChromiumOptions):
@property
def default_capabilities(self) -> dict:
return DesiredCapabilities.CHROME.copy()
def enable_mobile(self,
android_package: str = "com.android.chrome",
android_activity: Optional[str] = None,
device_serial: Optional[str] = None
) -> None:
super().enable_mobile(android_package, android_activity, device_serial)

@ -0,0 +1,48 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import List
from selenium.webdriver.chromium import service
DEFAULT_EXECUTABLE_PATH = "chromedriver"
class Service(service.ChromiumService):
"""
Object that manages the starting and stopping of the ChromeDriver
"""
def __init__(self, executable_path: str = DEFAULT_EXECUTABLE_PATH,
port: int = 0, service_args: List[str] = None,
log_path: str = None, env: dict = None):
"""
Creates a new instance of the Service
:Args:
- executable_path : Path to the ChromeDriver
- port : Port the service is running on
- service_args : List of args to pass to the chromedriver service
- log_path : Path for the chromedriver service to log to"""
super().__init__(
executable_path,
port,
service_args,
log_path,
env,
"Please see https://chromedriver.chromium.org/home")

@ -0,0 +1,72 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import warnings
from selenium.webdriver.chromium.webdriver import ChromiumDriver
from .options import Options
from .service import DEFAULT_EXECUTABLE_PATH, Service
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
DEFAULT_PORT = 0
DEFAULT_SERVICE_LOG_PATH = None
DEFAULT_KEEP_ALIVE = None
class WebDriver(ChromiumDriver):
"""
Controls the ChromeDriver and allows you to drive the browser.
You will need to download the ChromeDriver executable from
http://chromedriver.storage.googleapis.com/index.html
"""
def __init__(self, executable_path=DEFAULT_EXECUTABLE_PATH, port=DEFAULT_PORT,
options: Options = None, service_args=None,
desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH,
chrome_options=None, service: Service = None, keep_alive=DEFAULT_KEEP_ALIVE):
"""
Creates a new instance of the chrome driver.
Starts the service and then creates new instance of chrome driver.
:Args:
- executable_path - Deprecated: path to the executable. If the default is used it assumes the executable is in the $PATH
- port - Deprecated: port you would like the service to run, if left as 0, a free port will be found.
- options - this takes an instance of ChromeOptions
- service - Service object for handling the browser driver if you need to pass extra details
- service_args - Deprecated: List of args to pass to the driver service
- desired_capabilities - Deprecated: Dictionary object with non-browser specific
capabilities only, such as "proxy" or "loggingPref".
- service_log_path - Deprecated: Where to log information from the driver.
- keep_alive - Deprecated: Whether to configure ChromeRemoteConnection to use HTTP keep-alive.
"""
if executable_path != 'chromedriver':
warnings.warn('executable_path has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
if chrome_options:
warnings.warn('use options instead of chrome_options',
DeprecationWarning, stacklevel=2)
options = chrome_options
if keep_alive != DEFAULT_KEEP_ALIVE:
warnings.warn('keep_alive has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
else:
keep_alive = True
if not service:
service = Service(executable_path, port, service_args, service_log_path)
super().__init__(DesiredCapabilities.CHROME['browserName'], "goog",
port, options,
service_args, desired_capabilities,
service_log_path, service, keep_alive)

@ -0,0 +1,16 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

@ -0,0 +1,192 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import base64
import os
import warnings
from typing import List, Union, BinaryIO
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.options import ArgOptions
class ChromiumOptions(ArgOptions):
KEY = "goog:chromeOptions"
def __init__(self) -> None:
super().__init__()
self._binary_location = ''
self._extension_files = []
self._extensions = []
self._experimental_options = {}
self._debugger_address = None
@property
def binary_location(self) -> str:
"""
:Returns: The location of the binary, otherwise an empty string
"""
return self._binary_location
@binary_location.setter
def binary_location(self, value: str) -> None:
"""
Allows you to set where the chromium binary lives
:Args:
- value: path to the Chromium binary
"""
self._binary_location = value
@property
def debugger_address(self) -> str:
"""
:Returns: The address of the remote devtools instance
"""
return self._debugger_address
@debugger_address.setter
def debugger_address(self, value: str) -> None:
"""
Allows you to set the address of the remote devtools instance
that the ChromeDriver instance will try to connect to during an
active wait.
:Args:
- value: address of remote devtools instance if any (hostname[:port])
"""
self._debugger_address = value
@property
def extensions(self) -> List[str]:
"""
:Returns: A list of encoded extensions that will be loaded
"""
def _decode(file_data: BinaryIO) -> str:
# Should not use base64.encodestring() which inserts newlines every
# 76 characters (per RFC 1521). Chromedriver has to remove those
# unnecessary newlines before decoding, causing performance hit.
return base64.b64encode(file_data.read()).decode("utf-8")
encoded_extensions = []
for extension in self._extension_files:
with open(extension, "rb") as f:
encoded_extensions.append(_decode(f))
return encoded_extensions + self._extensions
def add_extension(self, extension: str) -> None:
"""
Adds the path to the extension to a list that will be used to extract it
to the ChromeDriver
:Args:
- extension: path to the \\*.crx file
"""
if extension:
extension_to_add = os.path.abspath(os.path.expanduser(extension))
if os.path.exists(extension_to_add):
self._extension_files.append(extension_to_add)
else:
raise OSError("Path to the extension doesn't exist")
else:
raise ValueError("argument can not be null")
def add_encoded_extension(self, extension: str) -> None:
"""
Adds Base64 encoded string with extension data to a list that will be used to extract it
to the ChromeDriver
:Args:
- extension: Base64 encoded string with extension data
"""
if extension:
self._extensions.append(extension)
else:
raise ValueError("argument can not be null")
@property
def experimental_options(self) -> dict:
"""
:Returns: A dictionary of experimental options for chromium
"""
return self._experimental_options
def add_experimental_option(self, name: str, value: Union[str, int, dict, List[str]]) -> None:
"""
Adds an experimental option which is passed to chromium.
:Args:
name: The experimental option name.
value: The option value.
"""
if name.lower() == "w3c" and (value == "false" or value is False):
warnings.warn(UserWarning("Manipulating `w3c` setting can have unintended consequences."))
self._experimental_options[name] = value
@property
def headless(self) -> bool:
"""
:Returns: True if the headless argument is set, else False
"""
return '--headless' in self._arguments
@headless.setter
def headless(self, value: bool) -> None:
"""
Sets the headless argument
:Args:
value: boolean value indicating to set the headless option
"""
args = {'--headless'}
if value is True:
self._arguments.extend(args)
else:
self._arguments = list(set(self._arguments) - args)
def to_capabilities(self) -> dict:
"""
Creates a capabilities with all the options that have been set
:Returns: A dictionary with everything
"""
caps = self._caps
chrome_options = self.experimental_options.copy()
if 'w3c' in chrome_options:
if chrome_options['w3c']:
warnings.warn(
"Setting 'w3c: True' is redundant and will no longer be allowed",
DeprecationWarning,
stacklevel=2
)
else:
raise AttributeError('setting w3c to False is not allowed, '
'Please update to W3C Syntax: '
'https://www.selenium.dev/blog/2022/legacy-protocol-support/')
if self.mobile_options:
chrome_options.update(self.mobile_options)
chrome_options["extensions"] = self.extensions
if self.binary_location:
chrome_options["binary"] = self.binary_location
chrome_options["args"] = self._arguments
if self.debugger_address:
chrome_options["debuggerAddress"] = self.debugger_address
caps[self.KEY] = chrome_options
return caps
@property
def default_capabilities(self) -> dict:
return DesiredCapabilities.CHROME.copy()

@ -0,0 +1,42 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import typing
from selenium.webdriver.remote.remote_connection import RemoteConnection
class ChromiumRemoteConnection(RemoteConnection):
def __init__(self,
remote_server_addr: str,
vendor_prefix: str,
browser_name: str,
keep_alive: bool = True,
ignore_proxy: typing.Optional[bool] = False):
super().__init__(remote_server_addr, keep_alive, ignore_proxy=ignore_proxy)
self.browser_name = browser_name
self._commands["launchApp"] = ('POST', '/session/$sessionId/chromium/launch_app')
self._commands["setPermissions"] = ('POST', '/session/$sessionId/permissions')
self._commands["setNetworkConditions"] = ('POST', '/session/$sessionId/chromium/network_conditions')
self._commands["getNetworkConditions"] = ('GET', '/session/$sessionId/chromium/network_conditions')
self._commands["deleteNetworkConditions"] = ('DELETE', '/session/$sessionId/chromium/network_conditions')
self._commands['executeCdpCommand'] = ('POST', f'/session/$sessionId/{vendor_prefix}/cdp/execute')
self._commands['getSinks'] = ('GET', f'/session/$sessionId/{vendor_prefix}/cast/get_sinks')
self._commands['getIssueMessage'] = ('GET', f'/session/$sessionId/{vendor_prefix}/cast/get_issue_message')
self._commands['setSinkToUse'] = ('POST', f'/session/$sessionId/{vendor_prefix}/cast/set_sink_to_use')
self._commands['startDesktopMirroring'] = ('POST', f'/session/$sessionId/{vendor_prefix}/cast/start_desktop_mirroring')
self._commands['startTabMirroring'] = ('POST', f'/session/$sessionId/{vendor_prefix}/cast/start_tab_mirroring')
self._commands['stopCasting'] = ('POST', f'/session/$sessionId/{vendor_prefix}/cast/stop_casting')

@ -0,0 +1,48 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import List
from selenium.webdriver.common import service
class ChromiumService(service.Service):
"""
Object that manages the starting and stopping the WebDriver instance of the ChromiumDriver
"""
def __init__(self, executable_path: str, port: int = 0, service_args: List[str] = None,
log_path: str = None, env: dict = None, start_error_message: str = None):
"""
Creates a new instance of the Service
:Args:
- executable_path : Path to the WebDriver executable
- port : Port the service is running on
- service_args : List of args to pass to the WebDriver service
- log_path : Path for the WebDriver service to log to"""
self.service_args = service_args or []
if log_path:
self.service_args.append('--log-path=%s' % log_path)
if not start_error_message:
raise AttributeError("start_error_message should not be empty")
super().__init__(executable_path, port=port, env=env, start_error_message=start_error_message)
def command_line_args(self) -> List[str]:
return ["--port=%d" % self.port] + self.service_args

@ -0,0 +1,244 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.common.options import BaseOptions
from selenium.webdriver.common.service import Service
from selenium.webdriver.edge.options import Options as EdgeOptions
from selenium.webdriver.chrome.options import Options as ChromeOptions
import warnings
from selenium.webdriver.chromium.remote_connection import ChromiumRemoteConnection
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
DEFAULT_PORT = 0
DEFAULT_SERVICE_LOG_PATH = None
DEFAULT_KEEP_ALIVE = None
class ChromiumDriver(RemoteWebDriver):
"""
Controls the WebDriver instance of ChromiumDriver and allows you to drive the browser.
"""
def __init__(self, browser_name, vendor_prefix,
port=DEFAULT_PORT, options: BaseOptions = None, service_args=None,
desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH,
service: Service = None, keep_alive=DEFAULT_KEEP_ALIVE):
"""
Creates a new WebDriver instance of the ChromiumDriver.
Starts the service and then creates new WebDriver instance of ChromiumDriver.
:Args:
- browser_name - Browser name used when matching capabilities.
- vendor_prefix - Company prefix to apply to vendor-specific WebDriver extension commands.
- port - Deprecated: port you would like the service to run, if left as 0, a free port will be found.
- options - this takes an instance of ChromiumOptions
- service_args - Deprecated: List of args to pass to the driver service
- desired_capabilities - Deprecated: Dictionary object with non-browser specific
capabilities only, such as "proxy" or "loggingPref".
- service_log_path - Deprecated: Where to log information from the driver.
- keep_alive - Deprecated: Whether to configure ChromiumRemoteConnection to use HTTP keep-alive.
"""
if desired_capabilities:
warnings.warn('desired_capabilities has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
if port != DEFAULT_PORT:
warnings.warn('port has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
self.port = port
if service_log_path != DEFAULT_SERVICE_LOG_PATH:
warnings.warn('service_log_path has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
if keep_alive != DEFAULT_KEEP_ALIVE and type(self) == __class__:
warnings.warn('keep_alive has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
else:
keep_alive = True
self.vendor_prefix = vendor_prefix
_ignore_proxy = None
if not options:
options = self.create_options()
if desired_capabilities:
for key, value in desired_capabilities.items():
options.set_capability(key, value)
if options._ignore_local_proxy:
_ignore_proxy = options._ignore_local_proxy
if not service:
raise AttributeError('service cannot be None')
self.service = service
self.service.start()
try:
super().__init__(
command_executor=ChromiumRemoteConnection(
remote_server_addr=self.service.service_url,
browser_name=browser_name, vendor_prefix=vendor_prefix,
keep_alive=keep_alive, ignore_proxy=_ignore_proxy),
options=options)
except Exception:
self.quit()
raise
self._is_remote = False
def launch_app(self, id):
"""Launches Chromium app specified by id."""
return self.execute("launchApp", {'id': id})
def get_network_conditions(self):
"""
Gets Chromium network emulation settings.
:Returns:
A dict. For example:
{'latency': 4, 'download_throughput': 2, 'upload_throughput': 2,
'offline': False}
"""
return self.execute("getNetworkConditions")['value']
def set_network_conditions(self, **network_conditions) -> None:
"""
Sets Chromium network emulation settings.
:Args:
- network_conditions: A dict with conditions specification.
:Usage:
::
driver.set_network_conditions(
offline=False,
latency=5, # additional latency (ms)
download_throughput=500 * 1024, # maximal throughput
upload_throughput=500 * 1024) # maximal throughput
Note: 'throughput' can be used to set both (for download and upload).
"""
self.execute("setNetworkConditions", {
'network_conditions': network_conditions
})
def delete_network_conditions(self) -> None:
"""
Resets Chromium network emulation settings.
"""
self.execute("deleteNetworkConditions")
def set_permissions(self, name: str, value: str) -> None:
"""
Sets Applicable Permission.
:Args:
- name: The item to set the permission on.
- value: The value to set on the item
:Usage:
::
driver.set_permissions('clipboard-read', 'denied')
"""
self.execute("setPermissions", {'descriptor': {'name': name}, 'state': value})
def execute_cdp_cmd(self, cmd: str, cmd_args: dict):
"""
Execute Chrome Devtools Protocol command and get returned result
The command and command args should follow chrome devtools protocol domains/commands, refer to link
https://chromedevtools.github.io/devtools-protocol/
:Args:
- cmd: A str, command name
- cmd_args: A dict, command args. empty dict {} if there is no command args
:Usage:
::
driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': requestId})
:Returns:
A dict, empty dict {} if there is no result to return.
For example to getResponseBody:
{'base64Encoded': False, 'body': 'response body string'}
"""
return self.execute("executeCdpCommand", {'cmd': cmd, 'params': cmd_args})['value']
def get_sinks(self) -> list:
"""
:Returns: A list of sinks available for Cast.
"""
return self.execute('getSinks')['value']
def get_issue_message(self):
"""
:Returns: An error message when there is any issue in a Cast session.
"""
return self.execute('getIssueMessage')['value']
def set_sink_to_use(self, sink_name: str) -> dict:
"""
Sets a specific sink, using its name, as a Cast session receiver target.
:Args:
- sink_name: Name of the sink to use as the target.
"""
return self.execute('setSinkToUse', {'sinkName': sink_name})
def start_desktop_mirroring(self, sink_name: str) -> dict:
"""
Starts a desktop mirroring session on a specific receiver target.
:Args:
- sink_name: Name of the sink to use as the target.
"""
return self.execute('startDesktopMirroring', {'sinkName': sink_name})
def start_tab_mirroring(self, sink_name: str) -> dict:
"""
Starts a tab mirroring session on a specific receiver target.
:Args:
- sink_name: Name of the sink to use as the target.
"""
return self.execute('startTabMirroring', {'sinkName': sink_name})
def stop_casting(self, sink_name: str) -> dict:
"""
Stops the existing Cast session on a specific receiver target.
:Args:
- sink_name: Name of the sink to stop the Cast session.
"""
return self.execute('stopCasting', {'sinkName': sink_name})
def quit(self) -> None:
"""
Closes the browser and shuts down the ChromiumDriver executable
that is started when starting the ChromiumDriver
"""
try:
super().quit()
except Exception:
# We don't care about the message because something probably has gone wrong
pass
finally:
self.service.stop()
def create_options(self) -> BaseOptions:
if self.vendor_prefix == "ms":
return EdgeOptions()
else:
return ChromeOptions()

@ -0,0 +1,16 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

@ -0,0 +1,402 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
The ActionChains implementation,
"""
import warnings
from .actions.wheel_input import ScrollOrigin
from .utils import keys_to_typing
from .actions.action_builder import ActionBuilder
from selenium.webdriver.remote.webelement import WebElement
class ActionChains:
"""
ActionChains are a way to automate low level interactions such as
mouse movements, mouse button actions, key press, and context menu interactions.
This is useful for doing more complex actions like hover over and drag and drop.
Generate user actions.
When you call methods for actions on the ActionChains object,
the actions are stored in a queue in the ActionChains object.
When you call perform(), the events are fired in the order they
are queued up.
ActionChains can be used in a chain pattern::
menu = driver.find_element(By.CSS_SELECTOR, ".nav")
hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")
ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()
Or actions can be queued up one by one, then performed.::
menu = driver.find_element(By.CSS_SELECTOR, ".nav")
hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")
actions = ActionChains(driver)
actions.move_to_element(menu)
actions.click(hidden_submenu)
actions.perform()
Either way, the actions are performed in the order they are called, one after
another.
"""
def __init__(self, driver, duration=250):
"""
Creates a new ActionChains.
:Args:
- driver: The WebDriver instance which performs user actions.
- duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in PointerInput
"""
self._driver = driver
self.w3c_actions = ActionBuilder(driver, duration=duration)
def perform(self):
"""
Performs all stored actions.
"""
self.w3c_actions.perform()
def reset_actions(self):
"""
Clears actions that are already stored locally and on the remote end
"""
self.w3c_actions.clear_actions()
for device in self.w3c_actions.devices:
device.clear_actions()
def click(self, on_element=None):
"""
Clicks an element.
:Args:
- on_element: The element to click.
If None, clicks on current mouse position.
"""
if on_element:
self.move_to_element(on_element)
self.w3c_actions.pointer_action.click()
self.w3c_actions.key_action.pause()
self.w3c_actions.key_action.pause()
return self
def click_and_hold(self, on_element=None):
"""
Holds down the left mouse button on an element.
:Args:
- on_element: The element to mouse down.
If None, clicks on current mouse position.
"""
if on_element:
self.move_to_element(on_element)
self.w3c_actions.pointer_action.click_and_hold()
self.w3c_actions.key_action.pause()
return self
def context_click(self, on_element=None):
"""
Performs a context-click (right click) on an element.
:Args:
- on_element: The element to context-click.
If None, clicks on current mouse position.
"""
if on_element:
self.move_to_element(on_element)
self.w3c_actions.pointer_action.context_click()
self.w3c_actions.key_action.pause()
self.w3c_actions.key_action.pause()
return self
def double_click(self, on_element=None):
"""
Double-clicks an element.
:Args:
- on_element: The element to double-click.
If None, clicks on current mouse position.
"""
if on_element:
self.move_to_element(on_element)
self.w3c_actions.pointer_action.double_click()
for _ in range(4):
self.w3c_actions.key_action.pause()
return self
def drag_and_drop(self, source, target):
"""
Holds down the left mouse button on the source element,
then moves to the target element and releases the mouse button.
:Args:
- source: The element to mouse down.
- target: The element to mouse up.
"""
self.click_and_hold(source)
self.release(target)
return self
def drag_and_drop_by_offset(self, source, xoffset, yoffset):
"""
Holds down the left mouse button on the source element,
then moves to the target offset and releases the mouse button.
:Args:
- source: The element to mouse down.
- xoffset: X offset to move to.
- yoffset: Y offset to move to.
"""
self.click_and_hold(source)
self.move_by_offset(xoffset, yoffset)
self.release()
return self
def key_down(self, value, element=None):
"""
Sends a key press only, without releasing it.
Should only be used with modifier keys (Control, Alt and Shift).
:Args:
- value: The modifier key to send. Values are defined in `Keys` class.
- element: The element to send keys.
If None, sends a key to current focused element.
Example, pressing ctrl+c::
ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
"""
if element:
self.click(element)
self.w3c_actions.key_action.key_down(value)
self.w3c_actions.pointer_action.pause()
return self
def key_up(self, value, element=None):
"""
Releases a modifier key.
:Args:
- value: The modifier key to send. Values are defined in Keys class.
- element: The element to send keys.
If None, sends a key to current focused element.
Example, pressing ctrl+c::
ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
"""
if element:
self.click(element)
self.w3c_actions.key_action.key_up(value)
self.w3c_actions.pointer_action.pause()
return self
def move_by_offset(self, xoffset, yoffset):
"""
Moving the mouse to an offset from current mouse position.
:Args:
- xoffset: X offset to move to, as a positive or negative integer.
- yoffset: Y offset to move to, as a positive or negative integer.
"""
self.w3c_actions.pointer_action.move_by(xoffset, yoffset)
self.w3c_actions.key_action.pause()
return self
def move_to_element(self, to_element):
"""
Moving the mouse to the middle of an element.
:Args:
- to_element: The WebElement to move to.
"""
self.w3c_actions.pointer_action.move_to(to_element)
self.w3c_actions.key_action.pause()
return self
def move_to_element_with_offset(self, to_element, xoffset, yoffset):
"""
Move the mouse by an offset of the specified element.
Offsets are relative to the top-left corner of the element.
:Args:
- to_element: The WebElement to move to.
- xoffset: X offset to move to.
- yoffset: Y offset to move to.
"""
self.w3c_actions.pointer_action.move_to(to_element,
int(xoffset),
int(yoffset))
self.w3c_actions.key_action.pause()
return self
def pause(self, seconds):
""" Pause all inputs for the specified duration in seconds """
self.w3c_actions.pointer_action.pause(seconds)
self.w3c_actions.key_action.pause(seconds)
return self
def release(self, on_element=None):
"""
Releasing a held mouse button on an element.
:Args:
- on_element: The element to mouse up.
If None, releases on current mouse position.
"""
if on_element:
self.move_to_element(on_element)
self.w3c_actions.pointer_action.release()
self.w3c_actions.key_action.pause()
return self
def send_keys(self, *keys_to_send):
"""
Sends keys to current focused element.
:Args:
- keys_to_send: The keys to send. Modifier keys constants can be found in the
'Keys' class.
"""
typing = keys_to_typing(keys_to_send)
for key in typing:
self.key_down(key)
self.key_up(key)
return self
def send_keys_to_element(self, element, *keys_to_send):
"""
Sends keys to an element.
:Args:
- element: The element to send keys.
- keys_to_send: The keys to send. Modifier keys constants can be found in the
'Keys' class.
"""
self.click(element)
self.send_keys(*keys_to_send)
return self
def scroll_to_element(self, element: WebElement):
"""
If the element is outside the viewport, scrolls the bottom of the element to the bottom of the viewport.
:Args:
- element: Which element to scroll into the viewport.
"""
self.w3c_actions.wheel_action.scroll(origin=element)
return self
def scroll_by_amount(self, delta_x: int, delta_y: int):
"""
Scrolls by provided amounts with the origin in the top left corner of the viewport.
:Args:
- delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.
- delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.
"""
self.w3c_actions.wheel_action.scroll(delta_x=delta_x, delta_y=delta_y)
return self
def scroll_from_origin(self, scroll_origin: ScrollOrigin, delta_x: int, delta_y: int):
"""
Scrolls by provided amount based on a provided origin.
The scroll origin is either the center of an element or the upper left of the viewport plus any offsets.
If the origin is an element, and the element is not in the viewport, the bottom of the element will first
be scrolled to the bottom of the viewport.
:Args:
- origin: Where scroll originates (viewport or element center) plus provided offsets.
- delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.
- delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.
:Raises: If the origin with offset is outside the viewport.
- MoveTargetOutOfBoundsException - If the origin with offset is outside the viewport.
"""
if not isinstance(scroll_origin, ScrollOrigin):
raise TypeError('Expected object of type ScrollOrigin, got: '
'{}'.format(type(scroll_origin)))
self.w3c_actions.wheel_action.scroll(origin=scroll_origin.origin,
x=scroll_origin.x_offset,
y=scroll_origin.y_offset,
delta_x=delta_x,
delta_y=delta_y)
return self
def scroll(self, x: int, y: int, delta_x: int, delta_y: int, duration: int = 0, origin: str = "viewport"):
"""
Sends wheel scroll information to the browser to be processed.
:Args:
- x: starting X coordinate
- y: starting Y coordinate
- delta_x: the distance the mouse will scroll on the x axis
- delta_y: the distance the mouse will scroll on the y axis
"""
warnings.warn(
"scroll() has been deprecated, please use scroll_to_element(), scroll_by_amount() or scroll_from_origin().",
DeprecationWarning,
stacklevel=2
)
self.w3c_actions.wheel_action.scroll(x=x, y=y, delta_x=delta_x, delta_y=delta_y,
duration=duration, origin=origin)
return self
# Context manager so ActionChains can be used in a 'with .. as' statements.
def __enter__(self):
return self # Return created instance of self.
def __exit__(self, _type, _value, _traceback):
pass # Do nothing, does not require additional cleanup.

@ -0,0 +1,16 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

@ -0,0 +1,97 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Union, List
from selenium.webdriver.remote.command import Command
from . import interaction
from .key_actions import KeyActions
from .key_input import KeyInput
from .pointer_actions import PointerActions
from .pointer_input import PointerInput
from .wheel_input import WheelInput
from .wheel_actions import WheelActions
class ActionBuilder:
def __init__(self, driver, mouse=None, wheel=None, keyboard=None, duration=250) -> None:
if not mouse:
mouse = PointerInput(interaction.POINTER_MOUSE, "mouse")
if not keyboard:
keyboard = KeyInput(interaction.KEY)
if not wheel:
wheel = WheelInput(interaction.WHEEL)
self.devices = [mouse, keyboard, wheel]
self._key_action = KeyActions(keyboard)
self._pointer_action = PointerActions(mouse, duration=duration)
self._wheel_action = WheelActions(wheel)
self.driver = driver
def get_device_with(self, name) -> Union["WheelInput", "PointerInput", "KeyInput"]:
return next(filter(lambda x: x == name, self.devices), None)
@property
def pointer_inputs(self) -> List[PointerInput]:
return [device for device in self.devices if device.type == interaction.POINTER]
@property
def key_inputs(self) -> List[KeyInput]:
return [device for device in self.devices if device.type == interaction.KEY]
@property
def key_action(self) -> KeyActions:
return self._key_action
@property
def pointer_action(self) -> PointerActions:
return self._pointer_action
@property
def wheel_action(self) -> WheelActions:
return self._wheel_action
def add_key_input(self, name) -> KeyInput:
new_input = KeyInput(name)
self._add_input(new_input)
return new_input
def add_pointer_input(self, kind, name) -> PointerInput:
new_input = PointerInput(kind, name)
self._add_input(new_input)
return new_input
def add_wheel_input(self, name) -> WheelInput:
new_input = WheelInput(name)
self._add_input(new_input)
return new_input
def perform(self) -> None:
enc = {"actions": []}
for device in self.devices:
encoded = device.encode()
if encoded['actions']:
enc["actions"].append(encoded)
device.actions = []
self.driver.execute(Command.W3C_ACTIONS, enc)
def clear_actions(self) -> None:
"""
Clears actions that are already stored on the remote end
"""
self.driver.execute(Command.W3C_CLEAR_ACTIONS)
def _add_input(self, input) -> None:
self.devices.append(input)

@ -0,0 +1,43 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import uuid
class InputDevice:
"""
Describes the input device being used for the action.
"""
def __init__(self, name=None):
if not name:
self.name = uuid.uuid4()
else:
self.name = name
self.actions = []
def add_action(self, action):
"""
"""
self.actions.append(action)
def clear_actions(self):
self.actions = []
def create_pause(self, duration=0):
pass

@ -0,0 +1,51 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
KEY = "key"
POINTER = "pointer"
NONE = "none"
WHEEL = "wheel"
SOURCE_TYPES = {KEY, POINTER, NONE}
POINTER_MOUSE = "mouse"
POINTER_TOUCH = "touch"
POINTER_PEN = "pen"
POINTER_KINDS = {POINTER_MOUSE, POINTER_TOUCH, POINTER_PEN}
class Interaction:
PAUSE = "pause"
def __init__(self, source):
self.source = source
class Pause(Interaction):
def __init__(self, source, duration=0):
super(Interaction, self).__init__()
self.source = source
self.duration = duration
def encode(self):
return {
"type": self.PAUSE,
"duration": int(self.duration * 1000)
}

@ -0,0 +1,50 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from .interaction import Interaction, KEY
from .key_input import KeyInput
from ..utils import keys_to_typing
class KeyActions(Interaction):
def __init__(self, source=None):
if not source:
source = KeyInput(KEY)
self.source = source
super().__init__(source)
def key_down(self, letter):
return self._key_action("create_key_down", letter)
def key_up(self, letter):
return self._key_action("create_key_up", letter)
def pause(self, duration=0):
return self._key_action("create_pause", duration)
def send_keys(self, text):
if not isinstance(text, list):
text = keys_to_typing(text)
for letter in text:
self.key_down(letter)
self.key_up(letter)
return self
def _key_action(self, action, letter):
meth = getattr(self.source, action)
meth(letter)
return self

@ -0,0 +1,51 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from . import interaction
from .input_device import InputDevice
from .interaction import (Interaction,
Pause)
class KeyInput(InputDevice):
def __init__(self, name) -> None:
super().__init__()
self.name = name
self.type = interaction.KEY
def encode(self) -> dict:
return {"type": self.type, "id": self.name, "actions": [acts.encode() for acts in self.actions]}
def create_key_down(self, key) -> None:
self.add_action(TypingInteraction(self, "keyDown", key))
def create_key_up(self, key) -> None:
self.add_action(TypingInteraction(self, "keyUp", key))
def create_pause(self, pause_duration=0) -> None:
self.add_action(Pause(self, pause_duration))
class TypingInteraction(Interaction):
def __init__(self, source, type_, key) -> None:
super().__init__(source)
self.type = type_
self.key = key
def encode(self) -> dict:
return {"type": self.type, "value": self.key}

@ -0,0 +1,25 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
class MouseButton:
LEFT = 0
MIDDLE = 1
RIGHT = 2
BACK = 3
FORWARD = 4

@ -0,0 +1,121 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.remote.webelement import WebElement
from . import interaction
from .interaction import Interaction
from .mouse_button import MouseButton
from .pointer_input import PointerInput
class PointerActions(Interaction):
def __init__(self, source=None, duration=250):
"""
Args:
- source: PointerInput instance
- duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in source
"""
if not source:
source = PointerInput(interaction.POINTER_MOUSE, "mouse")
self.source = source
self._duration = duration
super().__init__(source)
def pointer_down(self, button=MouseButton.LEFT, width=None, height=None, pressure=None,
tangential_pressure=None, tilt_x=None, tilt_y=None, twist=None,
altitude_angle=None, azimuth_angle=None):
self._button_action("create_pointer_down", button=button, width=width, height=height,
pressure=pressure, tangential_pressure=tangential_pressure,
tilt_x=tilt_x, tilt_y=tilt_y, twist=twist,
altitude_angle=altitude_angle, azimuth_angle=azimuth_angle)
return self
def pointer_up(self, button=MouseButton.LEFT):
self._button_action("create_pointer_up", button=button)
return self
def move_to(self, element, x=0, y=0, width=None, height=None, pressure=None,
tangential_pressure=None, tilt_x=None, tilt_y=None, twist=None,
altitude_angle=None, azimuth_angle=None):
if not isinstance(element, WebElement):
raise AttributeError("move_to requires a WebElement")
self.source.create_pointer_move(origin=element, duration=self._duration, x=int(x), y=int(y),
width=width, height=height, pressure=pressure,
tangential_pressure=tangential_pressure,
tilt_x=tilt_x, tilt_y=tilt_y, twist=twist,
altitude_angle=altitude_angle, azimuth_angle=azimuth_angle)
return self
def move_by(self, x, y, width=None, height=None, pressure=None,
tangential_pressure=None, tilt_x=None, tilt_y=None, twist=None,
altitude_angle=None, azimuth_angle=None):
self.source.create_pointer_move(origin=interaction.POINTER, duration=self._duration, x=int(x), y=int(y),
width=width, height=height, pressure=pressure,
tangential_pressure=tangential_pressure,
tilt_x=tilt_x, tilt_y=tilt_y, twist=twist,
altitude_angle=altitude_angle, azimuth_angle=azimuth_angle)
return self
def move_to_location(self, x, y, width=None, height=None, pressure=None,
tangential_pressure=None, tilt_x=None, tilt_y=None, twist=None,
altitude_angle=None, azimuth_angle=None):
self.source.create_pointer_move(origin='viewport', duration=self._duration, x=int(x), y=int(y),
width=width, height=height, pressure=pressure,
tangential_pressure=tangential_pressure,
tilt_x=tilt_x, tilt_y=tilt_y, twist=twist,
altitude_angle=altitude_angle, azimuth_angle=azimuth_angle)
return self
def click(self, element=None, button=MouseButton.LEFT):
if element:
self.move_to(element)
self.pointer_down(button)
self.pointer_up(button)
return self
def context_click(self, element=None):
return self.click(element=element, button=MouseButton.RIGHT)
def click_and_hold(self, element=None, button=MouseButton.LEFT):
if element:
self.move_to(element)
self.pointer_down(button=button)
return self
def release(self, button=MouseButton.LEFT):
self.pointer_up(button=button)
return self
def double_click(self, element=None):
if element:
self.move_to(element)
self.pointer_down(MouseButton.LEFT)
self.pointer_up(MouseButton.LEFT)
self.pointer_down(MouseButton.LEFT)
self.pointer_up(MouseButton.LEFT)
return self
def pause(self, duration=0):
self.source.create_pause(duration)
return self
def _button_action(self, action, **kwargs):
meth = getattr(self.source, action)
meth(**kwargs)
return self

@ -0,0 +1,79 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from .input_device import InputDevice
from .interaction import POINTER, POINTER_KINDS
from selenium.common.exceptions import InvalidArgumentException
from selenium.webdriver.remote.webelement import WebElement
class PointerInput(InputDevice):
DEFAULT_MOVE_DURATION = 250
def __init__(self, kind, name):
super().__init__()
if kind not in POINTER_KINDS:
raise InvalidArgumentException("Invalid PointerInput kind '%s'" % kind)
self.type = POINTER
self.kind = kind
self.name = name
def create_pointer_move(self, duration=DEFAULT_MOVE_DURATION, x=0, y=0, origin=None, **kwargs):
action = dict(type="pointerMove", duration=duration)
action["x"] = x
action["y"] = y
action.update(**kwargs)
if isinstance(origin, WebElement):
action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
elif origin:
action["origin"] = origin
self.add_action(self._convert_keys(action))
def create_pointer_down(self, **kwargs):
data = dict(type="pointerDown", duration=0)
data.update(**kwargs)
self.add_action(self._convert_keys(data))
def create_pointer_up(self, button):
self.add_action({"type": "pointerUp", "duration": 0, "button": button})
def create_pointer_cancel(self):
self.add_action({"type": "pointerCancel"})
def create_pause(self, pause_duration):
self.add_action({"type": "pause", "duration": int(pause_duration * 1000)})
def encode(self):
return {"type": self.type,
"parameters": {"pointerType": self.kind},
"id": self.name,
"actions": [acts for acts in self.actions]}
def _convert_keys(self, actions):
out = {}
for k in actions.keys():
if actions[k] is None:
continue
if k == "x" or k == "y":
out[k] = int(actions[k])
continue
splits = k.split('_')
new_key = splits[0] + ''.join(v.title() for v in splits[1:])
out[new_key] = actions[k]
return out

@ -0,0 +1,34 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from .wheel_input import WheelInput
from .interaction import Interaction
class WheelActions(Interaction):
def __init__(self, source: WheelInput = None):
if not source:
source = WheelInput("wheel")
super().__init__(source)
def pause(self, duration=0):
self.source.create_pause(duration)
return self
def scroll(self, x=0, y=0, delta_x=0, delta_y=0, duration=0, origin="viewport"):
self.source.create_scroll(x, y, delta_x, delta_y, duration, origin)
return self

@ -0,0 +1,74 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from . import interaction
from .input_device import InputDevice
from typing import Union
from selenium.webdriver.remote.webelement import WebElement
class ScrollOrigin:
def __init__(self, origin: Union[str, WebElement], x_offset: int, y_offset: int) -> None:
self._origin = origin
self._x_offset = x_offset
self._y_offset = y_offset
@classmethod
def from_element(cls, element: WebElement, x_offset: int = 0, y_offset: int = 0):
return cls(element, x_offset, y_offset)
@classmethod
def from_viewport(cls, x_offset: int = 0, y_offset: int = 0):
return cls('viewport', x_offset, y_offset)
@property
def origin(self) -> Union[str, WebElement]:
return self._origin
@property
def x_offset(self) -> int:
return self._x_offset
@property
def y_offset(self) -> int:
return self._y_offset
class WheelInput(InputDevice):
def __init__(self, name) -> None:
super().__init__(name=name)
self.name = name
self.type = interaction.WHEEL
def encode(self) -> dict:
return {"type": self.type,
"id": self.name,
"actions": [acts for acts in self.actions]}
def create_scroll(self, x: int, y: int, delta_x: int,
delta_y: int, duration: int, origin) -> None:
if isinstance(origin, WebElement):
origin = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
self.add_action({"type": "scroll", "x": x, "y": y, "deltaX": delta_x,
"deltaY": delta_y, "duration": duration,
"origin": origin})
def create_pause(self, pause_duration: Union[int, float]) -> None:
self.add_action(
{"type": "pause", "duration": int(pause_duration * 1000)})

@ -0,0 +1,90 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
The Alert implementation.
"""
from selenium.webdriver.common.utils import keys_to_typing
from selenium.webdriver.remote.command import Command
class Alert:
"""
Allows to work with alerts.
Use this class to interact with alert prompts. It contains methods for dismissing,
accepting, inputting, and getting text from alert prompts.
Accepting / Dismissing alert prompts::
Alert(driver).accept()
Alert(driver).dismiss()
Inputting a value into an alert prompt:
name_prompt = Alert(driver)
name_prompt.send_keys("Willian Shakesphere")
name_prompt.accept()
Reading a the text of a prompt for verification:
alert_text = Alert(driver).text
self.assertEqual("Do you wish to quit?", alert_text)
"""
def __init__(self, driver):
"""
Creates a new Alert.
:Args:
- driver: The WebDriver instance which performs user actions.
"""
self.driver = driver
@property
def text(self):
"""
Gets the text of the Alert.
"""
return self.driver.execute(Command.W3C_GET_ALERT_TEXT)["value"]
def dismiss(self):
"""
Dismisses the alert available.
"""
self.driver.execute(Command.W3C_DISMISS_ALERT)
def accept(self):
"""
Accepts the alert available.
Usage::
Alert(driver).accept() # Confirm a alert dialog.
"""
self.driver.execute(Command.W3C_ACCEPT_ALERT)
def send_keys(self, keysToSend):
"""
Send Keys to the Alert.
:Args:
- keysToSend: The text to be sent to Alert.
"""
self.driver.execute(Command.W3C_SET_ALERT_VALUE, {'value': keys_to_typing(keysToSend), 'text': keysToSend})

@ -0,0 +1,16 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

@ -0,0 +1,501 @@
# The MIT License(MIT)
#
# Copyright(c) 2018 Hyperion Gray
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# This code comes from https://github.com/HyperionGray/trio-chrome-devtools-protocol/tree/master/trio_cdp
# flake8: noqa
from trio_websocket import (
ConnectionClosed as WsConnectionClosed,
connect_websocket_url,
)
import trio
from collections import defaultdict
from contextlib import (contextmanager, asynccontextmanager)
from dataclasses import dataclass
import contextvars
import importlib
import itertools
import json
import logging
import pathlib
import typing
logger = logging.getLogger('trio_cdp')
T = typing.TypeVar('T')
MAX_WS_MESSAGE_SIZE = 2**24
devtools = None
version = None
def import_devtools(ver):
"""
Attempt to load the current latest available devtools into the module
cache for use later.
"""
global devtools
global version
version = ver
base = "selenium.webdriver.common.devtools.v"
try:
devtools = importlib.import_module(f"{base}{ver}")
return devtools
except ModuleNotFoundError:
# Attempt to parse and load the 'most recent' devtools module. This is likely
# because cdp has been updated but selenium python has not been released yet.
devtools_path = pathlib.Path(__file__).parents[1].joinpath("devtools")
versions = tuple(f.name for f in devtools_path.iterdir() if f.is_dir())
latest = max(int(x[1:]) for x in versions)
selenium_logger = logging.getLogger(__name__)
selenium_logger.debug(f"Falling back to loading `devtools`: v{latest}")
devtools = importlib.import_module(f"{base}{latest}")
return devtools
_connection_context: contextvars.ContextVar = contextvars.ContextVar('connection_context')
_session_context: contextvars.ContextVar = contextvars.ContextVar('session_context')
def get_connection_context(fn_name):
'''
Look up the current connection. If there is no current connection, raise a
``RuntimeError`` with a helpful message.
'''
try:
return _connection_context.get()
except LookupError:
raise RuntimeError(f'{fn_name}() must be called in a connection context.')
def get_session_context(fn_name):
'''
Look up the current session. If there is no current session, raise a
``RuntimeError`` with a helpful message.
'''
try:
return _session_context.get()
except LookupError:
raise RuntimeError(f'{fn_name}() must be called in a session context.')
@contextmanager
def connection_context(connection):
''' This context manager installs ``connection`` as the session context for the current
Trio task. '''
token = _connection_context.set(connection)
try:
yield
finally:
_connection_context.reset(token)
@contextmanager
def session_context(session):
''' This context manager installs ``session`` as the session context for the current
Trio task. '''
token = _session_context.set(session)
try:
yield
finally:
_session_context.reset(token)
def set_global_connection(connection):
'''
Install ``connection`` in the root context so that it will become the default
connection for all tasks. This is generally not recommended, except it may be
necessary in certain use cases such as running inside Jupyter notebook.
'''
global _connection_context
_connection_context = contextvars.ContextVar('_connection_context',
default=connection)
def set_global_session(session):
'''
Install ``session`` in the root context so that it will become the default
session for all tasks. This is generally not recommended, except it may be
necessary in certain use cases such as running inside Jupyter notebook.
'''
global _session_context
_session_context = contextvars.ContextVar('_session_context', default=session)
class BrowserError(Exception):
''' This exception is raised when the browser's response to a command
indicates that an error occurred. '''
def __init__(self, obj):
self.code = obj['code']
self.message = obj['message']
self.detail = obj.get('data')
def __str__(self):
return 'BrowserError<code={} message={}> {}'.format(self.code,
self.message, self.detail)
class CdpConnectionClosed(WsConnectionClosed):
''' Raised when a public method is called on a closed CDP connection. '''
def __init__(self, reason):
'''
Constructor.
:param reason:
:type reason: wsproto.frame_protocol.CloseReason
'''
self.reason = reason
def __repr__(self):
''' Return representation. '''
return f'{self.__class__.__name__}<{self.reason}>'
class InternalError(Exception):
''' This exception is only raised when there is faulty logic in TrioCDP or
the integration with PyCDP. '''
@dataclass
class CmEventProxy:
''' A proxy object returned by :meth:`CdpBase.wait_for()``. After the
context manager executes, this proxy object will have a value set that
contains the returned event. '''
value: typing.Any = None
class CdpBase:
def __init__(self, ws, session_id, target_id):
self.ws = ws
self.session_id = session_id
self.target_id = target_id
self.channels = defaultdict(set)
self.id_iter = itertools.count()
self.inflight_cmd = dict()
self.inflight_result = dict()
async def execute(self, cmd: typing.Generator[dict, T, typing.Any]) -> T:
'''
Execute a command on the server and wait for the result.
:param cmd: any CDP command
:returns: a CDP result
'''
cmd_id = next(self.id_iter)
cmd_event = trio.Event()
self.inflight_cmd[cmd_id] = cmd, cmd_event
request = next(cmd)
request['id'] = cmd_id
if self.session_id:
request['sessionId'] = self.session_id
request_str = json.dumps(request)
try:
await self.ws.send_message(request_str)
except WsConnectionClosed as wcc:
raise CdpConnectionClosed(wcc.reason) from None
await cmd_event.wait()
response = self.inflight_result.pop(cmd_id)
if isinstance(response, Exception):
raise response
return response
def listen(self, *event_types, buffer_size=10):
''' Return an async iterator that iterates over events matching the
indicated types. '''
sender, receiver = trio.open_memory_channel(buffer_size)
for event_type in event_types:
self.channels[event_type].add(sender)
return receiver
@asynccontextmanager
async def wait_for(self, event_type: typing.Type[T], buffer_size=10) -> \
typing.AsyncGenerator[CmEventProxy, None]:
'''
Wait for an event of the given type and return it.
This is an async context manager, so you should open it inside an async
with block. The block will not exit until the indicated event is
received.
'''
sender, receiver = trio.open_memory_channel(buffer_size)
self.channels[event_type].add(sender)
proxy = CmEventProxy()
yield proxy
async with receiver:
event = await receiver.receive()
proxy.value = event
def _handle_data(self, data):
'''
Handle incoming WebSocket data.
:param dict data: a JSON dictionary
'''
if 'id' in data:
self._handle_cmd_response(data)
else:
self._handle_event(data)
def _handle_cmd_response(self, data):
'''
Handle a response to a command. This will set an event flag that will
return control to the task that called the command.
:param dict data: response as a JSON dictionary
'''
cmd_id = data['id']
try:
cmd, event = self.inflight_cmd.pop(cmd_id)
except KeyError:
logger.warning('Got a message with a command ID that does'
' not exist: {}'.format(data))
return
if 'error' in data:
# If the server reported an error, convert it to an exception and do
# not process the response any further.
self.inflight_result[cmd_id] = BrowserError(data['error'])
else:
# Otherwise, continue the generator to parse the JSON result
# into a CDP object.
try:
response = cmd.send(data['result'])
raise InternalError("The command's generator function "
"did not exit when expected!")
except StopIteration as exit:
return_ = exit.value
self.inflight_result[cmd_id] = return_
event.set()
def _handle_event(self, data):
'''
Handle an event.
:param dict data: event as a JSON dictionary
'''
global devtools
event = devtools.util.parse_json_event(data)
logger.debug('Received event: %s', event)
to_remove = set()
for sender in self.channels[type(event)]:
try:
sender.send_nowait(event)
except trio.WouldBlock:
logger.error('Unable to send event "%r" due to full channel %s',
event, sender)
except trio.BrokenResourceError:
to_remove.add(sender)
if to_remove:
self.channels[type(event)] -= to_remove
class CdpSession(CdpBase):
'''
Contains the state for a CDP session.
Generally you should not instantiate this object yourself; you should call
:meth:`CdpConnection.open_session`.
'''
def __init__(self, ws, session_id, target_id):
'''
Constructor.
:param trio_websocket.WebSocketConnection ws:
:param devtools.target.SessionID session_id:
:param devtools.target.TargetID target_id:
'''
super().__init__(ws, session_id, target_id)
self._dom_enable_count = 0
self._dom_enable_lock = trio.Lock()
self._page_enable_count = 0
self._page_enable_lock = trio.Lock()
@asynccontextmanager
async def dom_enable(self):
'''
A context manager that executes ``dom.enable()`` when it enters and then
calls ``dom.disable()``.
This keeps track of concurrent callers and only disables DOM events when
all callers have exited.
'''
global devtools
async with self._dom_enable_lock:
self._dom_enable_count += 1
if self._dom_enable_count == 1:
await self.execute(devtools.dom.enable())
yield
async with self._dom_enable_lock:
self._dom_enable_count -= 1
if self._dom_enable_count == 0:
await self.execute(devtools.dom.disable())
@asynccontextmanager
async def page_enable(self):
'''
A context manager that executes ``page.enable()`` when it enters and
then calls ``page.disable()`` when it exits.
This keeps track of concurrent callers and only disables page events
when all callers have exited.
'''
global devtools
async with self._page_enable_lock:
self._page_enable_count += 1
if self._page_enable_count == 1:
await self.execute(devtools.page.enable())
yield
async with self._page_enable_lock:
self._page_enable_count -= 1
if self._page_enable_count == 0:
await self.execute(devtools.page.disable())
class CdpConnection(CdpBase, trio.abc.AsyncResource):
'''
Contains the connection state for a Chrome DevTools Protocol server.
CDP can multiplex multiple "sessions" over a single connection. This class
corresponds to the "root" session, i.e. the implicitly created session that
has no session ID. This class is responsible for reading incoming WebSocket
messages and forwarding them to the corresponding session, as well as
handling messages targeted at the root session itself.
You should generally call the :func:`open_cdp()` instead of
instantiating this class directly.
'''
def __init__(self, ws):
'''
Constructor
:param trio_websocket.WebSocketConnection ws:
'''
super().__init__(ws, session_id=None, target_id=None)
self.sessions = dict()
async def aclose(self):
'''
Close the underlying WebSocket connection.
This will cause the reader task to gracefully exit when it tries to read
the next message from the WebSocket. All of the public APIs
(``execute()``, ``listen()``, etc.) will raise
``CdpConnectionClosed`` after the CDP connection is closed.
It is safe to call this multiple times.
'''
await self.ws.aclose()
@asynccontextmanager
async def open_session(self, target_id) -> \
typing.AsyncIterator[CdpSession]:
'''
This context manager opens a session and enables the "simple" style of calling
CDP APIs.
For example, inside a session context, you can call ``await dom.get_document()``
and it will execute on the current session automatically.
'''
session = await self.connect_session(target_id)
with session_context(session):
yield session
async def connect_session(self, target_id) -> 'CdpSession':
'''
Returns a new :class:`CdpSession` connected to the specified target.
'''
global devtools
session_id = await self.execute(devtools.target.attach_to_target(
target_id, True))
session = CdpSession(self.ws, session_id, target_id)
self.sessions[session_id] = session
return session
async def _reader_task(self):
'''
Runs in the background and handles incoming messages: dispatching
responses to commands and events to listeners.
'''
global devtools
while True:
try:
message = await self.ws.get_message()
except WsConnectionClosed:
# If the WebSocket is closed, we don't want to throw an
# exception from the reader task. Instead we will throw
# exceptions from the public API methods, and we can quietly
# exit the reader task here.
break
try:
data = json.loads(message)
except json.JSONDecodeError:
raise BrowserError({
'code': -32700,
'message': 'Client received invalid JSON',
'data': message
})
logger.debug('Received message %r', data)
if 'sessionId' in data:
session_id = devtools.target.SessionID(data['sessionId'])
try:
session = self.sessions[session_id]
except KeyError:
raise BrowserError('Browser sent a message for an invalid '
'session: {!r}'.format(session_id))
session._handle_data(data)
else:
self._handle_data(data)
@asynccontextmanager
async def open_cdp(url) -> typing.AsyncIterator[CdpConnection]:
'''
This async context manager opens a connection to the browser specified by
``url`` before entering the block, then closes the connection when the block
exits.
The context manager also sets the connection as the default connection for the
current task, so that commands like ``await target.get_targets()`` will run on this
connection automatically. If you want to use multiple connections concurrently, it
is recommended to open each on in a separate task.
'''
async with trio.open_nursery() as nursery:
conn = await connect_cdp(nursery, url)
try:
with connection_context(conn):
yield conn
finally:
await conn.aclose()
async def connect_cdp(nursery, url) -> CdpConnection:
'''
Connect to the browser specified by ``url`` and spawn a background task in the
specified nursery.
The ``open_cdp()`` context manager is preferred in most situations. You should only
use this function if you need to specify a custom nursery.
This connection is not automatically closed! You can either use the connection
object as a context manager (``async with conn:``) or else call ``await
conn.aclose()`` on it when you are done with it.
If ``set_context`` is True, then the returned connection will be installed as
the default connection for the current task. This argument is for unusual use cases,
such as running inside of a notebook.
'''
ws = await connect_websocket_url(nursery, url,
max_message_size=MAX_WS_MESSAGE_SIZE)
cdp_conn = CdpConnection(ws)
nursery.start_soon(cdp_conn._reader_task)
return cdp_conn

@ -0,0 +1,25 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from enum import Enum
class Console(Enum):
ALL = "all"
LOG = "log"
ERROR = "error"

@ -0,0 +1,35 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
The By implementation.
"""
class By:
"""
Set of supported locator strategies.
"""
ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"

@ -0,0 +1,109 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
The Desired Capabilities implementation.
"""
class DesiredCapabilities:
"""
Set of default supported desired capabilities.
Use this as a starting point for creating a desired capabilities object for
requesting remote webdrivers for connecting to selenium server or selenium grid.
Usage Example::
from selenium import webdriver
selenium_grid_url = "http://198.0.0.1:4444/wd/hub"
# Create a desired capabilities object as a starting point.
capabilities = DesiredCapabilities.FIREFOX.copy()
capabilities['platform'] = "WINDOWS"
capabilities['version'] = "10"
# Instantiate an instance of Remote WebDriver with the desired capabilities.
driver = webdriver.Remote(desired_capabilities=capabilities,
command_executor=selenium_grid_url)
Note: Always use '.copy()' on the DesiredCapabilities object to avoid the side
effects of altering the Global class instance.
"""
FIREFOX = {
"browserName": "firefox",
"acceptInsecureCerts": True,
"moz:debuggerAddress": True,
}
INTERNETEXPLORER = {
"browserName": "internet explorer",
"platformName": "windows",
}
EDGE = {
"browserName": "MicrosoftEdge",
}
CHROME = {
"browserName": "chrome",
}
SAFARI = {
"browserName": "safari",
"platformName": "mac",
}
HTMLUNIT = {
"browserName": "htmlunit",
"version": "",
"platform": "ANY",
}
HTMLUNITWITHJS = {
"browserName": "htmlunit",
"version": "firefox",
"platform": "ANY",
"javascriptEnabled": True,
}
IPHONE = {
"browserName": "iPhone",
"version": "",
"platform": "mac",
}
IPAD = {
"browserName": "iPad",
"version": "",
"platform": "mac",
}
WEBKITGTK = {
"browserName": "MiniBrowser",
"version": "",
"platform": "ANY",
}
WPEWEBKIT = {
"browserName": "MiniBrowser",
"version": "",
"platform": "ANY",
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save