geventhttpclient 2.3.1__tar.gz → 2.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/MANIFEST.in +1 -2
- {geventhttpclient-2.3.1/geventhttpclient.egg-info → geventhttpclient-2.3.3}/PKG-INFO +1 -1
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/pyproject.toml +5 -6
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/__init__.py +1 -1
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/connectionpool.py +44 -15
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src/geventhttpclient.egg-info}/PKG-INFO +1 -1
- geventhttpclient-2.3.3/src/geventhttpclient.egg-info/SOURCES.txt +40 -0
- geventhttpclient-2.3.3/tests/test_client.py +337 -0
- geventhttpclient-2.3.3/tests/test_headers.py +227 -0
- geventhttpclient-2.3.3/tests/test_httplib.py +73 -0
- geventhttpclient-2.3.3/tests/test_httplib2.py +31 -0
- geventhttpclient-2.3.3/tests/test_keep_alive.py +95 -0
- geventhttpclient-2.3.3/tests/test_network_failures.py +154 -0
- geventhttpclient-2.3.3/tests/test_no_module_ssl.py +36 -0
- geventhttpclient-2.3.3/tests/test_parser.py +148 -0
- geventhttpclient-2.3.3/tests/test_requests.py +14 -0
- geventhttpclient-2.3.3/tests/test_ssl.py +334 -0
- geventhttpclient-2.3.3/tests/test_url.py +164 -0
- geventhttpclient-2.3.3/tests/test_useragent.py +307 -0
- geventhttpclient-2.3.1/geventhttpclient.egg-info/SOURCES.txt +0 -28
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/LICENSE-MIT +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/README.md +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/ext/Python_compat.h +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/ext/_parser.c +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/LICENSE-MIT +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/include/llhttp.h +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/src/api.c +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/src/http.c +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/src/llhttp.c +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/setup.cfg +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/setup.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/api.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/client.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/header.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/httplib.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/httplib2.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/requests.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/response.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/url.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/useragent.py +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient.egg-info/dependency_links.txt +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient.egg-info/requires.txt +0 -0
- {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient.egg-info/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "geventhttpclient"
|
|
8
|
-
version = "2.3.
|
|
8
|
+
version = "2.3.3" # don't forget to update version __init__.py as well
|
|
9
9
|
description = "HTTP client library for gevent"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -55,12 +55,11 @@ download = "https://pypi.org/project/geventhttpclient/#files"
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
[tool.setuptools]
|
|
58
|
+
package-dir = {"" = "src"}
|
|
58
59
|
include-package-data = true
|
|
59
60
|
|
|
60
61
|
[tool.setuptools.packages.find]
|
|
61
|
-
where = [""]
|
|
62
|
-
include = ["geventhttpclient"]
|
|
63
|
-
exclude = ["*.tests", "*.tests.*", "tests.*", "tests"]
|
|
62
|
+
where = ["src"]
|
|
64
63
|
|
|
65
64
|
[tool.setuptools.package-data]
|
|
66
65
|
"*" = ["README.md", "release.md", "LICENSE-MIT"]
|
|
@@ -94,8 +93,8 @@ commands =
|
|
|
94
93
|
# find . -name '*.pyc' -delete
|
|
95
94
|
python -m build
|
|
96
95
|
pip install -r requirements-dev.txt
|
|
97
|
-
pip install
|
|
98
|
-
pytest
|
|
96
|
+
pip install .
|
|
97
|
+
pytest
|
|
99
98
|
"""
|
|
100
99
|
|
|
101
100
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# package
|
|
2
2
|
|
|
3
|
-
__version__ = "2.3.
|
|
3
|
+
__version__ = "2.3.3" # don't forget to update version in pyproject.toml as well
|
|
4
4
|
|
|
5
5
|
from geventhttpclient.api import delete, get, head, options, patch, post, put, request
|
|
6
6
|
from geventhttpclient.client import HTTPClient
|
|
@@ -184,18 +184,6 @@ class ConnectionPool:
|
|
|
184
184
|
self._semaphore.release()
|
|
185
185
|
|
|
186
186
|
|
|
187
|
-
def init_ssl_context(ssl_context_factory, ca_certs, check_hostname=True):
|
|
188
|
-
try:
|
|
189
|
-
ssl_context = ssl_context_factory(cafile=ca_certs)
|
|
190
|
-
except TypeError:
|
|
191
|
-
ssl_context = ssl_context_factory()
|
|
192
|
-
ssl_context.load_verify_locations(cafile=ca_certs)
|
|
193
|
-
ssl_context.check_hostname = check_hostname
|
|
194
|
-
if check_hostname:
|
|
195
|
-
ssl_context.verify_mode = gevent.ssl.CERT_REQUIRED
|
|
196
|
-
return ssl_context
|
|
197
|
-
|
|
198
|
-
|
|
199
187
|
try:
|
|
200
188
|
from ssl import PROTOCOL_TLS_CLIENT
|
|
201
189
|
|
|
@@ -210,6 +198,45 @@ except ImportError:
|
|
|
210
198
|
pass
|
|
211
199
|
else:
|
|
212
200
|
|
|
201
|
+
def init_ssl_context(
|
|
202
|
+
ssl_context_factory, ca_certs, check_hostname=True, ssl_options=None
|
|
203
|
+
) -> gevent.ssl.SSLContext:
|
|
204
|
+
"""
|
|
205
|
+
Initializes an SSL context with additional SSL options.
|
|
206
|
+
|
|
207
|
+
:param ssl_context_factory: Callable to create an SSL context
|
|
208
|
+
:param ca_certs: Path to CA certificates file
|
|
209
|
+
:param check_hostname: Whether to enable hostname checking
|
|
210
|
+
:param ssl_options: Optional dictionary of additional SSL options
|
|
211
|
+
:return: Configured SSLContext instance
|
|
212
|
+
"""
|
|
213
|
+
ssl_options = ssl_options or {}
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
ssl_context = ssl_context_factory(cafile=ca_certs)
|
|
217
|
+
except TypeError:
|
|
218
|
+
ssl_context = ssl_context_factory()
|
|
219
|
+
ssl_context.load_verify_locations(cafile=ca_certs)
|
|
220
|
+
|
|
221
|
+
ssl_context.check_hostname = check_hostname
|
|
222
|
+
if check_hostname:
|
|
223
|
+
ssl_context.verify_mode = gevent.ssl.CERT_REQUIRED
|
|
224
|
+
|
|
225
|
+
if "certfile" in ssl_options and "keyfile" in ssl_options:
|
|
226
|
+
ssl_context.load_cert_chain(
|
|
227
|
+
certfile=ssl_options["certfile"], keyfile=ssl_options["keyfile"]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if "ciphers" in ssl_options:
|
|
231
|
+
ssl_context.set_ciphers(ssl_options["ciphers"])
|
|
232
|
+
|
|
233
|
+
# Apply additional SSL options (e.g., options, verify_flags)
|
|
234
|
+
for option in ["options", "verify_flags"]:
|
|
235
|
+
if option in ssl_options:
|
|
236
|
+
setattr(ssl_context, option, ssl_options[option])
|
|
237
|
+
|
|
238
|
+
return ssl_context
|
|
239
|
+
|
|
213
240
|
class SSLConnectionPool(ConnectionPool):
|
|
214
241
|
"""SSLConnectionPool creates connections wrapped with SSL/TLS.
|
|
215
242
|
|
|
@@ -249,6 +276,7 @@ else:
|
|
|
249
276
|
ssl_context_factory,
|
|
250
277
|
self.ssl_options["ca_certs"],
|
|
251
278
|
check_hostname=not self.insecure,
|
|
279
|
+
ssl_options=ssl_options,
|
|
252
280
|
)
|
|
253
281
|
else:
|
|
254
282
|
self.ssl_context = None
|
|
@@ -259,7 +287,8 @@ else:
|
|
|
259
287
|
sock = super()._connect_socket(sock, address)
|
|
260
288
|
|
|
261
289
|
if self.ssl_context is None:
|
|
290
|
+
# create_default_context not available
|
|
262
291
|
return gevent.ssl.wrap_socket(sock, **self.ssl_options)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
292
|
+
|
|
293
|
+
server_hostname = self.ssl_options.get("server_hostname", self._request_host)
|
|
294
|
+
return self.ssl_context.wrap_socket(sock, server_hostname=server_hostname)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
LICENSE-MIT
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
ext/Python_compat.h
|
|
7
|
+
ext/_parser.c
|
|
8
|
+
llhttp/LICENSE-MIT
|
|
9
|
+
llhttp/include/llhttp.h
|
|
10
|
+
llhttp/src/api.c
|
|
11
|
+
llhttp/src/http.c
|
|
12
|
+
llhttp/src/llhttp.c
|
|
13
|
+
src/geventhttpclient/__init__.py
|
|
14
|
+
src/geventhttpclient/api.py
|
|
15
|
+
src/geventhttpclient/client.py
|
|
16
|
+
src/geventhttpclient/connectionpool.py
|
|
17
|
+
src/geventhttpclient/header.py
|
|
18
|
+
src/geventhttpclient/httplib.py
|
|
19
|
+
src/geventhttpclient/httplib2.py
|
|
20
|
+
src/geventhttpclient/requests.py
|
|
21
|
+
src/geventhttpclient/response.py
|
|
22
|
+
src/geventhttpclient/url.py
|
|
23
|
+
src/geventhttpclient/useragent.py
|
|
24
|
+
src/geventhttpclient.egg-info/PKG-INFO
|
|
25
|
+
src/geventhttpclient.egg-info/SOURCES.txt
|
|
26
|
+
src/geventhttpclient.egg-info/dependency_links.txt
|
|
27
|
+
src/geventhttpclient.egg-info/requires.txt
|
|
28
|
+
src/geventhttpclient.egg-info/top_level.txt
|
|
29
|
+
tests/test_client.py
|
|
30
|
+
tests/test_headers.py
|
|
31
|
+
tests/test_httplib.py
|
|
32
|
+
tests/test_httplib2.py
|
|
33
|
+
tests/test_keep_alive.py
|
|
34
|
+
tests/test_network_failures.py
|
|
35
|
+
tests/test_no_module_ssl.py
|
|
36
|
+
tests/test_parser.py
|
|
37
|
+
tests/test_requests.py
|
|
38
|
+
tests/test_ssl.py
|
|
39
|
+
tests/test_url.py
|
|
40
|
+
tests/test_useragent.py
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import gevent.pool
|
|
4
|
+
import gevent.queue
|
|
5
|
+
import gevent.server
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from geventhttpclient import __version__
|
|
9
|
+
from geventhttpclient.client import METHOD_GET, HTTPClient
|
|
10
|
+
from tests.common import HTTPBIN_HOST, LISTENER, check_upload, server, wsgiserver
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def httpbin_client(
|
|
14
|
+
host=HTTPBIN_HOST,
|
|
15
|
+
port=None,
|
|
16
|
+
headers=None,
|
|
17
|
+
block_size=HTTPClient.BLOCK_SIZE,
|
|
18
|
+
connection_timeout=30.0,
|
|
19
|
+
network_timeout=30.0,
|
|
20
|
+
disable_ipv6=True,
|
|
21
|
+
concurrency=1,
|
|
22
|
+
ssl=False,
|
|
23
|
+
ssl_options=None,
|
|
24
|
+
ssl_context_factory=None,
|
|
25
|
+
insecure=False,
|
|
26
|
+
proxy_host=None,
|
|
27
|
+
proxy_port=None,
|
|
28
|
+
version=HTTPClient.HTTP_11,
|
|
29
|
+
):
|
|
30
|
+
"""Client factory for httpbin with higher timeout values"""
|
|
31
|
+
|
|
32
|
+
return HTTPClient(
|
|
33
|
+
host,
|
|
34
|
+
port=port,
|
|
35
|
+
headers=headers,
|
|
36
|
+
block_size=block_size,
|
|
37
|
+
connection_timeout=connection_timeout,
|
|
38
|
+
network_timeout=network_timeout,
|
|
39
|
+
disable_ipv6=disable_ipv6,
|
|
40
|
+
concurrency=concurrency,
|
|
41
|
+
ssl=ssl,
|
|
42
|
+
ssl_options=ssl_options,
|
|
43
|
+
ssl_context_factory=ssl_context_factory,
|
|
44
|
+
insecure=insecure,
|
|
45
|
+
proxy_host=proxy_host,
|
|
46
|
+
proxy_port=proxy_port,
|
|
47
|
+
version=version,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def httpbin():
|
|
53
|
+
return httpbin_client()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("request_uri", ["/tp/", "tp/", f"http://{HTTPBIN_HOST}/tp/"])
|
|
57
|
+
def test_build_request(httpbin, request_uri):
|
|
58
|
+
request_ref = f"GET /tp/ HTTP/1.1\r\nUser-Agent: python/gevent-http-client-{__version__}\r\nHost: {HTTPBIN_HOST}\r\n\r\n"
|
|
59
|
+
assert httpbin._build_request(METHOD_GET, request_uri) == request_ref
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_build_request_invalid_host(httpbin):
|
|
63
|
+
with pytest.raises(ValueError):
|
|
64
|
+
httpbin._build_request(METHOD_GET, "http://someunrelatedhost.com/")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize("port", [None, 1234])
|
|
68
|
+
@pytest.mark.parametrize("host", ["localhost", "127.0.0.1", "::1", "[::1]"])
|
|
69
|
+
def test_build_request_host(host, port):
|
|
70
|
+
client = HTTPClient(host, port)
|
|
71
|
+
host_ref = (
|
|
72
|
+
f"host: {f'[{host}]' if host.startswith(':') else host}{f':{port}' if port else ''}\r\n"
|
|
73
|
+
)
|
|
74
|
+
assert host_ref in client._build_request(METHOD_GET, "").lower()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
test_url_client_args = [
|
|
78
|
+
("http://python.org", ("python.org", 80)),
|
|
79
|
+
("http://python.org:333", ("python.org", 333)),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize(["url", "client_args"], test_url_client_args)
|
|
84
|
+
def test_from_url(url, client_args):
|
|
85
|
+
from_url = HTTPClient.from_url(url)
|
|
86
|
+
from_args = HTTPClient(*client_args)
|
|
87
|
+
assert from_args.host == from_url.host
|
|
88
|
+
assert from_args.port == from_url.port
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class StreamTestIterator:
|
|
92
|
+
def __init__(self, sep, count):
|
|
93
|
+
lines = [json.dumps({"index": i, "title": f"this is line {i}"}) for i in range(0, count)]
|
|
94
|
+
self.buf = (sep.join(lines) + sep).encode()
|
|
95
|
+
|
|
96
|
+
def __len__(self):
|
|
97
|
+
return len(self.buf)
|
|
98
|
+
|
|
99
|
+
def __iter__(self):
|
|
100
|
+
self.cursor = 0
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def next(self):
|
|
104
|
+
if self.cursor >= len(self.buf):
|
|
105
|
+
raise StopIteration()
|
|
106
|
+
|
|
107
|
+
gevent.sleep(0)
|
|
108
|
+
pos = self.cursor + 10
|
|
109
|
+
data = self.buf[self.cursor : pos]
|
|
110
|
+
self.cursor = pos
|
|
111
|
+
|
|
112
|
+
return data
|
|
113
|
+
|
|
114
|
+
def __next__(self):
|
|
115
|
+
return self.next()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def readline_iter(sock, addr):
|
|
119
|
+
sock.recv(1024)
|
|
120
|
+
iterator = StreamTestIterator("\n", 100)
|
|
121
|
+
sock.sendall(b"HTTP/1.1 200 Ok\r\nConnection: close\r\n\r\n")
|
|
122
|
+
for block in iterator:
|
|
123
|
+
sock.sendall(block)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_readline():
|
|
127
|
+
with server(readline_iter):
|
|
128
|
+
client = HTTPClient(*LISTENER, block_size=1)
|
|
129
|
+
response = client.get("/")
|
|
130
|
+
lines = []
|
|
131
|
+
while True:
|
|
132
|
+
line = response.readline(b"\n")
|
|
133
|
+
if not line:
|
|
134
|
+
break
|
|
135
|
+
data = json.loads(line[:-1].decode())
|
|
136
|
+
lines.append(data)
|
|
137
|
+
assert len(lines) == 100
|
|
138
|
+
assert [x["index"] for x in lines] == [x for x in range(0, 100)]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def readline_multibyte_sep(sock, addr):
|
|
142
|
+
sock.recv(1024)
|
|
143
|
+
iterator = StreamTestIterator("\r\n", 100)
|
|
144
|
+
sock.sendall(b"HTTP/1.1 200 Ok\r\nConnection: close\r\n\r\n")
|
|
145
|
+
for block in iterator:
|
|
146
|
+
sock.sendall(block)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_readline_multibyte_sep():
|
|
150
|
+
with server(readline_multibyte_sep):
|
|
151
|
+
client = HTTPClient(*LISTENER, block_size=1)
|
|
152
|
+
response = client.get("/")
|
|
153
|
+
lines = []
|
|
154
|
+
while True:
|
|
155
|
+
line = response.readline(b"\r\n")
|
|
156
|
+
if not line:
|
|
157
|
+
break
|
|
158
|
+
data = json.loads(line[:-1].decode())
|
|
159
|
+
lines.append(data)
|
|
160
|
+
assert len(lines) == 100
|
|
161
|
+
assert [x["index"] for x in lines] == [x for x in range(0, 100)]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def readline_multibyte_splitsep(sock, addr):
|
|
165
|
+
sock.recv(1024)
|
|
166
|
+
sock.sendall(b"HTTP/1.1 200 Ok\r\nConnection: close\r\n\r\n")
|
|
167
|
+
sock.sendall(b'{"a": 1}\r')
|
|
168
|
+
gevent.sleep(0)
|
|
169
|
+
sock.sendall(b'\n{"a": 2}\r\n{"a": 3}\r\n')
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_readline_multibyte_splitsep():
|
|
173
|
+
with server(readline_multibyte_splitsep):
|
|
174
|
+
client = HTTPClient(*LISTENER, block_size=1)
|
|
175
|
+
response = client.get("/")
|
|
176
|
+
lines = []
|
|
177
|
+
last_index = 0
|
|
178
|
+
while True:
|
|
179
|
+
line = response.readline(b"\r\n")
|
|
180
|
+
if not line:
|
|
181
|
+
break
|
|
182
|
+
data = json.loads(line[:-2].decode())
|
|
183
|
+
assert data["a"] == last_index + 1
|
|
184
|
+
last_index = data["a"]
|
|
185
|
+
len(lines) == 3
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def internal_server_error(sock, addr):
|
|
189
|
+
sock.recv(1024)
|
|
190
|
+
head = (
|
|
191
|
+
"HTTP/1.1 500 Internal Server Error\r\n"
|
|
192
|
+
"Connection: close\r\n"
|
|
193
|
+
"Content-Type: text/html\r\n"
|
|
194
|
+
"Content-Length: 135\r\n\r\n"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
body = (
|
|
198
|
+
"<html>\n <head>\n <title>Internal Server Error</title>\n "
|
|
199
|
+
"</head>\n <body>\n <h1>Internal Server Error</h1>\n \n "
|
|
200
|
+
"</body>\n</html>\n\n"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
sock.sendall((head + body).encode())
|
|
204
|
+
sock.close()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_internal_server_error():
|
|
208
|
+
with server(internal_server_error):
|
|
209
|
+
client = HTTPClient(*LISTENER)
|
|
210
|
+
response = client.get("/")
|
|
211
|
+
assert not response.should_keep_alive()
|
|
212
|
+
assert response.should_close()
|
|
213
|
+
body = response.read()
|
|
214
|
+
assert len(body) == response.content_length
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_file_post(tmp_path):
|
|
218
|
+
fpath = tmp_path / "tmp_body.txt"
|
|
219
|
+
with open(fpath, "wb") as body:
|
|
220
|
+
body.write(b"123456789")
|
|
221
|
+
with wsgiserver(check_upload(b"123456789", length=9)):
|
|
222
|
+
client = HTTPClient(*LISTENER)
|
|
223
|
+
with open(fpath, "rb") as body:
|
|
224
|
+
client.post("/", body)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_bytes_post():
|
|
228
|
+
with wsgiserver(check_upload(b"12345", length=5)):
|
|
229
|
+
client = HTTPClient(*LISTENER)
|
|
230
|
+
client.post("/", b"12345")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_string_post():
|
|
234
|
+
with wsgiserver(check_upload(b"12345", length=5)):
|
|
235
|
+
client = HTTPClient(*LISTENER)
|
|
236
|
+
client.post("/", "12345")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_unicode_post():
|
|
240
|
+
byte_string = b"\xc8\xb9\xc8\xbc\xc9\x85"
|
|
241
|
+
unicode_string = byte_string.decode("utf-8")
|
|
242
|
+
with wsgiserver(check_upload(byte_string, length=len(byte_string))):
|
|
243
|
+
client = HTTPClient(*LISTENER)
|
|
244
|
+
client.post("/", unicode_string)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# The tests below require online access. We should try to replace them at least
|
|
248
|
+
# partly with local testing solutions and have the online tests as an extra on top.
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@pytest.mark.network
|
|
252
|
+
def test_client_simple(httpbin):
|
|
253
|
+
assert httpbin.port == 80
|
|
254
|
+
response = httpbin.get("/")
|
|
255
|
+
assert response.status_code == 200
|
|
256
|
+
body = response.read()
|
|
257
|
+
assert len(body)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@pytest.mark.network
|
|
261
|
+
def test_client_without_leading_slash(httpbin):
|
|
262
|
+
with httpbin.get("") as response:
|
|
263
|
+
assert response.status_code == 200
|
|
264
|
+
with httpbin.get("base64/test") as response:
|
|
265
|
+
assert response.status_code in (200, 301, 302)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
FIREFOX_USER_AGENT = (
|
|
269
|
+
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0"
|
|
270
|
+
)
|
|
271
|
+
FIREFOX_HEADERS = {"User-Agent": FIREFOX_USER_AGENT}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def check_user_agent_header(ua_header, ua_header_ref):
|
|
275
|
+
"""
|
|
276
|
+
Unlike original httpbin, httpbingo.org sends back a list of header
|
|
277
|
+
strings instead of a simple string. So we need to be a bit flexible
|
|
278
|
+
with the answer.
|
|
279
|
+
"""
|
|
280
|
+
if isinstance(ua_header, list):
|
|
281
|
+
assert len(ua_header) == 1
|
|
282
|
+
assert ua_header[0] == ua_header_ref
|
|
283
|
+
return
|
|
284
|
+
assert ua_header == ua_header_ref
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@pytest.mark.network
|
|
288
|
+
def test_client_with_default_headers():
|
|
289
|
+
httpbin = httpbin_client(headers=FIREFOX_HEADERS)
|
|
290
|
+
response = httpbin.get("/headers")
|
|
291
|
+
assert response.status_code == 200
|
|
292
|
+
sent_headers = json.loads(response.read().decode())["headers"]
|
|
293
|
+
check_user_agent_header(sent_headers["User-Agent"], FIREFOX_USER_AGENT)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.mark.network
|
|
297
|
+
def test_request_with_headers(httpbin):
|
|
298
|
+
response = httpbin.get("/headers", headers=FIREFOX_HEADERS)
|
|
299
|
+
assert response.status_code == 200
|
|
300
|
+
sent_headers = json.loads(response.read().decode())["headers"]
|
|
301
|
+
check_user_agent_header(sent_headers["User-Agent"], FIREFOX_USER_AGENT)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@pytest.mark.network
|
|
305
|
+
def test_response_context_manager(httpbin):
|
|
306
|
+
r = None
|
|
307
|
+
with httpbin.get("/") as response:
|
|
308
|
+
assert response.status_code == 200
|
|
309
|
+
r = response
|
|
310
|
+
assert r._sock is None # released
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@pytest.mark.network
|
|
314
|
+
def test_multi_queries_greenlet_safe():
|
|
315
|
+
httpbin = httpbin_client(concurrency=3)
|
|
316
|
+
group = gevent.pool.Group()
|
|
317
|
+
event = gevent.event.Event()
|
|
318
|
+
|
|
319
|
+
def run(i):
|
|
320
|
+
event.wait()
|
|
321
|
+
response = httpbin.get("/")
|
|
322
|
+
return response, response.read()
|
|
323
|
+
|
|
324
|
+
count = 0
|
|
325
|
+
ok_count = 0
|
|
326
|
+
|
|
327
|
+
gevent.spawn_later(0.2, event.set)
|
|
328
|
+
for response, content in group.imap_unordered(run, range(5)):
|
|
329
|
+
# occasionally remotely hosted httpbin does return server errors :-/
|
|
330
|
+
assert response.status_code in {200, 502, 504}
|
|
331
|
+
if response.status_code == 200:
|
|
332
|
+
ok_count += 1
|
|
333
|
+
assert len(content)
|
|
334
|
+
count += 1
|
|
335
|
+
assert count == 5
|
|
336
|
+
# ensure at least 3 of requests got 200
|
|
337
|
+
assert ok_count >= 3
|