geventhttpclient 2.3.7__tar.gz → 2.3.9__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 (42) hide show
  1. {geventhttpclient-2.3.7/src/geventhttpclient.egg-info → geventhttpclient-2.3.9}/PKG-INFO +1 -1
  2. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/pyproject.toml +1 -1
  3. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/__init__.py +1 -1
  4. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/connectionpool.py +48 -8
  5. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9/src/geventhttpclient.egg-info}/PKG-INFO +1 -1
  6. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_client.py +31 -0
  7. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/LICENSE-MIT +0 -0
  8. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/MANIFEST.in +0 -0
  9. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/README.md +0 -0
  10. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/ext/Python_compat.h +0 -0
  11. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/ext/_parser.c +0 -0
  12. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/llhttp/LICENSE-MIT +0 -0
  13. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/llhttp/include/llhttp.h +0 -0
  14. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/llhttp/src/api.c +0 -0
  15. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/llhttp/src/http.c +0 -0
  16. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/llhttp/src/llhttp.c +0 -0
  17. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/setup.cfg +0 -0
  18. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/setup.py +0 -0
  19. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/api.py +0 -0
  20. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/client.py +0 -0
  21. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/header.py +0 -0
  22. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/httplib.py +0 -0
  23. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/httplib2.py +0 -0
  24. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/requests.py +0 -0
  25. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/response.py +0 -0
  26. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/url.py +0 -0
  27. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient/useragent.py +0 -0
  28. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient.egg-info/SOURCES.txt +0 -0
  29. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient.egg-info/dependency_links.txt +0 -0
  30. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient.egg-info/requires.txt +0 -0
  31. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/src/geventhttpclient.egg-info/top_level.txt +0 -0
  32. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_headers.py +0 -0
  33. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_httplib.py +0 -0
  34. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_httplib2.py +0 -0
  35. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_keep_alive.py +0 -0
  36. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_network_failures.py +0 -0
  37. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_no_module_ssl.py +0 -0
  38. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_parser.py +0 -0
  39. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_requests.py +0 -0
  40. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_ssl.py +0 -0
  41. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_url.py +0 -0
  42. {geventhttpclient-2.3.7 → geventhttpclient-2.3.9}/tests/test_useragent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geventhttpclient
3
- Version: 2.3.7
3
+ Version: 2.3.9
4
4
  Summary: HTTP client library for gevent
5
5
  Author-email: Antonin Amand <antonin.amand@gmail.com>
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "geventhttpclient"
8
- version = "2.3.7" # don't forget to update version __init__.py as well
8
+ version = "2.3.9" # 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"
@@ -1,6 +1,6 @@
1
1
  # package
2
2
 
3
- __version__ = "2.3.7" # don't forget to update version in pyproject.toml as well
3
+ __version__ = "2.3.9" # 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
@@ -1,4 +1,6 @@
1
1
  import os
2
+ import socket
3
+ import select
2
4
 
3
5
  import gevent.queue
4
6
  import gevent.socket
@@ -125,7 +127,7 @@ class ConnectionPool:
125
127
  if first_error:
126
128
  raise first_error
127
129
  else:
128
- raise RuntimeError(f"Cannot resolve {self._host}:{self._port}")
130
+ raise RuntimeError(f"Cannot resolve {self._connection_host}:{self._connection_port}")
129
131
 
130
132
  def after_connect(self, sock):
131
133
  pass
@@ -149,19 +151,57 @@ class ConnectionPool:
149
151
  if not parts or parts[1] != b"200":
150
152
  raise RuntimeError(f"Error response from Proxy server : {resp}")
151
153
 
154
+ def _is_socket_alive(self, sock):
155
+ """Check if a socket is still connected and alive.
156
+
157
+ Uses select() to check if socket is readable. An idle keep-alive socket
158
+ should NOT be readable.
159
+
160
+ Returns False if connection is closed or broken.
161
+ """
162
+ if sock is None:
163
+ return False
164
+ try:
165
+ # Proactive check: A closed socket has a fileno of -1.
166
+ if sock.fileno() < 0:
167
+ return False
168
+ # If the socket is readable while idle, it's either a FIN or dirty.
169
+ ready_to_read, _, _ = select.select([sock], [], [], 0.0)
170
+ if ready_to_read:
171
+ return False
172
+
173
+ return True
174
+ except (OSError, ValueError, socket.error):
175
+ return False
176
+
152
177
  def get_socket(self):
153
178
  """get a socket from the pool. This blocks until one is available."""
154
179
  self._semaphore.acquire()
155
180
  if self._closed:
156
181
  raise RuntimeError("connection pool closed")
157
- try:
158
- return self._socket_queue.get(block=False)
159
- except gevent.queue.Empty:
182
+
183
+ # Try to get a valid connection from the pool
184
+ while not self._socket_queue.empty():
160
185
  try:
161
- return self._create_socket()
162
- except: # noqa
163
- self._semaphore.release()
164
- raise
186
+ sock = self._socket_queue.get(block=False)
187
+ if self._is_socket_alive(sock):
188
+ # Connection is still alive, return it
189
+ return sock
190
+ else:
191
+ # Connection is dead, close it and try next
192
+ try:
193
+ sock.close()
194
+ except: # noqa
195
+ pass
196
+ except gevent.queue.Empty:
197
+ break
198
+
199
+ # No valid connections in pool, create a new one
200
+ try:
201
+ return self._create_socket()
202
+ except: # noqa
203
+ self._semaphore.release()
204
+ raise
165
205
 
166
206
  def return_socket(self, sock):
167
207
  """return a socket to the pool."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geventhttpclient
3
- Version: 2.3.7
3
+ Version: 2.3.9
4
4
  Summary: HTTP client library for gevent
5
5
  Author-email: Antonin Amand <antonin.amand@gmail.com>
6
6
  License-Expression: MIT
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import socket
2
3
 
3
4
  import gevent.pool
4
5
  import gevent.queue
@@ -7,6 +8,7 @@ import pytest
7
8
 
8
9
  from geventhttpclient import __version__
9
10
  from geventhttpclient.client import METHOD_GET, HTTPClient
11
+ from geventhttpclient.connectionpool import ConnectionPool
10
12
  from tests.common import HTTPBIN_HOST, LISTENER, check_upload, server, wsgiserver
11
13
 
12
14
 
@@ -350,3 +352,32 @@ def test_multi_queries_greenlet_safe():
350
352
  assert count == 5
351
353
  # ensure at least 3 of requests got 200
352
354
  assert ok_count >= 3
355
+
356
+
357
+ class TestIsSocketAlive:
358
+ """Unit tests for ConnectionPool._is_socket_alive()"""
359
+
360
+ def test_closed_socket_returns_false(self):
361
+ sock = socket.socket()
362
+ sock.close()
363
+ pool = ConnectionPool("127.0.0.1", 80, "127.0.0.1", 80)
364
+ assert sock.fileno() < 0
365
+ assert pool._is_socket_alive(sock) is False
366
+
367
+ def test_healthy_socket_returns_true(self):
368
+ s1, s2 = socket.socketpair()
369
+ try:
370
+ pool = ConnectionPool("127.0.0.1", 80, "127.0.0.1", 80)
371
+ assert pool._is_socket_alive(s1) is True
372
+ finally:
373
+ s1.close()
374
+ s2.close()
375
+
376
+ def test_peer_orderly_close_returns_false(self):
377
+ s1, s2 = socket.socketpair()
378
+ try:
379
+ s2.close()
380
+ pool = ConnectionPool("127.0.0.1", 80, "127.0.0.1", 80)
381
+ assert pool._is_socket_alive(s1) is False
382
+ finally:
383
+ s1.close()