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.
Files changed (43) hide show
  1. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/MANIFEST.in +1 -2
  2. {geventhttpclient-2.3.1/geventhttpclient.egg-info → geventhttpclient-2.3.3}/PKG-INFO +1 -1
  3. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/pyproject.toml +5 -6
  4. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/__init__.py +1 -1
  5. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/connectionpool.py +44 -15
  6. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src/geventhttpclient.egg-info}/PKG-INFO +1 -1
  7. geventhttpclient-2.3.3/src/geventhttpclient.egg-info/SOURCES.txt +40 -0
  8. geventhttpclient-2.3.3/tests/test_client.py +337 -0
  9. geventhttpclient-2.3.3/tests/test_headers.py +227 -0
  10. geventhttpclient-2.3.3/tests/test_httplib.py +73 -0
  11. geventhttpclient-2.3.3/tests/test_httplib2.py +31 -0
  12. geventhttpclient-2.3.3/tests/test_keep_alive.py +95 -0
  13. geventhttpclient-2.3.3/tests/test_network_failures.py +154 -0
  14. geventhttpclient-2.3.3/tests/test_no_module_ssl.py +36 -0
  15. geventhttpclient-2.3.3/tests/test_parser.py +148 -0
  16. geventhttpclient-2.3.3/tests/test_requests.py +14 -0
  17. geventhttpclient-2.3.3/tests/test_ssl.py +334 -0
  18. geventhttpclient-2.3.3/tests/test_url.py +164 -0
  19. geventhttpclient-2.3.3/tests/test_useragent.py +307 -0
  20. geventhttpclient-2.3.1/geventhttpclient.egg-info/SOURCES.txt +0 -28
  21. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/LICENSE-MIT +0 -0
  22. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/README.md +0 -0
  23. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/ext/Python_compat.h +0 -0
  24. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/ext/_parser.c +0 -0
  25. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/LICENSE-MIT +0 -0
  26. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/include/llhttp.h +0 -0
  27. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/src/api.c +0 -0
  28. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/src/http.c +0 -0
  29. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/llhttp/src/llhttp.c +0 -0
  30. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/setup.cfg +0 -0
  31. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3}/setup.py +0 -0
  32. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/api.py +0 -0
  33. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/client.py +0 -0
  34. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/header.py +0 -0
  35. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/httplib.py +0 -0
  36. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/httplib2.py +0 -0
  37. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/requests.py +0 -0
  38. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/response.py +0 -0
  39. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/url.py +0 -0
  40. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient/useragent.py +0 -0
  41. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient.egg-info/dependency_links.txt +0 -0
  42. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient.egg-info/requires.txt +0 -0
  43. {geventhttpclient-2.3.1 → geventhttpclient-2.3.3/src}/geventhttpclient.egg-info/top_level.txt +0 -0
@@ -1,8 +1,7 @@
1
- include geventhttpclient/*.py
1
+ include src/geventhttpclient/*.py
2
2
  include ext/*.c
3
3
  include ext/*.h
4
4
  recursive-include llhttp *.h *.c LICENSE-MIT
5
- recursive-exclude tests *
6
5
  global-exclude __pycache__
7
6
  global-exclude *.py[co]
8
7
  include pyproject.toml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geventhttpclient
3
- Version: 2.3.1
3
+ Version: 2.3.3
4
4
  Summary: HTTP client library for gevent
5
5
  Author-email: Antonin Amand <antonin.amand@gmail.com>
6
6
  License: This software is licensed under the MIT License.
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "geventhttpclient"
8
- version = "2.3.1" # dont forget to update version __init__.py as well
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 -e.
98
- pytest tests
96
+ pip install .
97
+ pytest
99
98
  """
100
99
 
101
100
 
@@ -1,6 +1,6 @@
1
1
  # package
2
2
 
3
- __version__ = "2.3.1" # dont forget to update version in pyproject.toml as well
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
- else:
264
- server_hostname = self.ssl_options.get("server_hostname", self._request_host)
265
- return self.ssl_context.wrap_socket(sock, server_hostname=server_hostname)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geventhttpclient
3
- Version: 2.3.1
3
+ Version: 2.3.3
4
4
  Summary: HTTP client library for gevent
5
5
  Author-email: Antonin Amand <antonin.amand@gmail.com>
6
6
  License: This software is licensed under the MIT License.
@@ -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