geventhttpclient 2.3.2__cp313-cp313-win_amd64.whl
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/__init__.py +9 -0
- geventhttpclient/_parser.cp313-win_amd64.pyd +0 -0
- geventhttpclient/api.py +157 -0
- geventhttpclient/client.py +318 -0
- geventhttpclient/connectionpool.py +294 -0
- geventhttpclient/header.py +295 -0
- geventhttpclient/httplib.py +252 -0
- geventhttpclient/httplib2.py +69 -0
- geventhttpclient/requests.py +252 -0
- geventhttpclient/response.py +284 -0
- geventhttpclient/url.py +160 -0
- geventhttpclient/useragent.py +665 -0
- geventhttpclient-2.3.2.dist-info/LICENSE-MIT +22 -0
- geventhttpclient-2.3.2.dist-info/METADATA +248 -0
- geventhttpclient-2.3.2.dist-info/RECORD +17 -0
- geventhttpclient-2.3.2.dist-info/WHEEL +5 -0
- geventhttpclient-2.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# package
|
|
2
|
+
|
|
3
|
+
__version__ = "2.3.2" # don't forget to update version in pyproject.toml as well
|
|
4
|
+
|
|
5
|
+
from geventhttpclient.api import delete, get, head, options, patch, post, put, request
|
|
6
|
+
from geventhttpclient.client import HTTPClient
|
|
7
|
+
from geventhttpclient.requests import Session
|
|
8
|
+
from geventhttpclient.url import URL
|
|
9
|
+
from geventhttpclient.useragent import UserAgent
|
|
Binary file
|
geventhttpclient/api.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module mimics and blatantly borrows with all due respect
|
|
3
|
+
from the excellent and rightfully popular Requests API.
|
|
4
|
+
|
|
5
|
+
Copyright of the original requests project:
|
|
6
|
+
|
|
7
|
+
:copyright: (c) 2012 by Kenneth Reitz.
|
|
8
|
+
:license: Apache2, see LICENSE for more details.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import geventhttpclient.requests
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def request(method, url, **kw):
|
|
15
|
+
"""Constructs and sends a HTTP request. WARNING: Only a subset of the following parameters is currently supported
|
|
16
|
+
|
|
17
|
+
:param method: method for the new request object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
|
|
18
|
+
:param url: URL for the new HTTP Request object.
|
|
19
|
+
:param params: (optional) Dictionary, list of tuples or bytes to send
|
|
20
|
+
in the query string for the HTTP Request.
|
|
21
|
+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
|
22
|
+
object to send in the body of the HTTP Request.
|
|
23
|
+
:param json: (optional) A JSON serializable Python object to send in the body of the HTTP Request.
|
|
24
|
+
:param headers: (optional) Dictionary of HTTP Headers to send with the HTTP Request.
|
|
25
|
+
:param cookies: (optional) Dict or CookieJar object to send with the HTTP Request.
|
|
26
|
+
:param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
|
|
27
|
+
``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
|
|
28
|
+
or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string
|
|
29
|
+
defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
|
|
30
|
+
to add for the file.
|
|
31
|
+
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
|
|
32
|
+
:param timeout: (optional) How many seconds to wait for the server to send data
|
|
33
|
+
before giving up, as a float, or a :ref:`(connect timeout, read
|
|
34
|
+
timeout) <timeouts>` tuple.
|
|
35
|
+
:type timeout: float or tuple
|
|
36
|
+
:param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.
|
|
37
|
+
:type allow_redirects: bool
|
|
38
|
+
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
|
|
39
|
+
:param verify: (optional) Either a boolean, in which case it controls whether we verify
|
|
40
|
+
the server's TLS certificate, or a string, in which case it must be a path
|
|
41
|
+
to a CA bundle to use. Defaults to ``True``.
|
|
42
|
+
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
|
|
43
|
+
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
|
44
|
+
:return: HTTP Response object
|
|
45
|
+
:rtype: HTTP Response
|
|
46
|
+
|
|
47
|
+
Usage::
|
|
48
|
+
|
|
49
|
+
>>> import geventhttpclient
|
|
50
|
+
>>> req = geventhttpclient.request('GET', 'https://httpbingo.org/get')
|
|
51
|
+
>>> req
|
|
52
|
+
<Response [200]>
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# By using the 'with' statement we are sure the session is closed, thus we
|
|
56
|
+
# avoid leaving sockets open which can trigger a ResourceWarning in some
|
|
57
|
+
# cases, and look like a memory leak in others.
|
|
58
|
+
with geventhttpclient.requests.Session() as session:
|
|
59
|
+
return session.request(method=method, url=url, **kw)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get(url, params=None, **kw):
|
|
63
|
+
r"""Sends a GET request.
|
|
64
|
+
|
|
65
|
+
:param url: URL for the new HTTP Request object.
|
|
66
|
+
:param params: (optional) Dictionary, list of tuples or bytes to send
|
|
67
|
+
in the query string for the HTTP Request.
|
|
68
|
+
:param \*\*kw: Optional arguments that ``request`` takes.
|
|
69
|
+
:return: HTTP Response object
|
|
70
|
+
:rtype: requests.Response
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
return request("get", url, params=params, **kw)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def options(url, **kw):
|
|
77
|
+
r"""Sends an OPTIONS request.
|
|
78
|
+
|
|
79
|
+
:param url: URL for the new HTTP Request object.
|
|
80
|
+
:param \*\*kw: Optional arguments that ``request`` takes.
|
|
81
|
+
:return: HTTP Response object
|
|
82
|
+
:rtype: requests.Response
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
return request("options", url, **kw)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def head(url, **kw):
|
|
89
|
+
r"""Sends a HEAD request.
|
|
90
|
+
|
|
91
|
+
:param url: URL for the new HTTP Request object.
|
|
92
|
+
:param \*\*kw: Optional arguments that ``request`` takes. If
|
|
93
|
+
`allow_redirects` is not provided, it will be set to `False` (as
|
|
94
|
+
opposed to the default :meth:`request` behavior).
|
|
95
|
+
:return: HTTP Response object
|
|
96
|
+
:rtype: requests.Response
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
kw.setdefault("allow_redirects", False)
|
|
100
|
+
return request("head", url, **kw)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def post(url, data=None, json=None, **kw):
|
|
104
|
+
r"""Sends a POST request.
|
|
105
|
+
|
|
106
|
+
:param url: URL for the new HTTP Request object.
|
|
107
|
+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
|
108
|
+
object to send in the body of the HTTP Request.
|
|
109
|
+
:param json: (optional) A JSON serializable Python object to send in the body of the HTTP Request.
|
|
110
|
+
:param \*\*kw: Optional arguments that ``request`` takes.
|
|
111
|
+
:return: HTTP Response object
|
|
112
|
+
:rtype: requests.Response
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
return request("post", url, data=data, json=json, **kw)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def put(url, data=None, **kw):
|
|
119
|
+
r"""Sends a PUT request.
|
|
120
|
+
|
|
121
|
+
:param url: URL for the new HTTP Request object.
|
|
122
|
+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
|
123
|
+
object to send in the body of the HTTP Request.
|
|
124
|
+
:param json: (optional) A JSON serializable Python object to send in the body of the HTTP Request.
|
|
125
|
+
:param \*\*kw: Optional arguments that ``request`` takes.
|
|
126
|
+
:return: HTTP Response object
|
|
127
|
+
:rtype: requests.Response
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
return request("put", url, data=data, **kw)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def patch(url, data=None, **kw):
|
|
134
|
+
r"""Sends a PATCH request.
|
|
135
|
+
|
|
136
|
+
:param url: URL for the new HTTP Request object.
|
|
137
|
+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
|
138
|
+
object to send in the body of the HTTP Request.
|
|
139
|
+
:param json: (optional) A JSON serializable Python object to send in the body of the HTTP Request.
|
|
140
|
+
:param \*\*kw: Optional arguments that ``request`` takes.
|
|
141
|
+
:return: HTTP Response object
|
|
142
|
+
:rtype: requests.Response
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
return request("patch", url, data=data, **kw)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def delete(url, **kw):
|
|
149
|
+
r"""Sends a DELETE request.
|
|
150
|
+
|
|
151
|
+
:param url: URL for the new HTTP Request object.
|
|
152
|
+
:param \*\*kw: Optional arguments that ``request`` takes.
|
|
153
|
+
:return: HTTP Response object
|
|
154
|
+
:rtype: requests.Response
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
return request("delete", url, **kw)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import errno
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import gevent.socket
|
|
5
|
+
|
|
6
|
+
from geventhttpclient import __version__
|
|
7
|
+
from geventhttpclient.connectionpool import ConnectionPool
|
|
8
|
+
from geventhttpclient.header import Headers
|
|
9
|
+
from geventhttpclient.response import HTTPConnectionClosed, HTTPSocketPoolResponse
|
|
10
|
+
from geventhttpclient.url import URL
|
|
11
|
+
|
|
12
|
+
CRLF = "\r\n"
|
|
13
|
+
WHITESPACE = " "
|
|
14
|
+
FIELD_VALUE_SEP = ": "
|
|
15
|
+
HOST_PORT_SEP = ":"
|
|
16
|
+
SLASH = "/"
|
|
17
|
+
PROTO_HTTP = "http"
|
|
18
|
+
PROTO_HTTPS = "https"
|
|
19
|
+
HEADER_HOST = "Host"
|
|
20
|
+
HEADER_CONTENT_LENGTH = "Content-Length"
|
|
21
|
+
|
|
22
|
+
METHOD_GET = "GET"
|
|
23
|
+
METHOD_HEAD = "HEAD"
|
|
24
|
+
METHOD_POST = "POST"
|
|
25
|
+
METHOD_PUT = "PUT"
|
|
26
|
+
METHOD_DELETE = "DELETE"
|
|
27
|
+
METHOD_PATCH = "PATCH"
|
|
28
|
+
METHOD_OPTIONS = "OPTIONS"
|
|
29
|
+
METHOD_TRACE = "TRACE"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_body_length(body):
|
|
33
|
+
"""
|
|
34
|
+
Get len of string or file
|
|
35
|
+
|
|
36
|
+
:param body:
|
|
37
|
+
:return:
|
|
38
|
+
:rtype: int
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
return len(body)
|
|
42
|
+
except TypeError:
|
|
43
|
+
try:
|
|
44
|
+
return os.fstat(body.fileno()).st_size
|
|
45
|
+
except (AttributeError, OSError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HTTPClient:
|
|
50
|
+
HTTP_11 = "HTTP/1.1"
|
|
51
|
+
HTTP_10 = "HTTP/1.0"
|
|
52
|
+
|
|
53
|
+
BLOCK_SIZE = 1024 * 4 # 4KB
|
|
54
|
+
|
|
55
|
+
DEFAULT_HEADERS = Headers({"User-Agent": "python/gevent-http-client-" + __version__})
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_url(cls, url, **kw):
|
|
59
|
+
if not isinstance(url, URL):
|
|
60
|
+
url = URL(url)
|
|
61
|
+
enable_ssl = url.scheme == PROTO_HTTPS
|
|
62
|
+
if not enable_ssl:
|
|
63
|
+
kw.pop("ssl_options", None)
|
|
64
|
+
return cls(url.host, port=url.port, ssl=enable_ssl, **kw)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
host,
|
|
69
|
+
port=None,
|
|
70
|
+
headers=None,
|
|
71
|
+
block_size=BLOCK_SIZE,
|
|
72
|
+
connection_timeout=ConnectionPool.DEFAULT_CONNECTION_TIMEOUT,
|
|
73
|
+
network_timeout=ConnectionPool.DEFAULT_NETWORK_TIMEOUT,
|
|
74
|
+
disable_ipv6=False,
|
|
75
|
+
concurrency=1,
|
|
76
|
+
ssl=False,
|
|
77
|
+
ssl_options=None,
|
|
78
|
+
ssl_context_factory=None,
|
|
79
|
+
insecure=False,
|
|
80
|
+
proxy_host=None,
|
|
81
|
+
proxy_port=None,
|
|
82
|
+
version=HTTP_11,
|
|
83
|
+
headers_type=Headers,
|
|
84
|
+
):
|
|
85
|
+
if headers is None:
|
|
86
|
+
headers = headers_type()
|
|
87
|
+
self.host = host
|
|
88
|
+
self.port = port
|
|
89
|
+
connection_host = self.host
|
|
90
|
+
connection_port = self.port
|
|
91
|
+
if proxy_host is not None:
|
|
92
|
+
assert proxy_port is not None, "you have to provide proxy_port if you set proxy_host"
|
|
93
|
+
self.use_proxy = True
|
|
94
|
+
connection_host = proxy_host
|
|
95
|
+
connection_port = proxy_port
|
|
96
|
+
else:
|
|
97
|
+
self.use_proxy = False
|
|
98
|
+
if ssl:
|
|
99
|
+
ssl_options = ssl_options.copy() if ssl_options else {}
|
|
100
|
+
if ssl_options is not None:
|
|
101
|
+
if ssl_context_factory is not None:
|
|
102
|
+
requested_hostname = headers.get("host", self.host)
|
|
103
|
+
ssl_options.setdefault("server_hostname", requested_hostname)
|
|
104
|
+
self.ssl = True
|
|
105
|
+
if not self.port:
|
|
106
|
+
self.port = 443
|
|
107
|
+
if not connection_port:
|
|
108
|
+
connection_port = self.port
|
|
109
|
+
# Import SSL as late as possible, fail hard with Import Error
|
|
110
|
+
from geventhttpclient.connectionpool import SSLConnectionPool
|
|
111
|
+
|
|
112
|
+
self._connection_pool = SSLConnectionPool(
|
|
113
|
+
connection_host,
|
|
114
|
+
connection_port,
|
|
115
|
+
self.host,
|
|
116
|
+
self.port,
|
|
117
|
+
size=concurrency,
|
|
118
|
+
ssl_options=ssl_options,
|
|
119
|
+
ssl_context_factory=ssl_context_factory,
|
|
120
|
+
insecure=insecure,
|
|
121
|
+
network_timeout=network_timeout,
|
|
122
|
+
connection_timeout=connection_timeout,
|
|
123
|
+
disable_ipv6=disable_ipv6,
|
|
124
|
+
use_proxy=self.use_proxy,
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
self.ssl = False
|
|
128
|
+
if not self.port:
|
|
129
|
+
self.port = 80
|
|
130
|
+
if not connection_port:
|
|
131
|
+
connection_port = self.port
|
|
132
|
+
self._connection_pool = ConnectionPool(
|
|
133
|
+
connection_host,
|
|
134
|
+
connection_port,
|
|
135
|
+
self.host,
|
|
136
|
+
self.port,
|
|
137
|
+
size=concurrency,
|
|
138
|
+
network_timeout=network_timeout,
|
|
139
|
+
connection_timeout=connection_timeout,
|
|
140
|
+
disable_ipv6=disable_ipv6,
|
|
141
|
+
use_proxy=self.use_proxy,
|
|
142
|
+
)
|
|
143
|
+
self.version = version
|
|
144
|
+
self.headers_type = headers_type
|
|
145
|
+
self.default_headers = headers_type()
|
|
146
|
+
self.default_headers.update(self.DEFAULT_HEADERS)
|
|
147
|
+
self.default_headers.update(headers)
|
|
148
|
+
self.block_size = block_size
|
|
149
|
+
|
|
150
|
+
scheme = PROTO_HTTPS if self.ssl else PROTO_HTTP
|
|
151
|
+
port_str = f":{port}" if port else ""
|
|
152
|
+
self._base_url_string = f"{scheme}://{self.host}{port_str}/"
|
|
153
|
+
|
|
154
|
+
def close(self):
|
|
155
|
+
self._connection_pool.close()
|
|
156
|
+
|
|
157
|
+
# Like urllib2, try to treat the body as a file if we can't determine the
|
|
158
|
+
# file length with `len()`
|
|
159
|
+
|
|
160
|
+
def _build_request(self, method, request_uri, body="", headers=None):
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
:param method:
|
|
164
|
+
:type method: str or bytes
|
|
165
|
+
:param request_uri:
|
|
166
|
+
:type request_uri: str or bytes
|
|
167
|
+
:param body:
|
|
168
|
+
:type body: str or bytes or file
|
|
169
|
+
:param headers:
|
|
170
|
+
:type headers: dict
|
|
171
|
+
:return:
|
|
172
|
+
:rtype: str or bytes
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
if headers is None:
|
|
176
|
+
headers = {}
|
|
177
|
+
|
|
178
|
+
header_fields = self.headers_type()
|
|
179
|
+
header_fields.update(self.default_headers)
|
|
180
|
+
header_fields.update(headers)
|
|
181
|
+
if self.version == self.HTTP_11 and HEADER_HOST not in header_fields:
|
|
182
|
+
host_port = self.host
|
|
183
|
+
# IPv6 addresses require square brackets in the Host header.
|
|
184
|
+
if ":" in self.host and self.host[0] != "[" and self.host[-1] != "]":
|
|
185
|
+
host_port = "[" + host_port + "]"
|
|
186
|
+
if self.port not in (80, 443):
|
|
187
|
+
host_port += HOST_PORT_SEP + str(self.port)
|
|
188
|
+
header_fields[HEADER_HOST] = host_port
|
|
189
|
+
if body and HEADER_CONTENT_LENGTH not in header_fields:
|
|
190
|
+
body_length = _get_body_length(body)
|
|
191
|
+
if body_length:
|
|
192
|
+
header_fields[HEADER_CONTENT_LENGTH] = body_length
|
|
193
|
+
|
|
194
|
+
request_url = request_uri
|
|
195
|
+
if self.use_proxy:
|
|
196
|
+
base_url = self._base_url_string
|
|
197
|
+
if request_uri.startswith(SLASH):
|
|
198
|
+
base_url = base_url[:-1]
|
|
199
|
+
request_url = base_url + request_url
|
|
200
|
+
elif not request_url.startswith((SLASH, PROTO_HTTP)):
|
|
201
|
+
request_url = SLASH + request_url
|
|
202
|
+
elif request_url.startswith(PROTO_HTTP):
|
|
203
|
+
if request_url.startswith(self._base_url_string):
|
|
204
|
+
request_url = request_url[len(self._base_url_string) - 1 :]
|
|
205
|
+
else:
|
|
206
|
+
raise ValueError("Invalid host in URL")
|
|
207
|
+
|
|
208
|
+
request = method + WHITESPACE + request_url + WHITESPACE + self.version + CRLF
|
|
209
|
+
|
|
210
|
+
for field, value in header_fields.items():
|
|
211
|
+
request += field + FIELD_VALUE_SEP + str(value) + CRLF
|
|
212
|
+
request += CRLF
|
|
213
|
+
return request
|
|
214
|
+
|
|
215
|
+
def request(self, method, request_uri, body=b"", headers=None):
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
:param method:
|
|
219
|
+
:param request_uri:
|
|
220
|
+
:param body: byte or file
|
|
221
|
+
:param headers:
|
|
222
|
+
:return:
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
if isinstance(body, str):
|
|
226
|
+
body = body.encode("utf-8")
|
|
227
|
+
|
|
228
|
+
request = self._build_request(method.upper(), request_uri, body=body, headers=headers)
|
|
229
|
+
|
|
230
|
+
attempts_left = self._connection_pool.size + 1
|
|
231
|
+
|
|
232
|
+
while 1:
|
|
233
|
+
sock = self._connection_pool.get_socket()
|
|
234
|
+
try:
|
|
235
|
+
_request = request.encode()
|
|
236
|
+
if body:
|
|
237
|
+
if isinstance(body, bytes):
|
|
238
|
+
sock.sendall(_request + body)
|
|
239
|
+
else:
|
|
240
|
+
sock.sendall(_request)
|
|
241
|
+
# TODO: Support non file-like iterables, e.g. `(u"string1", u"string2")`.
|
|
242
|
+
sock.sendfile(body)
|
|
243
|
+
else:
|
|
244
|
+
sock.sendall(_request)
|
|
245
|
+
except gevent.socket.error as e:
|
|
246
|
+
self._connection_pool.release_socket(sock)
|
|
247
|
+
if (e.errno == errno.ECONNRESET or e.errno == errno.EPIPE) and attempts_left > 0:
|
|
248
|
+
attempts_left -= 1
|
|
249
|
+
continue
|
|
250
|
+
raise e
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
response = HTTPSocketPoolResponse(
|
|
254
|
+
sock,
|
|
255
|
+
self._connection_pool,
|
|
256
|
+
block_size=self.block_size,
|
|
257
|
+
method=method.upper(),
|
|
258
|
+
headers_type=self.headers_type,
|
|
259
|
+
)
|
|
260
|
+
except HTTPConnectionClosed as e:
|
|
261
|
+
# connection is released by the response itself
|
|
262
|
+
if attempts_left > 0:
|
|
263
|
+
attempts_left -= 1
|
|
264
|
+
continue
|
|
265
|
+
raise e
|
|
266
|
+
else:
|
|
267
|
+
response._sent_request = request
|
|
268
|
+
return response
|
|
269
|
+
|
|
270
|
+
def get(self, request_uri, headers={}):
|
|
271
|
+
return self.request(METHOD_GET, request_uri, headers=headers)
|
|
272
|
+
|
|
273
|
+
def head(self, request_uri, headers=None):
|
|
274
|
+
return self.request(METHOD_HEAD, request_uri, headers=headers)
|
|
275
|
+
|
|
276
|
+
def post(self, request_uri, body="", headers=None):
|
|
277
|
+
return self.request(METHOD_POST, request_uri, body=body, headers=headers)
|
|
278
|
+
|
|
279
|
+
def put(self, request_uri, body="", headers=None):
|
|
280
|
+
return self.request(METHOD_PUT, request_uri, body=body, headers=headers)
|
|
281
|
+
|
|
282
|
+
def delete(self, request_uri, body="", headers=None):
|
|
283
|
+
return self.request(METHOD_DELETE, request_uri, body=body, headers=headers)
|
|
284
|
+
|
|
285
|
+
def patch(self, request_uri, body="", headers=None):
|
|
286
|
+
return self.request(METHOD_PATCH, request_uri, body=body, headers=headers)
|
|
287
|
+
|
|
288
|
+
def trace(self, request_uri, body="", headers=None):
|
|
289
|
+
return self.request(METHOD_TRACE, request_uri, body=body, headers=headers)
|
|
290
|
+
|
|
291
|
+
def options(self, request_uri, headers=None):
|
|
292
|
+
return self.request(METHOD_OPTIONS, request_uri, headers=headers)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class HTTPClientPool:
|
|
296
|
+
"""Factory for maintaining a bunch of clients, one per host:port"""
|
|
297
|
+
|
|
298
|
+
# TODO: Add some housekeeping and cleanup logic
|
|
299
|
+
|
|
300
|
+
def __init__(self, **kw):
|
|
301
|
+
self.clients = {}
|
|
302
|
+
self.client_args = kw
|
|
303
|
+
|
|
304
|
+
def get_client(self, url):
|
|
305
|
+
if not isinstance(url, URL):
|
|
306
|
+
url = URL(url)
|
|
307
|
+
client_key = url.host, url.port
|
|
308
|
+
try:
|
|
309
|
+
return self.clients[client_key]
|
|
310
|
+
except KeyError:
|
|
311
|
+
client = HTTPClient.from_url(url, **self.client_args)
|
|
312
|
+
self.clients[client_key] = client
|
|
313
|
+
return client
|
|
314
|
+
|
|
315
|
+
def close(self):
|
|
316
|
+
for client in self.clients.values():
|
|
317
|
+
client.close()
|
|
318
|
+
self.clients.clear()
|