geventhttpclient 2.1.1__tar.gz → 2.2.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.
- geventhttpclient-2.2.1/LICENSE-MIT +22 -0
- {geventhttpclient-2.1.1/src/geventhttpclient.egg-info → geventhttpclient-2.2.1}/PKG-INFO +42 -33
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/README.md +19 -8
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/pyproject.toml +8 -3
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/setup.py +1 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/__init__.py +1 -1
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/client.py +5 -9
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/connectionpool.py +22 -10
- geventhttpclient-2.2.1/src/geventhttpclient/header.py +277 -0
- geventhttpclient-2.2.1/src/geventhttpclient/httplib.py +252 -0
- geventhttpclient-2.2.1/src/geventhttpclient/httplib2.py +69 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/response.py +10 -11
- geventhttpclient-2.2.1/src/geventhttpclient/tests/conftest.py +57 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_client.py +19 -53
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_headers.py +30 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_httplib.py +24 -17
- geventhttpclient-2.2.1/src/geventhttpclient/tests/test_httplib2.py +31 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_network_failures.py +2 -15
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_parser.py +8 -8
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_ssl.py +77 -23
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_url.py +49 -26
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_useragent.py +2 -18
- geventhttpclient-2.2.1/src/geventhttpclient/url.py +160 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/useragent.py +7 -7
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1/src/geventhttpclient.egg-info}/PKG-INFO +42 -33
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient.egg-info/SOURCES.txt +3 -2
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient.egg-info/requires.txt +3 -0
- geventhttpclient-2.1.1/LICENSE.txt +0 -27
- geventhttpclient-2.1.1/src/geventhttpclient/header.py +0 -232
- geventhttpclient-2.1.1/src/geventhttpclient/httplib.py +0 -133
- geventhttpclient-2.1.1/src/geventhttpclient/tests/conftest.py +0 -3
- geventhttpclient-2.1.1/src/geventhttpclient/tests/test_http_host.py +0 -14
- geventhttpclient-2.1.1/src/geventhttpclient/url.py +0 -246
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/MANIFEST.in +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/ext/Python_compat.h +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/ext/_parser.c +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/LICENSE-MIT +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/include/llhttp.h +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/src/api.c +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/src/http.c +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/src/llhttp.c +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/setup.cfg +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/oncert.pem +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/server.crt +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/server.key +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_keep_alive.py +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_no_module_ssl.py +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient.egg-info/dependency_links.txt +0 -0
- {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
This software is licensed under the MIT License.
|
|
2
|
+
|
|
3
|
+
Copyright Antonin Amand <antonin.amand@gmail.com>, 2018.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
6
|
+
copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
10
|
+
persons to whom the Software is furnished to do so, subject to the
|
|
11
|
+
following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included
|
|
14
|
+
in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
17
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
19
|
+
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
20
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
21
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
22
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -1,35 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: geventhttpclient
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: HTTP client library for gevent
|
|
5
5
|
Author-email: Antonin Amand <antonin.amand@gmail.com>
|
|
6
|
-
License:
|
|
6
|
+
License: This software is licensed under the MIT License.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
licensed under the same terms.
|
|
8
|
+
Copyright Antonin Amand <antonin.amand@gmail.com>, 2018.
|
|
10
9
|
|
|
11
|
-
Permission is hereby granted, free of charge, to any person obtaining a
|
|
12
|
-
of this software and associated documentation files (the
|
|
13
|
-
deal in the Software without restriction, including
|
|
14
|
-
rights to use, copy, modify, merge, publish,
|
|
15
|
-
sell copies of the Software, and to permit
|
|
16
|
-
furnished to do so, subject to the
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
11
|
+
copy of this software and associated documentation files (the
|
|
12
|
+
"Software"), to deal in the Software without restriction, including
|
|
13
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
14
|
+
distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
15
|
+
persons to whom the Software is furnished to do so, subject to the
|
|
16
|
+
following conditions:
|
|
17
17
|
|
|
18
|
-
The above copyright notice and this permission notice shall be included
|
|
19
|
-
all copies or substantial portions of the Software.
|
|
18
|
+
The above copyright notice and this permission notice shall be included
|
|
19
|
+
in all copies or substantial portions of the Software.
|
|
20
20
|
|
|
21
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
22
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
23
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
24
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
25
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
26
|
-
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
27
|
-
IN THE SOFTWARE.
|
|
28
|
-
|
|
29
|
-
Note: Previous versions of gevenhttpclient used http_parser.c, which in turn
|
|
30
|
-
was based on src/http/ngx_http_parse.c from NGINX, copyright Igor Sysoev,
|
|
31
|
-
Joyent, Inc. and other Node contributors. See http://github.com/joyent/http-parser
|
|
32
|
-
for more information
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
22
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
23
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
24
|
+
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
25
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
26
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
27
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
33
28
|
|
|
34
29
|
Project-URL: homepage, http://github.com/geventhttpclient/geventhttpclient
|
|
35
30
|
Project-URL: issues, http://github.com/geventhttpclient/geventhttpclient/issues
|
|
@@ -38,6 +33,7 @@ Keywords: http,gevent,client
|
|
|
38
33
|
Classifier: Development Status :: 5 - Production/Stable
|
|
39
34
|
Classifier: Intended Audience :: Developers
|
|
40
35
|
Classifier: Intended Audience :: Information Technology
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
41
37
|
Classifier: Operating System :: OS Independent
|
|
42
38
|
Classifier: Programming Language :: Python :: 3
|
|
43
39
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -48,7 +44,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
48
44
|
Classifier: Topic :: Software Development :: Libraries
|
|
49
45
|
Requires-Python: >=3.9
|
|
50
46
|
Description-Content-Type: text/markdown
|
|
51
|
-
License-File: LICENSE
|
|
47
|
+
License-File: LICENSE-MIT
|
|
52
48
|
Requires-Dist: gevent
|
|
53
49
|
Requires-Dist: certifi
|
|
54
50
|
Requires-Dist: brotli
|
|
@@ -62,6 +58,8 @@ Requires-Dist: requests; extra == "benchmarks"
|
|
|
62
58
|
Requires-Dist: httpx; extra == "benchmarks"
|
|
63
59
|
Requires-Dist: urllib3; extra == "benchmarks"
|
|
64
60
|
Requires-Dist: httplib2; extra == "benchmarks"
|
|
61
|
+
Provides-Extra: examples
|
|
62
|
+
Requires-Dist: oauth2; extra == "examples"
|
|
65
63
|
|
|
66
64
|
[](https://github.com/geventhttpclient/geventhttpclient/actions)
|
|
67
65
|
[](https://pypi.org/project/geventhttpclient/)
|
|
@@ -226,13 +224,24 @@ with open('/tmp/100.dat', 'w') as f:
|
|
|
226
224
|
|
|
227
225
|
The benchmark does 10000 get requests against a local nginx server in the default
|
|
228
226
|
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
|
-
|
|
|
227
|
+
second for a couple of popular clients is given in the table below. Please read
|
|
228
|
+
[benchmarks/README.md](https://github.com/geventhttpclient/geventhttpclient/blob/master/benchmarks/README.md)
|
|
229
|
+
for mor details. Also note, HTTPX is better be used with asyncio, not gevent.
|
|
230
|
+
|
|
231
|
+
| HTTP Client | RPS |
|
|
232
|
+
|--------------------|--------|
|
|
233
|
+
| GeventHTTPClient | 7268.9 |
|
|
234
|
+
| Httplib2 (patched) | 2323.9 |
|
|
235
|
+
| Urllib3 | 2242.5 |
|
|
236
|
+
| Requests | 1046.1 |
|
|
237
|
+
| Httpx | 770.3 |
|
|
237
238
|
|
|
238
239
|
*Linux(x86_64), Python 3.11.6 @ Intel i7-7560U*
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
This package is distributed under the [MIT license](https://github.com/geventhttpclient/geventhttpclient/blob/master/LICENSE-MIT).
|
|
244
|
+
Previous versions of geventhttpclient used http_parser.c, which in turn
|
|
245
|
+
was based on `src/http/ngx_http_parse.c` from NGINX, copyright Igor Sysoev,
|
|
246
|
+
Joyent, Inc., and other Node contributors. For more information, see
|
|
247
|
+
http://github.com/joyent/http-parser
|
|
@@ -161,13 +161,24 @@ 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*
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
This package is distributed under the [MIT license](https://github.com/geventhttpclient/geventhttpclient/blob/master/LICENSE-MIT).
|
|
181
|
+
Previous versions of geventhttpclient used http_parser.c, which in turn
|
|
182
|
+
was based on `src/http/ngx_http_parse.c` from NGINX, copyright Igor Sysoev,
|
|
183
|
+
Joyent, Inc., and other Node contributors. For more information, see
|
|
184
|
+
http://github.com/joyent/http-parser
|
|
@@ -5,17 +5,18 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "geventhttpclient"
|
|
8
|
-
version = "2.
|
|
8
|
+
version = "2.2.1" # 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"
|
|
12
|
-
license = {file = "LICENSE
|
|
12
|
+
license = {file = "LICENSE-MIT"}
|
|
13
13
|
keywords = ["http", "gevent", "client"]
|
|
14
14
|
authors = [{name = "Antonin Amand", email = "antonin.amand@gmail.com"}]
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Development Status :: 5 - Production/Stable",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Intended Audience :: Information Technology",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
19
20
|
"Operating System :: OS Independent",
|
|
20
21
|
"Programming Language :: Python :: 3",
|
|
21
22
|
"Programming Language :: Python :: 3.9",
|
|
@@ -40,8 +41,12 @@ optional-dependencies.benchmarks = [
|
|
|
40
41
|
"requests",
|
|
41
42
|
"httpx",
|
|
42
43
|
"urllib3",
|
|
43
|
-
"httplib2"
|
|
44
|
+
"httplib2",
|
|
44
45
|
]
|
|
46
|
+
optional-dependencies.examples = [
|
|
47
|
+
"oauth2",
|
|
48
|
+
]
|
|
49
|
+
|
|
45
50
|
|
|
46
51
|
[project.urls]
|
|
47
52
|
homepage = "http://github.com/geventhttpclient/geventhttpclient"
|
|
@@ -83,7 +83,7 @@ class HTTPClient:
|
|
|
83
83
|
headers_type=Headers,
|
|
84
84
|
):
|
|
85
85
|
if headers is None:
|
|
86
|
-
headers =
|
|
86
|
+
headers = headers_type()
|
|
87
87
|
self.host = host
|
|
88
88
|
self.port = port
|
|
89
89
|
connection_host = self.host
|
|
@@ -146,14 +146,10 @@ class HTTPClient:
|
|
|
146
146
|
self.default_headers.update(self.DEFAULT_HEADERS)
|
|
147
147
|
self.default_headers.update(headers)
|
|
148
148
|
self.block_size = block_size
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
url.host = self.host
|
|
154
|
-
url.port = self.port
|
|
155
|
-
url.scheme = self.ssl and PROTO_HTTPS or PROTO_HTTP
|
|
156
|
-
return url
|
|
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}/"
|
|
157
153
|
|
|
158
154
|
def close(self):
|
|
159
155
|
self._connection_pool.close()
|
|
@@ -184,12 +184,28 @@ class ConnectionPool:
|
|
|
184
184
|
self._semaphore.release()
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
def init_ssl_context(ssl_context_factory, ca_certs, check_hostname=True):
|
|
188
|
+
try:
|
|
189
|
+
ssl_context = ssl_context_factory(cafile=ca_certs)
|
|
190
|
+
except TypeError:
|
|
191
|
+
ssl_context = ssl_context_factory()
|
|
192
|
+
ssl_context.load_verify_locations(cafile=ca_certs)
|
|
193
|
+
ssl_context.check_hostname = check_hostname
|
|
194
|
+
if check_hostname:
|
|
195
|
+
ssl_context.verify_mode = gevent.ssl.CERT_REQUIRED
|
|
196
|
+
return ssl_context
|
|
197
|
+
|
|
198
|
+
|
|
187
199
|
try:
|
|
200
|
+
from ssl import PROTOCOL_TLS_CLIENT
|
|
201
|
+
|
|
188
202
|
import gevent.ssl
|
|
203
|
+
|
|
189
204
|
try:
|
|
190
205
|
from gevent.ssl import create_default_context
|
|
191
206
|
except ImportError:
|
|
192
207
|
create_default_context = None
|
|
208
|
+
|
|
193
209
|
except ImportError:
|
|
194
210
|
pass
|
|
195
211
|
else:
|
|
@@ -208,6 +224,7 @@ else:
|
|
|
208
224
|
"ciphers": _DEFAULT_CIPHERS,
|
|
209
225
|
"ca_certs": _CA_CERTS,
|
|
210
226
|
"cert_reqs": gevent.ssl.CERT_REQUIRED,
|
|
227
|
+
"ssl_version": PROTOCOL_TLS_CLIENT,
|
|
211
228
|
}
|
|
212
229
|
|
|
213
230
|
def __init__(
|
|
@@ -228,21 +245,16 @@ else:
|
|
|
228
245
|
|
|
229
246
|
ssl_context_factory = ssl_context_factory or create_default_context
|
|
230
247
|
if ssl_context_factory is not None:
|
|
231
|
-
self.init_ssl_context(
|
|
248
|
+
self.ssl_context = init_ssl_context(
|
|
249
|
+
ssl_context_factory,
|
|
250
|
+
self.ssl_options["ca_certs"],
|
|
251
|
+
check_hostname=not self.insecure,
|
|
252
|
+
)
|
|
232
253
|
else:
|
|
233
254
|
self.ssl_context = None
|
|
234
255
|
|
|
235
256
|
super().__init__(connection_host, connection_port, request_host, request_port, **kw)
|
|
236
257
|
|
|
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
258
|
def _connect_socket(self, sock, address):
|
|
247
259
|
sock = super()._connect_socket(sock, address)
|
|
248
260
|
|
|
@@ -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
|