geventhttpclient 2.1.1__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.
Files changed (47) hide show
  1. {geventhttpclient-2.1.1/src/geventhttpclient.egg-info → geventhttpclient-2.2.0}/PKG-INFO +14 -9
  2. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/README.md +11 -8
  3. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/pyproject.toml +6 -2
  4. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/setup.py +1 -0
  5. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/__init__.py +1 -1
  6. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/client.py +1 -1
  7. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/connectionpool.py +46 -10
  8. geventhttpclient-2.2.0/src/geventhttpclient/header.py +277 -0
  9. geventhttpclient-2.2.0/src/geventhttpclient/httplib.py +228 -0
  10. geventhttpclient-2.2.0/src/geventhttpclient/httplib2.py +37 -0
  11. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/response.py +10 -11
  12. geventhttpclient-2.2.0/src/geventhttpclient/tests/conftest.py +7 -0
  13. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_client.py +2 -2
  14. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_headers.py +30 -0
  15. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_httplib.py +9 -0
  16. geventhttpclient-2.2.0/src/geventhttpclient/tests/test_httplib2.py +20 -0
  17. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_parser.py +8 -8
  18. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_useragent.py +1 -3
  19. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/useragent.py +2 -2
  20. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0/src/geventhttpclient.egg-info}/PKG-INFO +14 -9
  21. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient.egg-info/SOURCES.txt +2 -0
  22. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient.egg-info/requires.txt +3 -0
  23. geventhttpclient-2.1.1/src/geventhttpclient/header.py +0 -232
  24. geventhttpclient-2.1.1/src/geventhttpclient/httplib.py +0 -133
  25. geventhttpclient-2.1.1/src/geventhttpclient/tests/conftest.py +0 -3
  26. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/LICENSE.txt +0 -0
  27. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/MANIFEST.in +0 -0
  28. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/ext/Python_compat.h +0 -0
  29. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/ext/_parser.c +0 -0
  30. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/llhttp/LICENSE-MIT +0 -0
  31. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/llhttp/include/llhttp.h +0 -0
  32. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/llhttp/src/api.c +0 -0
  33. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/llhttp/src/http.c +0 -0
  34. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/llhttp/src/llhttp.c +0 -0
  35. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/setup.cfg +0 -0
  36. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/oncert.pem +0 -0
  37. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/server.crt +0 -0
  38. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/server.key +0 -0
  39. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_http_host.py +0 -0
  40. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_keep_alive.py +0 -0
  41. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_network_failures.py +0 -0
  42. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_no_module_ssl.py +0 -0
  43. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_ssl.py +0 -0
  44. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/tests/test_url.py +0 -0
  45. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient/url.py +0 -0
  46. {geventhttpclient-2.1.1 → geventhttpclient-2.2.0}/src/geventhttpclient.egg-info/dependency_links.txt +0 -0
  47. {geventhttpclient-2.1.1 → 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.1.1
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
  [![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
69
  [![PyPI](https://img.shields.io/pypi/v/geventhttpclient.svg?style=flat)](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
- | HTTP Client | RPS |
232
- |----------------------|--------|
233
- | GeventHTTPClient | 7181.2 |
234
- | Urllib3 | 2847.5 |
235
- | Httpx | 1682.9 |
236
- | Requests | 1186.4 |
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
- | 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*
@@ -5,7 +5,7 @@ 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.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
  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.0" # 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
@@ -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(ssl_context_factory)
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