geventhttpclient 2.1.0__tar.gz → 2.2.0__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.
- {geventhttpclient-2.1.0/src/geventhttpclient.egg-info → geventhttpclient-2.2.0}/PKG-INFO +14 -9
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/README.md +11 -8
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/pyproject.toml +6 -2
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/setup.py +1 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/__init__.py +1 -1
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/client.py +1 -1
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/connectionpool.py +46 -10
- geventhttpclient-2.2.0/src/geventhttpclient/header.py +277 -0
- geventhttpclient-2.2.0/src/geventhttpclient/httplib.py +228 -0
- geventhttpclient-2.2.0/src/geventhttpclient/httplib2.py +37 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/response.py +10 -11
- geventhttpclient-2.2.0/src/geventhttpclient/tests/conftest.py +7 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_client.py +17 -4
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_headers.py +30 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_httplib.py +9 -0
- geventhttpclient-2.2.0/src/geventhttpclient/tests/test_httplib2.py +20 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_parser.py +8 -8
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_useragent.py +1 -3
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/useragent.py +2 -2
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0/src/geventhttpclient.egg-info}/PKG-INFO +14 -9
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient.egg-info/SOURCES.txt +2 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient.egg-info/requires.txt +3 -0
- geventhttpclient-2.1.0/src/geventhttpclient/header.py +0 -232
- geventhttpclient-2.1.0/src/geventhttpclient/httplib.py +0 -133
- geventhttpclient-2.1.0/src/geventhttpclient/tests/conftest.py +0 -3
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/LICENSE.txt +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/MANIFEST.in +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/ext/Python_compat.h +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/ext/_parser.c +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/llhttp/LICENSE-MIT +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/llhttp/include/llhttp.h +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/llhttp/src/api.c +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/llhttp/src/http.c +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/llhttp/src/llhttp.c +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/setup.cfg +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/oncert.pem +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/server.crt +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/server.key +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_http_host.py +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_keep_alive.py +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_network_failures.py +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_no_module_ssl.py +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_ssl.py +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_url.py +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient/url.py +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient.egg-info/dependency_links.txt +0 -0
- {geventhttpclient-2.1.0 → geventhttpclient-2.2.0}/src/geventhttpclient.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: geventhttpclient
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: HTTP client library for gevent
|
|
5
5
|
Author-email: Antonin Amand <antonin.amand@gmail.com>
|
|
6
6
|
License: Based on llhttp, copyright Fedor Indutny, 2018.
|
|
@@ -62,6 +62,8 @@ Requires-Dist: requests; extra == "benchmarks"
|
|
|
62
62
|
Requires-Dist: httpx; extra == "benchmarks"
|
|
63
63
|
Requires-Dist: urllib3; extra == "benchmarks"
|
|
64
64
|
Requires-Dist: httplib2; extra == "benchmarks"
|
|
65
|
+
Provides-Extra: examples
|
|
66
|
+
Requires-Dist: oauth2; extra == "examples"
|
|
65
67
|
|
|
66
68
|
[](https://github.com/geventhttpclient/geventhttpclient/actions)
|
|
67
69
|
[](https://pypi.org/project/geventhttpclient/)
|
|
@@ -226,13 +228,16 @@ with open('/tmp/100.dat', 'w') as f:
|
|
|
226
228
|
|
|
227
229
|
The benchmark does 10000 get requests against a local nginx server in the default
|
|
228
230
|
configuration with a concurrency of 10. See `benchmarks` folder. The requests per
|
|
229
|
-
second for a couple of popular clients is given in the table below.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
|
234
|
-
|
|
235
|
-
|
|
|
236
|
-
|
|
|
231
|
+
second for a couple of popular clients is given in the table below. Please read
|
|
232
|
+
[benchmarks/README.md](https://github.com/geventhttpclient/geventhttpclient/blob/master/benchmarks/README.md)
|
|
233
|
+
for mor details. Also note, HTTPX is better be used with asyncio, not gevent.
|
|
234
|
+
|
|
235
|
+
| HTTP Client | RPS |
|
|
236
|
+
|--------------------|--------|
|
|
237
|
+
| GeventHTTPClient | 7268.9 |
|
|
238
|
+
| Httplib2 (patched) | 2323.9 |
|
|
239
|
+
| Urllib3 | 2242.5 |
|
|
240
|
+
| Requests | 1046.1 |
|
|
241
|
+
| Httpx | 770.3 |
|
|
237
242
|
|
|
238
243
|
*Linux(x86_64), Python 3.11.6 @ Intel i7-7560U*
|
|
@@ -161,13 +161,16 @@ with open('/tmp/100.dat', 'w') as f:
|
|
|
161
161
|
|
|
162
162
|
The benchmark does 10000 get requests against a local nginx server in the default
|
|
163
163
|
configuration with a concurrency of 10. See `benchmarks` folder. The requests per
|
|
164
|
-
second for a couple of popular clients is given in the table below.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
|
169
|
-
|
|
170
|
-
|
|
|
171
|
-
|
|
|
164
|
+
second for a couple of popular clients is given in the table below. Please read
|
|
165
|
+
[benchmarks/README.md](https://github.com/geventhttpclient/geventhttpclient/blob/master/benchmarks/README.md)
|
|
166
|
+
for mor details. Also note, HTTPX is better be used with asyncio, not gevent.
|
|
167
|
+
|
|
168
|
+
| HTTP Client | RPS |
|
|
169
|
+
|--------------------|--------|
|
|
170
|
+
| GeventHTTPClient | 7268.9 |
|
|
171
|
+
| Httplib2 (patched) | 2323.9 |
|
|
172
|
+
| Urllib3 | 2242.5 |
|
|
173
|
+
| Requests | 1046.1 |
|
|
174
|
+
| Httpx | 770.3 |
|
|
172
175
|
|
|
173
176
|
*Linux(x86_64), Python 3.11.6 @ Intel i7-7560U*
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "geventhttpclient"
|
|
8
|
-
version = "2.
|
|
8
|
+
version = "2.2.0" # dont 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"
|
|
@@ -40,8 +40,12 @@ optional-dependencies.benchmarks = [
|
|
|
40
40
|
"requests",
|
|
41
41
|
"httpx",
|
|
42
42
|
"urllib3",
|
|
43
|
-
"httplib2"
|
|
43
|
+
"httplib2",
|
|
44
44
|
]
|
|
45
|
+
optional-dependencies.examples = [
|
|
46
|
+
"oauth2",
|
|
47
|
+
]
|
|
48
|
+
|
|
45
49
|
|
|
46
50
|
[project.urls]
|
|
47
51
|
homepage = "http://github.com/geventhttpclient/geventhttpclient"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from contextlib import contextmanager
|
|
2
3
|
|
|
3
4
|
import gevent.queue
|
|
4
5
|
import gevent.socket
|
|
@@ -184,8 +185,21 @@ class ConnectionPool:
|
|
|
184
185
|
self._semaphore.release()
|
|
185
186
|
|
|
186
187
|
|
|
188
|
+
def init_ssl_context(ssl_context_factory, ca_certs, check_hostname=True):
|
|
189
|
+
try:
|
|
190
|
+
ssl_context = ssl_context_factory(cafile=ca_certs)
|
|
191
|
+
except TypeError:
|
|
192
|
+
ssl_context = ssl_context_factory()
|
|
193
|
+
ssl_context.load_verify_locations(cafile=ca_certs)
|
|
194
|
+
ssl_context.check_hostname = check_hostname
|
|
195
|
+
if check_hostname:
|
|
196
|
+
ssl_context.verify_mode = gevent.ssl.CERT_REQUIRED
|
|
197
|
+
return ssl_context
|
|
198
|
+
|
|
199
|
+
|
|
187
200
|
try:
|
|
188
201
|
import gevent.ssl
|
|
202
|
+
|
|
189
203
|
try:
|
|
190
204
|
from gevent.ssl import create_default_context
|
|
191
205
|
except ImportError:
|
|
@@ -228,21 +242,16 @@ else:
|
|
|
228
242
|
|
|
229
243
|
ssl_context_factory = ssl_context_factory or create_default_context
|
|
230
244
|
if ssl_context_factory is not None:
|
|
231
|
-
self.init_ssl_context(
|
|
245
|
+
self.ssl_context = init_ssl_context(
|
|
246
|
+
ssl_context_factory,
|
|
247
|
+
self.ssl_options["ca_certs"],
|
|
248
|
+
check_hostname=not self.insecure,
|
|
249
|
+
)
|
|
232
250
|
else:
|
|
233
251
|
self.ssl_context = None
|
|
234
252
|
|
|
235
253
|
super().__init__(connection_host, connection_port, request_host, request_port, **kw)
|
|
236
254
|
|
|
237
|
-
def init_ssl_context(self, ssl_context_factory):
|
|
238
|
-
ca_certs = self.ssl_options["ca_certs"]
|
|
239
|
-
try:
|
|
240
|
-
self.ssl_context = ssl_context_factory(cafile=ca_certs)
|
|
241
|
-
except TypeError:
|
|
242
|
-
self.ssl_context = ssl_context_factory()
|
|
243
|
-
self.ssl_context.load_verify_locations(cafile=ca_certs)
|
|
244
|
-
self.ssl_context.check_hostname = not self.insecure
|
|
245
|
-
|
|
246
255
|
def _connect_socket(self, sock, address):
|
|
247
256
|
sock = super()._connect_socket(sock, address)
|
|
248
257
|
|
|
@@ -251,3 +260,30 @@ else:
|
|
|
251
260
|
else:
|
|
252
261
|
server_hostname = self.ssl_options.get("server_hostname", self._request_host)
|
|
253
262
|
return self.ssl_context.wrap_socket(sock, server_hostname=server_hostname)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class ClientPool:
|
|
266
|
+
"""
|
|
267
|
+
Pool implementation for HTTP clients, that weren't designed with concurrency in mind.
|
|
268
|
+
Usage example:
|
|
269
|
+
|
|
270
|
+
pool = ClientPool(MyHttpClient)
|
|
271
|
+
with pool.get() as client:
|
|
272
|
+
response = client.request("127.0.0.1")
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(self, factory, concurrency=5):
|
|
276
|
+
self.factory = factory
|
|
277
|
+
self.queue = gevent.queue.Queue(concurrency)
|
|
278
|
+
for i in range(concurrency):
|
|
279
|
+
self.queue.put(factory())
|
|
280
|
+
|
|
281
|
+
@contextmanager
|
|
282
|
+
def get(self):
|
|
283
|
+
client = self.queue.get()
|
|
284
|
+
yield client
|
|
285
|
+
self.queue.put(client)
|
|
286
|
+
|
|
287
|
+
def close(self):
|
|
288
|
+
for client in self.queue:
|
|
289
|
+
client.close()
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from collections.abc import Mapping, MutableMapping
|
|
2
|
+
|
|
3
|
+
_dict_setitem = dict.__setitem__
|
|
4
|
+
_dict_getitem = dict.__getitem__
|
|
5
|
+
_dict_delitem = dict.__delitem__
|
|
6
|
+
_dict_contains = dict.__contains__
|
|
7
|
+
_dict_setdefault = dict.setdefault
|
|
8
|
+
_dict_items = dict.items
|
|
9
|
+
_dict_values = dict.values
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Headers(dict):
|
|
13
|
+
"""
|
|
14
|
+
:param headers:
|
|
15
|
+
An iterable of field-value pairs.
|
|
16
|
+
|
|
17
|
+
:param kwargs:
|
|
18
|
+
Additional field-value pairs to pass in to ``Headers.extend``.
|
|
19
|
+
|
|
20
|
+
A ``dict`` like container for storing HTTP Headers.
|
|
21
|
+
|
|
22
|
+
According to RFC 7230, header field names have to be treated case-insentitive.
|
|
23
|
+
This means `SET-COOKIE` as header field denotes the same field as `set-cookie`.
|
|
24
|
+
|
|
25
|
+
In order to implement this behavior efficiently, all header names are lowered
|
|
26
|
+
and used as dictionary key for fast access of certain header lines. The original
|
|
27
|
+
case-sensitive header field is stored alongside the values under the hood.
|
|
28
|
+
|
|
29
|
+
Iterating over Header.items() returns all field-value pairs with the original
|
|
30
|
+
cases. The order of the returned items matches the input order, except that
|
|
31
|
+
headers with a matching header field are grouped.
|
|
32
|
+
|
|
33
|
+
Use Header.__setitem__() and Header.update() for overwriting the content
|
|
34
|
+
of matching header fields and .add() and .extend() for appending header
|
|
35
|
+
lines instead of overwriting them.
|
|
36
|
+
|
|
37
|
+
Note: b"asdf" and "asdf" are separate things. This class tries not to
|
|
38
|
+
enforce the one or the other.
|
|
39
|
+
|
|
40
|
+
>>> headers = Headers()
|
|
41
|
+
>>> headers.add('Set-Cookie', 'foo=bar')
|
|
42
|
+
>>> headers.add('set-cookie', 'baz=quxx')
|
|
43
|
+
>>> headers['SET-cookie']
|
|
44
|
+
['foo=bar', 'baz=quxx']
|
|
45
|
+
>>> headers['content-length'] = '7'
|
|
46
|
+
>>> headers['Content-Length']
|
|
47
|
+
'7'
|
|
48
|
+
>>> headers
|
|
49
|
+
Headers({'Set-Cookie': 'foo=bar, baz=quxx', 'content-length': '7'})
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, headers=None, **kwargs):
|
|
53
|
+
dict.__init__(self)
|
|
54
|
+
if headers is not None:
|
|
55
|
+
if isinstance(headers, type(self)):
|
|
56
|
+
self._copy_from(headers)
|
|
57
|
+
else:
|
|
58
|
+
self.extend(headers)
|
|
59
|
+
if kwargs:
|
|
60
|
+
self.extend(kwargs)
|
|
61
|
+
|
|
62
|
+
def __setitem__(self, field, value):
|
|
63
|
+
return _dict_setitem(self, field.lower(), (field, value))
|
|
64
|
+
|
|
65
|
+
def __getitem__(self, field):
|
|
66
|
+
vals = _dict_getitem(self, field.lower())
|
|
67
|
+
if isinstance(vals, tuple):
|
|
68
|
+
return vals[1]
|
|
69
|
+
return [val[1] for val in vals]
|
|
70
|
+
|
|
71
|
+
def __delitem__(self, field):
|
|
72
|
+
return _dict_delitem(self, field.lower())
|
|
73
|
+
|
|
74
|
+
def __contains__(self, field):
|
|
75
|
+
return _dict_contains(self, field.lower())
|
|
76
|
+
|
|
77
|
+
def __eq__(self, other):
|
|
78
|
+
if not isinstance(other, Mapping) and not hasattr(other, "keys"):
|
|
79
|
+
return False
|
|
80
|
+
if not isinstance(other, type(self)):
|
|
81
|
+
other = type(self)(other)
|
|
82
|
+
return {f1: self[f1] for f1 in self} == {f2: other[f2] for f2 in other}
|
|
83
|
+
|
|
84
|
+
def __ne__(self, other):
|
|
85
|
+
return not self.__eq__(other)
|
|
86
|
+
|
|
87
|
+
values = MutableMapping.values
|
|
88
|
+
get = MutableMapping.get
|
|
89
|
+
keys = MutableMapping.keys
|
|
90
|
+
|
|
91
|
+
__marker = object()
|
|
92
|
+
|
|
93
|
+
def pop(self, field, default=__marker):
|
|
94
|
+
"""D.pop(field[,default]) -> value, remove specified field and return the corresponding value.
|
|
95
|
+
If field is not found, d is returned if given, otherwise KeyError is raised.
|
|
96
|
+
"""
|
|
97
|
+
# Using the MutableMapping function directly fails due to the private marker.
|
|
98
|
+
# Using ordinary dict.pop would expose the internal structures.
|
|
99
|
+
# So let's reinvent the wheel.
|
|
100
|
+
try:
|
|
101
|
+
value = self[field]
|
|
102
|
+
except KeyError:
|
|
103
|
+
if default is self.__marker:
|
|
104
|
+
raise
|
|
105
|
+
return default
|
|
106
|
+
del self[field]
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def discard(self, field):
|
|
110
|
+
try:
|
|
111
|
+
del self[field]
|
|
112
|
+
except KeyError:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def add(self, field, value):
|
|
116
|
+
"""Add a (field, value) pair without overwriting the value if it already
|
|
117
|
+
exists.
|
|
118
|
+
|
|
119
|
+
>>> headers = Headers(foo='bar')
|
|
120
|
+
>>> headers.add('Foo', 'baz')
|
|
121
|
+
>>> headers['foo']
|
|
122
|
+
'bar, baz'
|
|
123
|
+
"""
|
|
124
|
+
field_lower = field.lower()
|
|
125
|
+
new_vals = field, value
|
|
126
|
+
# Keep the common case aka no item present as fast as possible
|
|
127
|
+
vals = _dict_setdefault(self, field_lower, new_vals)
|
|
128
|
+
if new_vals is not vals:
|
|
129
|
+
# new_vals was not inserted, as there was a previous one
|
|
130
|
+
if isinstance(vals, tuple):
|
|
131
|
+
# Only one item so far, create a list and append new_vals
|
|
132
|
+
_dict_setitem(self, field_lower, [vals, new_vals])
|
|
133
|
+
elif isinstance(vals, list):
|
|
134
|
+
# Already several items got inserted, we have a list
|
|
135
|
+
vals.append(new_vals)
|
|
136
|
+
else:
|
|
137
|
+
raise TypeError("invalid vals stored")
|
|
138
|
+
|
|
139
|
+
def extend(self, *args, **kwargs):
|
|
140
|
+
"""Generic import function for any type of header-like object.
|
|
141
|
+
Adapted version of MutableMapping.update in order to insert items
|
|
142
|
+
with self.add instead of self.__setitem__
|
|
143
|
+
"""
|
|
144
|
+
if len(args) > 1:
|
|
145
|
+
raise TypeError(f"extend() takes at most 1 positional argument ({len(args)} given)")
|
|
146
|
+
other = args[0] if len(args) >= 1 else ()
|
|
147
|
+
|
|
148
|
+
if isinstance(other, type(self)):
|
|
149
|
+
for field, value in other.items():
|
|
150
|
+
self.add(field, value)
|
|
151
|
+
elif isinstance(other, Mapping):
|
|
152
|
+
for field in other:
|
|
153
|
+
self.add(field, other[field])
|
|
154
|
+
elif hasattr(other, "keys"):
|
|
155
|
+
for field in other.fields():
|
|
156
|
+
self.add(field, other[field])
|
|
157
|
+
else:
|
|
158
|
+
for field, value in other:
|
|
159
|
+
self.add(field, value)
|
|
160
|
+
|
|
161
|
+
for field, value in kwargs.items():
|
|
162
|
+
self.add(field, value)
|
|
163
|
+
|
|
164
|
+
def update(self, *args, **kwargs):
|
|
165
|
+
"""Generic import function for any type of header-like object.
|
|
166
|
+
Adapted version of MutableMapping.update in order to overwrite items
|
|
167
|
+
while preserving case-sensitive header fields.
|
|
168
|
+
"""
|
|
169
|
+
if len(args) > 1:
|
|
170
|
+
raise TypeError(f"extend() takes at most 1 positional argument ({len(args)} given)")
|
|
171
|
+
other = args[0] if len(args) >= 1 else ()
|
|
172
|
+
|
|
173
|
+
if isinstance(other, type(self)):
|
|
174
|
+
for field, value in other.items():
|
|
175
|
+
self[field] = value
|
|
176
|
+
elif isinstance(other, Mapping):
|
|
177
|
+
for field in other:
|
|
178
|
+
self[field] = other[field]
|
|
179
|
+
elif hasattr(other, "keys"):
|
|
180
|
+
for field in other.keys():
|
|
181
|
+
self[field] = other[field]
|
|
182
|
+
else:
|
|
183
|
+
for field, value in other:
|
|
184
|
+
self[field] = value
|
|
185
|
+
|
|
186
|
+
for field, value in kwargs.items():
|
|
187
|
+
self[field] = value
|
|
188
|
+
|
|
189
|
+
def getlist(self, field):
|
|
190
|
+
"""Returns a list of all the values for the named field. Returns an
|
|
191
|
+
empty list if the field doesn't exist.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
vals = _dict_getitem(self, field.lower())
|
|
195
|
+
except KeyError:
|
|
196
|
+
return []
|
|
197
|
+
if isinstance(vals, tuple):
|
|
198
|
+
return [vals[1]]
|
|
199
|
+
return [val[1] for val in vals]
|
|
200
|
+
|
|
201
|
+
def get_all(self, field, failobj=None):
|
|
202
|
+
values = self.getlist(field)
|
|
203
|
+
if not values:
|
|
204
|
+
return failobj
|
|
205
|
+
return values
|
|
206
|
+
|
|
207
|
+
def _copy_from(self, other):
|
|
208
|
+
for field in other:
|
|
209
|
+
vals = _dict_getitem(other, field)
|
|
210
|
+
if isinstance(vals, list):
|
|
211
|
+
# No need to clone immutable tuples, only lists
|
|
212
|
+
vals = list(vals)
|
|
213
|
+
_dict_setitem(self, field, vals)
|
|
214
|
+
|
|
215
|
+
def copy(self):
|
|
216
|
+
clone = type(self)()
|
|
217
|
+
clone._copy_from(self)
|
|
218
|
+
return clone
|
|
219
|
+
|
|
220
|
+
def __len__(self):
|
|
221
|
+
return sum(1 if isinstance(vals, tuple) else len(vals) for vals in _dict_values(self))
|
|
222
|
+
|
|
223
|
+
def __repr__(self):
|
|
224
|
+
return f"{type(self).__name__}({dict(self.itermerged())})"
|
|
225
|
+
|
|
226
|
+
def __str__(self):
|
|
227
|
+
"""Return a similar string as the original HTTP text received for parsing. Lines with
|
|
228
|
+
matching header fields are grouped.
|
|
229
|
+
"""
|
|
230
|
+
return "\n".join(f"{field}: {value}" for field, value in self.items())
|
|
231
|
+
|
|
232
|
+
def itermerged(self):
|
|
233
|
+
"""Iterate over all headers, merging lines with duplicate header
|
|
234
|
+
fields together into one item. The case of the first header field
|
|
235
|
+
is preserved.
|
|
236
|
+
"""
|
|
237
|
+
for field, vals in _dict_items(self):
|
|
238
|
+
if isinstance(vals, tuple):
|
|
239
|
+
yield vals
|
|
240
|
+
else:
|
|
241
|
+
# this should preserve either binary or string type
|
|
242
|
+
sep = ", " if isinstance(vals[0][0], str) else b", "
|
|
243
|
+
yield vals[0][0], sep.join(val[1] for val in vals)
|
|
244
|
+
|
|
245
|
+
def compatible_dict(self):
|
|
246
|
+
"""Create a dictionary. Header lines with duplicate field names are
|
|
247
|
+
merged into one line. This can be used for exchange with other
|
|
248
|
+
libraries.
|
|
249
|
+
"""
|
|
250
|
+
return dict(self.itermerged())
|
|
251
|
+
|
|
252
|
+
def iterlower(self):
|
|
253
|
+
"""Iterate over all header lines, including duplicate ones.
|
|
254
|
+
The header fields are all lowered.
|
|
255
|
+
"""
|
|
256
|
+
for field, vals in _dict_items(self):
|
|
257
|
+
if isinstance(vals, tuple):
|
|
258
|
+
yield field, vals[1]
|
|
259
|
+
else:
|
|
260
|
+
for val in vals:
|
|
261
|
+
yield field, val[1]
|
|
262
|
+
|
|
263
|
+
def items(self):
|
|
264
|
+
"""Iterate over all header lines, including duplicate ones."""
|
|
265
|
+
for field, vals in _dict_items(self):
|
|
266
|
+
if isinstance(vals, tuple):
|
|
267
|
+
yield vals
|
|
268
|
+
else:
|
|
269
|
+
yield from vals
|
|
270
|
+
|
|
271
|
+
# Backwards compatibility for httplib
|
|
272
|
+
getheaders = getlist
|
|
273
|
+
getallmatchingheaders = getlist
|
|
274
|
+
iget = getlist
|
|
275
|
+
|
|
276
|
+
# deprecated
|
|
277
|
+
iteroriginal = items
|