geventhttpclient 2.5.0__tar.gz → 2.5.1__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.5.0/src/geventhttpclient.egg-info → geventhttpclient-2.5.1}/PKG-INFO +1 -1
  2. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/pyproject.toml +1 -1
  3. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/__init__.py +1 -1
  4. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/client.py +10 -3
  5. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1/src/geventhttpclient.egg-info}/PKG-INFO +1 -1
  6. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_client.py +55 -0
  7. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/LICENSE-MIT +0 -0
  8. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/MANIFEST.in +0 -0
  9. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/README.md +0 -0
  10. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/ext/Python_compat.h +0 -0
  11. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/ext/_parser.c +0 -0
  12. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/llhttp/LICENSE-MIT +0 -0
  13. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/llhttp/include/llhttp.h +0 -0
  14. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/llhttp/src/api.c +0 -0
  15. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/llhttp/src/http.c +0 -0
  16. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/llhttp/src/llhttp.c +0 -0
  17. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/setup.cfg +0 -0
  18. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/setup.py +0 -0
  19. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/api.py +0 -0
  20. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/connectionpool.py +0 -0
  21. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/header.py +0 -0
  22. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/httplib.py +0 -0
  23. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/httplib2.py +0 -0
  24. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/requests.py +0 -0
  25. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/response.py +0 -0
  26. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/url.py +0 -0
  27. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient/useragent.py +0 -0
  28. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient.egg-info/SOURCES.txt +0 -0
  29. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient.egg-info/dependency_links.txt +0 -0
  30. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient.egg-info/requires.txt +0 -0
  31. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/src/geventhttpclient.egg-info/top_level.txt +0 -0
  32. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_continue.py +0 -0
  33. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_headers.py +0 -0
  34. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_httplib.py +0 -0
  35. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_keep_alive.py +0 -0
  36. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_network_failures.py +0 -0
  37. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_parser.py +0 -0
  38. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_proxy.py +0 -0
  39. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_requests.py +0 -0
  40. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_ssl.py +0 -0
  41. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_url.py +0 -0
  42. {geventhttpclient-2.5.0 → geventhttpclient-2.5.1}/tests/test_useragent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geventhttpclient
3
- Version: 2.5.0
3
+ Version: 2.5.1
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.5.0" # don't forget to update version __init__.py as well
8
+ version = "2.5.1" # 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.11"
@@ -1,6 +1,6 @@
1
1
  # package
2
2
 
3
- __version__ = "2.5.0" # don't forget to update version in pyproject.toml as well
3
+ __version__ = "2.5.1" # 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
@@ -72,6 +72,9 @@ def _uses_chunked_transfer(header_fields: Mapping[str, Any], body: Any) -> bool:
72
72
  and TRANSFER_ENCODING_CHUNKED in str(value).lower()
73
73
  ):
74
74
  return True
75
+ if body is None:
76
+ # No body at all; unknown-length streams are chunked, None is not.
77
+ return False
75
78
  return not isinstance(body, (bytes, bytearray, memoryview)) and _get_body_length(body) is None
76
79
 
77
80
 
@@ -348,7 +351,10 @@ class HTTPClient:
348
351
  :return:
349
352
  """
350
353
 
351
- if isinstance(body, str):
354
+ if body is None:
355
+ # None means "no body", not a body of unknown length.
356
+ body = b""
357
+ elif isinstance(body, str):
352
358
  body = body.encode("utf-8")
353
359
 
354
360
  # the same case-insensitive merge `_build_request` performs before
@@ -425,15 +431,16 @@ class HTTPClient:
425
431
  response._sent_request = request
426
432
  return response
427
433
  if chunked:
428
- sock.sendall(_request)
429
434
  # Note: on retry, file-like/iterable bodies continue
430
435
  # from their current position or are exhausted, same
431
436
  # as with `sendfile` before.
432
437
  if body:
438
+ sock.sendall(_request)
433
439
  for block in _iter_chunked(body, self.block_size):
434
440
  sock.sendall(block)
435
441
  else:
436
- sock.sendall(b"0\r\n\r\n")
442
+ # Single write: a separate small write stalls on Nagle.
443
+ sock.sendall(_request + b"0\r\n\r\n")
437
444
  elif body:
438
445
  if isinstance(body, bytes):
439
446
  sock.sendall(_request + body)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geventhttpclient
3
- Version: 2.5.0
3
+ Version: 2.5.1
4
4
  Summary: HTTP client library for gevent
5
5
  Author-email: Antonin Amand <antonin.amand@gmail.com>
6
6
  License-Expression: MIT
@@ -6,6 +6,7 @@ import sys
6
6
  import gevent.pool
7
7
  import gevent.queue
8
8
  import gevent.server
9
+ import gevent.socket
9
10
  import pytest
10
11
 
11
12
  from geventhttpclient import __version__
@@ -239,6 +240,60 @@ def test_post_chunked_request_empty_body_with_header():
239
240
  assert response.status_code == 200
240
241
 
241
242
 
243
+ def test_request_body_none_is_not_chunked():
244
+ """Regression: body=None means "no body", not an unknown-length body (#248)."""
245
+
246
+ def handler(sock, addr):
247
+ # The request is complete after the headers; chunked would add `0\r\n\r\n`.
248
+ data = b""
249
+ while b"\r\n\r\n" not in data:
250
+ block = sock.recv(4096)
251
+ assert block, "connection closed before request was complete"
252
+ data += block
253
+ header, _, rest = data.partition(b"\r\n\r\n")
254
+ assert header.lower().startswith(b"get / http/1.1")
255
+ assert b"transfer-encoding" not in header.lower()
256
+ assert b"content-length" not in header.lower()
257
+ assert rest == b"", f"unexpected body bytes after headers: {rest!r}"
258
+ sock.sendall(b"HTTP/1.1 200 Ok\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
259
+
260
+ with server(handler):
261
+ client = HTTPClient(*LISTENER)
262
+ response = client.request(METHOD_GET, "/", body=None)
263
+ assert response.status_code == 200
264
+
265
+
266
+ def test_chunked_empty_body_single_write(monkeypatch):
267
+ """An explicit chunked request without body must go out in a single write
268
+ (a second small write stalls on Nagle until the delayed ACK)."""
269
+
270
+ def handler(sock, addr):
271
+ data = b""
272
+ while not data.endswith(b"0\r\n\r\n"):
273
+ block = sock.recv(4096)
274
+ assert block, "connection closed before final chunk"
275
+ data += block
276
+ sock.sendall(b"HTTP/1.1 200 Ok\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
277
+
278
+ client_writes = []
279
+ original_sendall = gevent.socket.socket.sendall
280
+
281
+ def recording_sendall(sock, data):
282
+ if data.startswith(b"POST"):
283
+ client_writes.append(bytes(data))
284
+ return original_sendall(sock, data)
285
+
286
+ monkeypatch.setattr(gevent.socket.socket, "sendall", recording_sendall)
287
+ with server(handler):
288
+ client = HTTPClient(*LISTENER)
289
+ response = client.post("/", body=b"", headers={"Transfer-Encoding": "chunked"})
290
+ assert response.status_code == 200
291
+
292
+ assert len(client_writes) == 1
293
+ assert client_writes[0].startswith(b"POST / HTTP/1.1\r\n")
294
+ assert client_writes[0].endswith(b"\r\n\r\n0\r\n\r\n")
295
+
296
+
242
297
  def test_chunked_request_rejects_http_1_0():
243
298
  client = HTTPClient(*LISTENER, version=HTTPClient.HTTP_10)
244
299
  with pytest.raises(ValueError):