parent
04afab6dde
commit
d72ab349c6
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -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,2 @@
|
|||||||
|
socks
|
||||||
|
sockshandler
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -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 @@
|
|||||||
|
async_generator
|
@ -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
|
File diff suppressed because it is too large
Load Diff
@ -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 @@
|
|||||||
|
__version__ = "1.10"
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -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 @@
|
|||||||
|
h11
|
@ -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 @@
|
|||||||
|
Marker
|
@ -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"]
|
File diff suppressed because it is too large
Load Diff
@ -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 @@
|
|||||||
|
pip
|
@ -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 @@
|
|||||||
|
outcome
|
@ -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 @@
|
|||||||
|
pip
|
@ -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…
Reference in new issue