http_client_request 0.0.6__tar.gz → 0.0.8__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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: http_client_request
3
- Version: 0.0.6
4
- Summary: http.client request extension.
3
+ Version: 0.0.8
4
+ Summary: ttp.client request extension.
5
5
  Home-page: https://github.com/ChenyangGao/python-modules/tree/main/http_client_request
6
6
  License: MIT
7
7
  Keywords: http.client,request
@@ -22,10 +22,11 @@ Classifier: Topic :: Software Development :: Libraries
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Requires-Dist: http_response (>=0.0.9)
24
24
  Requires-Dist: python-argtools (>=0.0.2)
25
- Requires-Dist: python-cookietools (>=0.1.2)
25
+ Requires-Dist: python-cookietools (>=0.1.3)
26
26
  Requires-Dist: python-dicttools (>=0.0.4)
27
27
  Requires-Dist: python-filewrap (>=0.2.8)
28
28
  Requires-Dist: python-http_request (>=0.1.4)
29
+ Requires-Dist: python-property (>=0.0.3)
29
30
  Requires-Dist: python-undefined (>=0.0.3)
30
31
  Requires-Dist: urllib3
31
32
  Requires-Dist: yarl
@@ -4,9 +4,15 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  __author__ = "ChenyangGao <https://chenyanggao.github.io>"
7
- __version__ = (0, 0, 6)
8
- __all__ = ["HTTPConnection", "HTTPSConnection", "HTTPResponse", "ConnectionPool", "request"]
7
+ __version__ = (0, 0, 8)
8
+ __all__ = [
9
+ "CONNECTION_POOL", "HTTPConnection", "HTTPSConnection", "HTTPResponse",
10
+ "ConnectionPool", "request",
11
+ ]
9
12
 
13
+ import socket
14
+
15
+ from array import array
10
16
  from collections import defaultdict, deque, UserString
11
17
  from collections.abc import Buffer, Callable, Iterable, Mapping
12
18
  from http.client import (
@@ -15,9 +21,12 @@ from http.client import (
15
21
  )
16
22
  from http.cookiejar import CookieJar
17
23
  from http.cookies import BaseCookie
24
+ from io import BufferedReader
18
25
  from inspect import signature
19
26
  from os import PathLike
20
- from socket import socket
27
+ from select import select
28
+ from socket import socket as Socket
29
+ from ssl import SSLWantReadError
21
30
  from types import EllipsisType
22
31
  from typing import cast, overload, Any, Final, Literal
23
32
  from urllib.error import HTTPError
@@ -30,6 +39,7 @@ from dicttools import get_all_items
30
39
  from filewrap import SupportsRead
31
40
  from http_request import normalize_request_args, SupportsGeturl
32
41
  from http_response import decompress_response, parse_response
42
+ from property import funcproperty
33
43
  from urllib3 import HTTPResponse as Urllib3HTTPResponse, HTTPHeaderDict
34
44
  from undefined import undefined, Undefined
35
45
  from yarl import URL
@@ -41,15 +51,106 @@ HTTP_CONNECTION_KWARGS: Final = signature(BaseHTTPConnection).parameters.keys()
41
51
  HTTPS_CONNECTION_KWARGS: Final = signature(BaseHTTPSConnection).parameters.keys()
42
52
 
43
53
 
54
+ def get_host_pair(url: None | str, /) -> None | tuple[str, None | int]:
55
+ if not url:
56
+ return None
57
+ if not url.startswith(("http://", "https://")):
58
+ url = "http://" + url
59
+ urlp = urlsplit(url)
60
+ return urlp.hostname or "localhost", urlp.port
61
+
62
+
63
+ def is_ipv6(host: str, /) -> bool:
64
+ from ipaddress import _BaseV6, AddressValueError
65
+ try:
66
+ _BaseV6._ip_int_from_string(host) # type: ignore
67
+ return True
68
+ except AddressValueError:
69
+ return False
70
+
71
+
72
+ def sock_buf_readable(sock: Socket, /) -> bool:
73
+ rlist, *_ = select([sock], (), (), 0)
74
+ return bool(rlist)
75
+
76
+
77
+ try:
78
+ from fcntl import ioctl
79
+ from termios import FIONREAD
80
+
81
+ def sock_bufsize(sock, /) -> int:
82
+ sock_size = array("i", [0])
83
+ ioctl(sock, FIONREAD, sock_size)
84
+ return sock_size[0]
85
+ except ImportError:
86
+ from ctypes import byref, c_ulong, WinDLL # type: ignore
87
+ ws2_32 = WinDLL("ws2_32")
88
+ def sock_bufsize(sock, /) -> int:
89
+ FIONREAD = 0x4004667f
90
+ b = c_ulong(0)
91
+ ws2_32.ioctlsocket(sock.fileno(), FIONREAD, byref(b))
92
+ return b.value
93
+
94
+
44
95
  class HTTPResponse(BaseHTTPResponse):
96
+ _pos: int = 0
97
+ method: str
45
98
  pool: None | ConnectionPool = None
46
99
  connection: None | HTTPConnection | HTTPSConnection = None
47
100
 
48
101
  def __del__(self, /):
49
102
  self.close()
50
103
 
104
+ @funcproperty
105
+ def _fp(self, /) -> BufferedReader:
106
+ return self.fp
107
+
108
+ @property
109
+ def buffer_size(self, /) -> int:
110
+ fp = self._fp
111
+ sock = fp.raw._sock
112
+ sock.setblocking(False)
113
+ try:
114
+ cache_size = len(fp.peek() or b"")
115
+ while True:
116
+ buffer_size = cache_size + sock_bufsize(sock)
117
+ if cache_size == (cache_size := len(fp.peek() or b"")):
118
+ return buffer_size
119
+ except (BlockingIOError, SSLWantReadError):
120
+ return 0
121
+ finally:
122
+ sock.setblocking(True)
123
+
124
+ @property
125
+ def unbuffer_size(self, /) -> int:
126
+ method = self.__dict__.get("method", "").upper()
127
+ content_length = self.length
128
+ if method == "HEAD" or content_length == 0:
129
+ return 0
130
+ if content_length:
131
+ return content_length - self.tell() - self.buffer_size
132
+ sock = self._fp.raw._sock
133
+ if sock_bufsize(sock) == sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF):
134
+ return -1
135
+ else:
136
+ return 0
137
+
138
+ @property
139
+ def unread_size(self, /) -> int:
140
+ method = self.__dict__.get("method", "").upper()
141
+ content_length = self.length
142
+ if method == "HEAD" or content_length == 0:
143
+ return 0
144
+ sock = self._fp.raw._sock
145
+ if content_length:
146
+ return content_length - self.tell()
147
+ elif sock_bufsize(sock) == sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF):
148
+ return -1
149
+ else:
150
+ return self.buffer_size
151
+
51
152
  def _close_conn(self, /):
52
- fp = self.fp
153
+ fp = self._fp = self.fp
53
154
  setattr(self, "fp", None)
54
155
  try:
55
156
  pool = self.pool
@@ -70,12 +171,44 @@ class HTTPResponse(BaseHTTPResponse):
70
171
  preload_content=False,
71
172
  decode_content=True,
72
173
  original_response=self,
73
- #pool=getattr(self, "pool", None),
74
174
  msg=self.headers,
75
175
  request_method=getattr(self, "method", None),
76
176
  request_url=self.url,
77
177
  )
78
178
 
179
+ def read(self, /, amt=None) -> bytes:
180
+ data = super().read(amt)
181
+ self._pos += len(data)
182
+ return data
183
+
184
+ def read1(self, /, n=-1) -> bytes:
185
+ data = super().read1(n)
186
+ self._pos += len(data)
187
+ return data
188
+
189
+ def readinto(self, /, b) -> int:
190
+ count = super().readinto(b)
191
+ self._pos += count
192
+ return count
193
+
194
+ def readinto1(self, buffer, /) -> int:
195
+ count = super().readinto1(buffer)
196
+ self._pos += count
197
+ return count
198
+
199
+ def readline(self, limit=-1) -> bytes:
200
+ data = super().readline(limit)
201
+ self._pos += len(data)
202
+ return data
203
+
204
+ def readlines(self, hint=-1, /) -> list[bytes]:
205
+ ls = super().readlines(hint)
206
+ self._pos += sum(map(len, ls))
207
+ return ls
208
+
209
+ def tell(self, /) -> int:
210
+ return self._pos
211
+
79
212
 
80
213
  class HTTPConnection(BaseHTTPConnection):
81
214
  response_class = HTTPResponse
@@ -83,9 +216,20 @@ class HTTPConnection(BaseHTTPConnection):
83
216
  def __del__(self, /):
84
217
  self.close()
85
218
 
219
+ @property
220
+ def response(self, /) -> None | HTTPResponse:
221
+ return self._HTTPConnection__response # type: ignore
222
+
86
223
  def getresponse(self, /) -> HTTPResponse:
87
224
  return cast(HTTPResponse, super().getresponse())
88
225
 
226
+ def putrequest(self, /, *args, **kwds):
227
+ while True:
228
+ try:
229
+ return super().putrequest(*args, **kwds)
230
+ except (ConnectionResetError, BrokenPipeError):
231
+ self.connect()
232
+
89
233
 
90
234
  class HTTPSConnection(BaseHTTPSConnection):
91
235
  response_class = HTTPResponse
@@ -93,26 +237,19 @@ class HTTPSConnection(BaseHTTPSConnection):
93
237
  def __del__(self, /):
94
238
  self.close()
95
239
 
240
+ @property
241
+ def response(self, /) -> None | HTTPResponse:
242
+ return self._HTTPConnection__response # type: ignore
243
+
96
244
  def getresponse(self, /) -> HTTPResponse:
97
245
  return cast(HTTPResponse, super().getresponse())
98
246
 
99
-
100
- def get_host_pair(url: None | str, /) -> None | tuple[str, None | int]:
101
- if not url:
102
- return None
103
- if not url.startswith(("http://", "https://")):
104
- url = "http://" + url
105
- urlp = urlsplit(url)
106
- return urlp.hostname or "localhost", urlp.port
107
-
108
-
109
- def is_ipv6(host: str, /) -> bool:
110
- from ipaddress import _BaseV6, AddressValueError
111
- try:
112
- _BaseV6._ip_int_from_string(host) # type: ignore
113
- return True
114
- except AddressValueError:
115
- return False
247
+ def putrequest(self, /, *args, **kwds):
248
+ while True:
249
+ try:
250
+ return super().putrequest(*args, **kwds)
251
+ except (ConnectionResetError, BrokenPipeError):
252
+ self.connect()
116
253
 
117
254
 
118
255
  class ConnectionPool:
@@ -155,21 +292,27 @@ class ConnectionPool:
155
292
  con = dq.popleft()
156
293
  except IndexError:
157
294
  break
295
+ con.timeout = timeout
158
296
  sock = con.sock
297
+ resp = con.response
159
298
  if not sock or getattr(sock, "_closed"):
160
299
  con.connect()
300
+ elif resp and 0 < resp.unbuffer_size <= 1024 * 1024:
301
+ try:
302
+ resp.read()
303
+ except (ConnectionResetError, BrokenPipeError):
304
+ con.connect()
161
305
  else:
162
306
  sock.setblocking(False)
163
307
  try:
164
- if socket.recv(sock, 1):
308
+ if Socket.recv(sock, 1):
165
309
  con.connect()
166
310
  except BlockingIOError:
167
311
  pass
168
- except ConnectionResetError:
312
+ except (ConnectionResetError, BrokenPipeError):
169
313
  con.connect()
170
314
  finally:
171
315
  sock.setblocking(True)
172
- con.timeout = timeout
173
316
  return con
174
317
  if url.scheme == "https":
175
318
  return HTTPSConnection(url.hostname or "localhost", url.port, timeout=timeout)
@@ -286,7 +429,7 @@ def request[T](
286
429
  raise_for_status: bool = True,
287
430
  cookies: None | CookieJar | BaseCookie = None,
288
431
  proxies: None | str | dict[str, str] = None,
289
- pool: None | Undefined | ConnectionPool = undefined,
432
+ pool: None | Undefined | ConnectionPool = undefined,
290
433
  *,
291
434
  parse: None | EllipsisType| bool | Callable[[HTTPResponse, bytes], T] | Callable[[HTTPResponse], T] = None,
292
435
  **request_kwargs,
@@ -350,10 +493,16 @@ def request[T](
350
493
  connection = HTTPSConnection(**dict(get_all_items(request_kwargs, *HTTPS_CONNECTION_KWARGS)))
351
494
  if http_proxy:
352
495
  connection.set_tunnel(*http_proxy)
496
+ else:
497
+ setattr(connection, "_tunnel_host", None)
498
+ setattr(connection, "_tunnel_port", None)
353
499
  else:
354
500
  connection = HTTPConnection(**dict(get_all_items(request_kwargs, *HTTP_CONNECTION_KWARGS)))
355
501
  if https_proxy:
356
502
  connection.set_tunnel(*https_proxy)
503
+ else:
504
+ setattr(connection, "_tunnel_host", None)
505
+ setattr(connection, "_tunnel_port", None)
357
506
  connection.request(
358
507
  method,
359
508
  urlunsplit(urlp._replace(scheme="", netloc="")),
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "http_client_request"
3
- version = "0.0.6"
4
- description = "http.client request extension."
3
+ version = "0.0.8"
4
+ description = "ttp.client request extension."
5
5
  authors = ["ChenyangGao <wosiwujm@gmail.com>"]
6
6
  license = "MIT"
7
7
  readme = "readme.md"
@@ -29,10 +29,11 @@ include = [
29
29
  python = "^3.12"
30
30
  http_response = ">=0.0.9"
31
31
  python-argtools = ">=0.0.2"
32
- python-cookietools = ">=0.1.2"
32
+ python-cookietools = ">=0.1.3"
33
33
  python-dicttools = ">=0.0.4"
34
34
  python-filewrap = ">=0.2.8"
35
35
  python-http_request = ">=0.1.4"
36
+ python-property = ">=0.0.3"
36
37
  python-undefined = ">=0.0.3"
37
38
  urllib3 = "*"
38
39
  yarl = "*"