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.
Files changed (49) hide show
  1. geventhttpclient-2.2.1/LICENSE-MIT +22 -0
  2. {geventhttpclient-2.1.1/src/geventhttpclient.egg-info → geventhttpclient-2.2.1}/PKG-INFO +42 -33
  3. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/README.md +19 -8
  4. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/pyproject.toml +8 -3
  5. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/setup.py +1 -0
  6. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/__init__.py +1 -1
  7. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/client.py +5 -9
  8. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/connectionpool.py +22 -10
  9. geventhttpclient-2.2.1/src/geventhttpclient/header.py +277 -0
  10. geventhttpclient-2.2.1/src/geventhttpclient/httplib.py +252 -0
  11. geventhttpclient-2.2.1/src/geventhttpclient/httplib2.py +69 -0
  12. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/response.py +10 -11
  13. geventhttpclient-2.2.1/src/geventhttpclient/tests/conftest.py +57 -0
  14. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_client.py +19 -53
  15. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_headers.py +30 -0
  16. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_httplib.py +24 -17
  17. geventhttpclient-2.2.1/src/geventhttpclient/tests/test_httplib2.py +31 -0
  18. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_network_failures.py +2 -15
  19. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_parser.py +8 -8
  20. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_ssl.py +77 -23
  21. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_url.py +49 -26
  22. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_useragent.py +2 -18
  23. geventhttpclient-2.2.1/src/geventhttpclient/url.py +160 -0
  24. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/useragent.py +7 -7
  25. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1/src/geventhttpclient.egg-info}/PKG-INFO +42 -33
  26. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient.egg-info/SOURCES.txt +3 -2
  27. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient.egg-info/requires.txt +3 -0
  28. geventhttpclient-2.1.1/LICENSE.txt +0 -27
  29. geventhttpclient-2.1.1/src/geventhttpclient/header.py +0 -232
  30. geventhttpclient-2.1.1/src/geventhttpclient/httplib.py +0 -133
  31. geventhttpclient-2.1.1/src/geventhttpclient/tests/conftest.py +0 -3
  32. geventhttpclient-2.1.1/src/geventhttpclient/tests/test_http_host.py +0 -14
  33. geventhttpclient-2.1.1/src/geventhttpclient/url.py +0 -246
  34. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/MANIFEST.in +0 -0
  35. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/ext/Python_compat.h +0 -0
  36. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/ext/_parser.c +0 -0
  37. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/LICENSE-MIT +0 -0
  38. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/include/llhttp.h +0 -0
  39. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/src/api.c +0 -0
  40. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/src/http.c +0 -0
  41. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/llhttp/src/llhttp.c +0 -0
  42. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/setup.cfg +0 -0
  43. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/oncert.pem +0 -0
  44. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/server.crt +0 -0
  45. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/server.key +0 -0
  46. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_keep_alive.py +0 -0
  47. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient/tests/test_no_module_ssl.py +0 -0
  48. {geventhttpclient-2.1.1 → geventhttpclient-2.2.1}/src/geventhttpclient.egg-info/dependency_links.txt +0 -0
  49. {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.1.1
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: Based on llhttp, copyright Fedor Indutny, 2018.
6
+ License: This software is licensed under the MIT License.
7
7
 
8
- Python extension is copyright Antonin Amand <antonin.amand@gmail.com>,
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 copy
12
- of this software and associated documentation files (the "Software"), to
13
- deal in the Software without restriction, including without limitation the
14
- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
15
- sell copies of the Software, and to permit persons to whom the Software is
16
- furnished to do so, subject to the following conditions:
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 in
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 OR
22
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
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.txt
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
  [![GitHub Workflow CI Status](https://img.shields.io/github/actions/workflow/status/geventhttpclient/geventhttpclient/test.yml?branch=master&logo=github&style=flat)](https://github.com/geventhttpclient/geventhttpclient/actions)
67
65
  [![PyPI](https://img.shields.io/pypi/v/geventhttpclient.svg?style=flat)](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
- | HTTP Client | RPS |
232
- |----------------------|--------|
233
- | GeventHTTPClient | 7181.2 |
234
- | Urllib3 | 2847.5 |
235
- | Httpx | 1682.9 |
236
- | Requests | 1186.4 |
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
- | HTTP Client | RPS |
167
- |----------------------|--------|
168
- | GeventHTTPClient | 7181.2 |
169
- | Urllib3 | 2847.5 |
170
- | Httpx | 1682.9 |
171
- | Requests | 1186.4 |
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.1.1" # dont forget to update version __init__.py as well
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.txt"}
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"
@@ -1,4 +1,5 @@
1
1
  from distutils.core import setup
2
+
2
3
  from setuptools.extension import Extension
3
4
 
4
5
  http_parser = Extension(
@@ -1,6 +1,6 @@
1
1
  # package
2
2
 
3
- __version__ = "2.1.1" # dont forget to update version in pyproject.toml as well
3
+ __version__ = "2.2.1" # dont forget to update version in pyproject.toml as well
4
4
 
5
5
  from geventhttpclient.client import HTTPClient
6
6
  from geventhttpclient.url import URL
@@ -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
- self._base_url_string = str(self.get_base_url())
150
-
151
- def get_base_url(self):
152
- url = URL()
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(ssl_context_factory)
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