txredisapi 1.5.0__tar.gz → 1.6.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 (44) hide show
  1. {txredisapi-1.5.0/txredisapi.egg-info → txredisapi-1.6.0}/PKG-INFO +2 -2
  2. {txredisapi-1.5.0 → txredisapi-1.6.0}/setup.py +2 -2
  3. txredisapi-1.6.0/tests/test_ipv6.py +98 -0
  4. txredisapi-1.6.0/tests/test_reconnect.py +244 -0
  5. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_sentinel.py +72 -10
  6. txredisapi-1.6.0/tests/test_ssl.py +101 -0
  7. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_subscriber.py +7 -4
  8. {txredisapi-1.5.0 → txredisapi-1.6.0/txredisapi.egg-info}/PKG-INFO +2 -2
  9. {txredisapi-1.5.0 → txredisapi-1.6.0}/txredisapi.egg-info/SOURCES.txt +3 -0
  10. txredisapi-1.6.0/txredisapi.egg-info/requires.txt +1 -0
  11. {txredisapi-1.5.0 → txredisapi-1.6.0}/txredisapi.py +384 -114
  12. txredisapi-1.5.0/txredisapi.egg-info/requires.txt +0 -1
  13. {txredisapi-1.5.0 → txredisapi-1.6.0}/LICENSE +0 -0
  14. {txredisapi-1.5.0 → txredisapi-1.6.0}/MANIFEST.in +0 -0
  15. {txredisapi-1.5.0 → txredisapi-1.6.0}/README.md +0 -0
  16. {txredisapi-1.5.0 → txredisapi-1.6.0}/setup.cfg +0 -0
  17. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/__init__.py +0 -0
  18. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/mixins.py +0 -0
  19. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_basics.py +0 -0
  20. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_bitops.py +0 -0
  21. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_blocking.py +0 -0
  22. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_cancel.py +0 -0
  23. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_connection.py +0 -0
  24. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_connection_charset.py +0 -0
  25. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_hash_ops.py +0 -0
  26. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_hyperloglog.py +0 -0
  27. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_implicit_pipelining.py +0 -0
  28. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_list_ops.py +0 -0
  29. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_multibulk.py +0 -0
  30. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_number_conversions.py +0 -0
  31. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_operations.py +0 -0
  32. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_pipelining.py +0 -0
  33. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_protocol.py +0 -0
  34. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_publish.py +0 -0
  35. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_scan.py +0 -0
  36. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_scripting.py +0 -0
  37. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_sets.py +0 -0
  38. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_sort.py +0 -0
  39. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_sortedsets.py +0 -0
  40. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_transactions.py +0 -0
  41. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_unix_connection.py +0 -0
  42. {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_watch.py +0 -0
  43. {txredisapi-1.5.0 → txredisapi-1.6.0}/txredisapi.egg-info/dependency_links.txt +0 -0
  44. {txredisapi-1.5.0 → txredisapi-1.6.0}/txredisapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: txredisapi
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: non-blocking redis client for python
5
5
  Home-page: http://github.com/IlyaSkriblovsky/txredisapi
6
6
  Author: Alexandre Fiori
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
22
  Requires-Python: >=3.5
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: twisted[tls]
25
+ Requires-Dist: twisted[tls]>=18.7.0
26
26
  Dynamic: author
27
27
  Dynamic: author-email
28
28
  Dynamic: classifier
@@ -23,9 +23,9 @@ long_description = (this_directory / "README.md").read_text()
23
23
 
24
24
  setuptools.setup(
25
25
  name="txredisapi",
26
- version="1.5.0",
26
+ version="1.6.0",
27
27
  py_modules=["txredisapi"],
28
- install_requires=["twisted[tls]"],
28
+ install_requires=["twisted[tls]>=18.7.0"],
29
29
  python_requires=">=3.5",
30
30
  author="Alexandre Fiori",
31
31
  author_email="fiorix@gmail.com",
@@ -0,0 +1,98 @@
1
+ # coding: utf-8
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from twisted.internet import defer, reactor
16
+ from twisted.internet.address import IPv6Address
17
+ from twisted.internet.error import CannotListenError
18
+ from twisted.internet.interfaces import IHostnameResolver, IHostResolution
19
+ from twisted.trial import unittest
20
+ from zope.interface import implementer
21
+
22
+ import txredisapi as redis
23
+
24
+ from tests.test_sentinel import FakeRedisFactory
25
+
26
+
27
+ @implementer(IHostResolution)
28
+ class Resolution(object):
29
+ def __init__(self, name):
30
+ self.name = name
31
+
32
+ def cancel(self):
33
+ pass
34
+
35
+
36
+ @implementer(IHostnameResolver)
37
+ class IPv6OnlyResolver(object):
38
+ """
39
+ Resolves a single hostname to a single IPv6 address and knows nothing
40
+ else, just like a name that has an AAAA record and no A record.
41
+ """
42
+
43
+ def __init__(self, hostname, address="::1"):
44
+ self.hostname = hostname
45
+ self.address = address
46
+
47
+ def resolveHostName(self, resolutionReceiver, hostName, portNumber=0,
48
+ addressTypes=None, transportSemantics="TCP"):
49
+ resolution = Resolution(hostName)
50
+ resolutionReceiver.resolutionBegan(resolution)
51
+ if hostName == self.hostname and \
52
+ (addressTypes is None or IPv6Address in addressTypes):
53
+ resolutionReceiver.addressResolved(
54
+ IPv6Address("TCP", self.address, portNumber))
55
+ resolutionReceiver.resolutionComplete()
56
+ return resolution
57
+
58
+
59
+ class TestIPv6Connections(unittest.TestCase):
60
+ """
61
+ Servers reachable over IPv6 only must be reachable as well: names are
62
+ resolved with getaddrinfo(), not with the IPv4-only gethostbyname().
63
+ """
64
+
65
+ hostname = "ipv6-only.invalid"
66
+
67
+ # connecting to the loopback is instant; don't sit through trial's default
68
+ # timeout if it ever stops working
69
+ timeout = 15
70
+
71
+ def setUp(self):
72
+ try:
73
+ self.listener = reactor.listenTCP(0, FakeRedisFactory(),
74
+ interface="::1")
75
+ except CannotListenError:
76
+ raise unittest.SkipTest("no IPv6 loopback available")
77
+
78
+ self.port = self.listener.getHost().port
79
+ self.addCleanup(self.listener.stopListening)
80
+
81
+ @defer.inlineCallbacks
82
+ def test_aaaa_only_hostname(self):
83
+ previous = reactor.installNameResolver(IPv6OnlyResolver(self.hostname))
84
+ self.addCleanup(reactor.installNameResolver, previous)
85
+
86
+ db = yield redis.Connection(self.hostname, self.port, reconnect=False)
87
+ self.addCleanup(db.disconnect)
88
+
89
+ role = yield db.role()
90
+ self.assertEqual(role[0], "master")
91
+
92
+ @defer.inlineCallbacks
93
+ def test_ipv6_literal(self):
94
+ db = yield redis.Connection("::1", self.port, reconnect=False)
95
+ self.addCleanup(db.disconnect)
96
+
97
+ role = yield db.role()
98
+ self.assertEqual(role[0], "master")
@@ -0,0 +1,244 @@
1
+ # coding: utf-8
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from twisted.internet import defer, error, reactor, task
16
+ from twisted.internet.endpoints import HostnameEndpoint
17
+ from twisted.internet.interfaces import IStreamClientEndpoint
18
+ from twisted.internet.protocol import Factory
19
+ from twisted.trial import unittest
20
+ from zope.interface import implementer
21
+
22
+ import txredisapi as redis
23
+
24
+ from tests.test_sentinel import TrackingRedisFactory
25
+
26
+
27
+ @implementer(IStreamClientEndpoint)
28
+ class FailingEndpoint(object):
29
+ """
30
+ Endpoint that fails synchronously, the way UNIXClientEndpoint does when
31
+ the socket does not exist.
32
+ """
33
+
34
+ def __init__(self):
35
+ self.attempts = 0
36
+
37
+ def connect(self, protocolFactory):
38
+ self.attempts += 1
39
+ return defer.fail(error.ConnectionRefusedError())
40
+
41
+
42
+ @implementer(IStreamClientEndpoint)
43
+ class GatedEndpoint(object):
44
+ """
45
+ Endpoint that lets the first connection through and refuses the ones
46
+ after it, but not before the given gate fires.
47
+ """
48
+
49
+ def __init__(self, endpoint, gate):
50
+ self.endpoint = endpoint
51
+ self.gate = gate
52
+ self.attempts = 0
53
+
54
+ def connect(self, protocolFactory):
55
+ self.attempts += 1
56
+ if self.attempts == 1:
57
+ return self.endpoint.connect(protocolFactory)
58
+
59
+ refused = defer.Deferred()
60
+ self.gate.addCallback(
61
+ lambda _: refused.errback(error.ConnectionRefusedError()))
62
+ return refused
63
+
64
+
65
+ class TestReconnect(unittest.TestCase):
66
+ timeout = 30
67
+
68
+ def setUp(self):
69
+ self.server = TrackingRedisFactory()
70
+ self.listener = reactor.listenTCP(0, self.server,
71
+ interface="127.0.0.1")
72
+ self.port = self.listener.getHost().port
73
+ self.addCleanup(self.listener.stopListening)
74
+
75
+ def endpoint(self):
76
+ return HostnameEndpoint(reactor, "127.0.0.1", self.port)
77
+
78
+ @defer.inlineCallbacks
79
+ def waitFor(self, predicate, message):
80
+ deadline = reactor.seconds() + 15
81
+ while not predicate():
82
+ if reactor.seconds() > deadline:
83
+ self.fail(message)
84
+ yield task.deferLater(reactor, 0.05, lambda: None)
85
+
86
+ @defer.inlineCallbacks
87
+ def closedPort(self):
88
+ listener = reactor.listenTCP(0, Factory(), interface="127.0.0.1")
89
+ port = listener.getHost().port
90
+ yield listener.stopListening()
91
+ return port
92
+
93
+ @defer.inlineCallbacks
94
+ def test_reconnects(self):
95
+ db = yield redis.Connection("127.0.0.1", self.port)
96
+ self.addCleanup(db.disconnect)
97
+ yield db.role()
98
+
99
+ self.server.protocols[0].transport.loseConnection()
100
+
101
+ yield self.waitFor(lambda: len(self.server.protocols) > 1,
102
+ "connection has not been reestablished")
103
+ role = yield db.role()
104
+ self.assertEqual(role[0], "master")
105
+
106
+ @defer.inlineCallbacks
107
+ def test_does_not_reconnect_when_disabled(self):
108
+ # short enough for the wait below to cover several attempts
109
+ self.patch(redis.RedisFactory, "initialDelay", 0.05)
110
+
111
+ db = yield redis.Connection("127.0.0.1", self.port, reconnect=False)
112
+ self.addCleanup(db.disconnect)
113
+ yield db.role()
114
+
115
+ self.server.protocols[0].transport.loseConnection()
116
+ yield self.waitFor(lambda: db._factory.size == 0,
117
+ "connection has not been dropped")
118
+
119
+ # give a reconnect a chance to happen, it should not
120
+ yield task.deferLater(reactor, 0.3, lambda: None)
121
+ self.assertEqual(len(self.server.protocols), 1)
122
+
123
+ yield self.assertFailure(db.role(), redis.ConnectionError)
124
+
125
+ @defer.inlineCallbacks
126
+ def test_failed_connection_is_reported(self):
127
+ port = yield self.closedPort()
128
+ yield self.assertFailure(
129
+ redis.Connection("127.0.0.1", port, reconnect=False), ValueError)
130
+
131
+ @defer.inlineCallbacks
132
+ def test_first_failure_is_final(self):
133
+ # long enough for a second attempt to be noticed as a hang
134
+ self.patch(redis.RedisFactory, "initialDelay", 30)
135
+
136
+ endpoint = FailingEndpoint()
137
+ factory = redis.RedisFactory(None, dbid=None, poolsize=1)
138
+ factory.reconnect = False
139
+ deferred = factory.deferred
140
+ factory.startConnecting(endpoint)
141
+
142
+ yield self.assertFailure(deferred, ValueError)
143
+ self.assertEqual(endpoint.attempts, 1)
144
+
145
+ @defer.inlineCallbacks
146
+ def test_failed_pool_startup_closes_connections(self):
147
+ gate = defer.Deferred()
148
+ endpoint = GatedEndpoint(self.endpoint(), gate)
149
+
150
+ factory = redis.RedisFactory(None, dbid=None, poolsize=2)
151
+ factory.reconnect = False
152
+ deferred = factory.deferred
153
+ factory.startConnecting(endpoint)
154
+
155
+ yield self.waitFor(lambda: factory.size == 1,
156
+ "the first connection has not been established")
157
+ gate.callback(None)
158
+
159
+ yield self.assertFailure(deferred, ValueError)
160
+
161
+ # the caller got an error instead of the handler, so it can't close
162
+ # the connection that did come up - we have to
163
+ yield self.waitFor(lambda: not self.server.protocols[0].connected,
164
+ "the established connection has not been closed")
165
+
166
+ @defer.inlineCallbacks
167
+ def test_failed_lazy_pool_keeps_working_connections(self):
168
+ gate = defer.Deferred()
169
+ endpoint = GatedEndpoint(self.endpoint(), gate)
170
+
171
+ factory = redis.RedisFactory(None, dbid=None, poolsize=2, isLazy=True)
172
+ factory.reconnect = False
173
+ # nobody consumes the Deferred of a lazy factory
174
+ factory.deferred.addErrback(lambda failure: None)
175
+ factory.startConnecting(endpoint)
176
+
177
+ yield self.waitFor(lambda: factory.size == 1,
178
+ "the first connection has not been established")
179
+ gate.callback(None)
180
+ yield task.deferLater(reactor, 0.3, lambda: None)
181
+
182
+ # the handler is in the caller's hands already, so the connection that
183
+ # did come up stays usable
184
+ self.assertEqual(factory.size, 1)
185
+ role = yield factory.handler.role()
186
+ self.assertEqual(role[0], "master")
187
+
188
+ yield factory.handler.disconnect()
189
+
190
+ @defer.inlineCallbacks
191
+ def test_disconnect_cancels_reconnection(self):
192
+ # long enough for the reconnection to still be scheduled below
193
+ self.patch(redis.RedisFactory, "initialDelay", 0.5)
194
+
195
+ db = yield redis.Connection("127.0.0.1", self.port)
196
+ yield db.role()
197
+
198
+ self.server.protocols[0].transport.loseConnection()
199
+ yield self.waitFor(lambda: db._factory.size == 0,
200
+ "connection has not been dropped")
201
+
202
+ # a scheduled reconnection attempt must not survive disconnect()
203
+ yield db.disconnect()
204
+ yield task.deferLater(reactor, 1, lambda: None)
205
+ self.assertEqual(len(self.server.protocols), 1)
206
+
207
+ @defer.inlineCallbacks
208
+ def test_stop_trying_from_connection_made(self):
209
+ # the pattern examples/subscriber.py shows: give up on the connection
210
+ # from inside connectionMade, while the endpoint is still finishing
211
+ # it. Neither hook chains to the base protocol, the way protocols
212
+ # written for a SubscriberFactory usually don't
213
+ class StoppingProtocol(redis.RedisProtocol):
214
+ def connectionMade(self):
215
+ self.factory.stopTrying()
216
+ self.transport.loseConnection()
217
+
218
+ def connectionLost(self, reason):
219
+ pass
220
+
221
+ factory = redis.RedisFactory(None, dbid=None, poolsize=1)
222
+ factory.protocol = StoppingProtocol
223
+ factory.startConnecting(self.endpoint())
224
+
225
+ yield self.waitFor(lambda: self.server.protocols and
226
+ not self.server.protocols[0].connected,
227
+ "the connection has not been closed")
228
+
229
+ yield task.deferLater(reactor, 0.3, lambda: None)
230
+ self.assertEqual(len(self.server.protocols), 1)
231
+
232
+ def test_dead_connection_is_not_pooled(self):
233
+ factory = redis.RedisFactory(None, dbid=None, poolsize=1)
234
+ conn = factory.buildProtocol(None)
235
+ # died during the reactor turn addConnection is delayed by
236
+ conn.connected = 0
237
+
238
+ factory.addConnection(conn)
239
+ self.assertEqual(factory.size, 0)
240
+
241
+ def test_retry_delay_does_not_overflow(self):
242
+ factory = redis.RedisFactory(None, dbid=None, poolsize=1)
243
+ # a long outage keeps increasing the attempt number
244
+ self.assertLess(factory.retryDelay(10 ** 6), factory.maxDelay * 2)
@@ -1,5 +1,5 @@
1
1
  import sys
2
- from twisted.internet import defer, reactor
2
+ from twisted.internet import defer, reactor, task
3
3
  from twisted.internet.protocol import Factory
4
4
  from twisted.trial.unittest import TestCase
5
5
 
@@ -106,6 +106,18 @@ class FakeRedisFactory(Factory):
106
106
  role = ["master", 0, ["127.0.0.1", 63791, 0]]
107
107
 
108
108
 
109
+ class TrackingRedisFactory(FakeRedisFactory):
110
+ """Fake server that keeps every protocol it has built."""
111
+
112
+ def __init__(self):
113
+ self.protocols = []
114
+
115
+ def buildProtocol(self, addr):
116
+ protocol = FakeRedisFactory.buildProtocol(self, addr)
117
+ self.protocols.append(protocol)
118
+ return protocol
119
+
120
+
109
121
  class FakeAuthenticatedRedisFactory(FakeRedisFactory):
110
122
  def __init__(self, requirepass):
111
123
  self.requirepass = requirepass
@@ -297,6 +309,19 @@ class TestConnectViaSentinel(TestCase):
297
309
  reactor.callLater(secs, d.callback, None)
298
310
  return d
299
311
 
312
+ @defer.inlineCallbacks
313
+ def _waitFor(self, predicate, message):
314
+ deadline = reactor.seconds() + 5
315
+ while not predicate():
316
+ if reactor.seconds() > deadline:
317
+ self.fail(message)
318
+ yield self._delay(0.02)
319
+
320
+ def _pool_is_at(self, conn, port):
321
+ pool = conn._factory.pool
322
+ return len(pool) == 3 and all(p.transport.getPeer().port == port
323
+ for p in pool)
324
+
300
325
  @defer.inlineCallbacks
301
326
  def test_drop_all_when_master_changes(self):
302
327
  # When master address change detected, factory should drop and reestablish
@@ -304,10 +329,8 @@ class TestConnectViaSentinel(TestCase):
304
329
 
305
330
  conn = self.client.master_for("test", poolsize=3)
306
331
  yield conn.role() # wait for connection
307
-
308
- addrs = [proto.transport.getPeer() for proto in conn._factory.pool]
309
- self.assertTrue(len(addrs), 3)
310
- self.assertTrue(all(addr.port == self.master_port for addr in addrs))
332
+ yield self._waitFor(lambda: self._pool_is_at(conn, self.master_port),
333
+ "the pool has not been filled from the master")
311
334
 
312
335
  # Change master address at sentinel and change role of the slave to master
313
336
  self.fake_sentinel.master_addr = ("127.0.0.1", self.slave_port)
@@ -316,11 +339,9 @@ class TestConnectViaSentinel(TestCase):
316
339
  # Force reconnection of one connection
317
340
  conn._factory.pool[0].transport.loseConnection()
318
341
 
319
- # After a short time all connections should be to the new master
320
- yield self._delay(0.2)
321
- addrs = [proto.transport.getPeer() for proto in conn._factory.pool]
322
- self.assertTrue(len(addrs), 3)
323
- self.assertTrue(all(addr.port == self.slave_port for addr in addrs))
342
+ # All of the connections should end up at the new master
343
+ yield self._waitFor(lambda: self._pool_is_at(conn, self.slave_port),
344
+ "the connections have not moved to the new master")
324
345
 
325
346
  yield conn.disconnect()
326
347
 
@@ -355,3 +376,44 @@ class TestAuthViaSentinel(TestCase):
355
376
  reply = yield conn.role()
356
377
  self.assertEqual(reply[0], "master")
357
378
  yield conn.disconnect()
379
+
380
+
381
+ class TestSentinelRetries(TestCase):
382
+
383
+ master_port = 36379
384
+ sentinel_port = 46379
385
+
386
+ def setUp(self):
387
+ self.fake_master = TrackingRedisFactory()
388
+ self.fake_master.role = ["slave", "127.0.0.1", 63790, "connected", 0]
389
+ self.master_listener = reactor.listenTCP(self.master_port,
390
+ self.fake_master)
391
+
392
+ self.fake_sentinel = FakeSentinelFactory()
393
+ self.fake_sentinel.master_addr = ("127.0.0.1", self.master_port)
394
+ self.fake_sentinel.slave_addrs = []
395
+ self.fake_sentinel.slave_flags = []
396
+ self.sentinel_listener = reactor.listenTCP(self.sentinel_port,
397
+ self.fake_sentinel)
398
+
399
+ self.client = Sentinel([("127.0.0.1", self.sentinel_port)])
400
+ self.client.discovery_timeout = 1
401
+
402
+ @defer.inlineCallbacks
403
+ def tearDown(self):
404
+ yield self.client.disconnect()
405
+ yield self.sentinel_listener.stopListening()
406
+ yield self.master_listener.stopListening()
407
+
408
+ @defer.inlineCallbacks
409
+ def test_wrong_role_backs_off(self):
410
+ # A node that never has the role we are looking for is dropped by
411
+ # SentinelRedisProtocol as soon as it replies to ROLE. Such a
412
+ # connection must not reset the backoff, or we would reconnect to it
413
+ # in a tight loop
414
+ conn = self.client.master_for("test")
415
+
416
+ yield task.deferLater(reactor, 0.5, lambda: None)
417
+ self.assertLess(len(self.fake_master.protocols), 5)
418
+
419
+ yield conn.disconnect()
@@ -0,0 +1,101 @@
1
+ # coding: utf-8
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import datetime
16
+
17
+ from cryptography import x509
18
+ from cryptography.hazmat.primitives import hashes
19
+ from cryptography.hazmat.primitives.asymmetric import rsa
20
+ from cryptography.x509.oid import NameOID
21
+ from OpenSSL import crypto
22
+ from twisted.internet import defer, reactor, ssl
23
+ from twisted.trial import unittest
24
+
25
+ import txredisapi as redis
26
+
27
+ from tests.test_sentinel import FakeRedisFactory
28
+
29
+
30
+ _options = []
31
+
32
+
33
+ def selfSignedOptions():
34
+ """
35
+ Server side TLS options with a self-signed certificate, built once because
36
+ generating a key is slow.
37
+
38
+ twisted.internet.ssl.KeyPair.selfSignedCert() would be shorter, but it
39
+ needs OpenSSL.crypto.X509Req, which recent pyOpenSSL releases no longer
40
+ have.
41
+ """
42
+ if not _options:
43
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
44
+ name = x509.Name(
45
+ [x509.NameAttribute(NameOID.COMMON_NAME, u"localhost")])
46
+ now = datetime.datetime.now(datetime.timezone.utc)
47
+ certificate = (
48
+ x509.CertificateBuilder()
49
+ .subject_name(name)
50
+ .issuer_name(name)
51
+ .public_key(key.public_key())
52
+ .serial_number(1)
53
+ .not_valid_before(now - datetime.timedelta(days=1))
54
+ .not_valid_after(now + datetime.timedelta(days=1))
55
+ .sign(key, hashes.SHA256())
56
+ )
57
+ _options.append(ssl.CertificateOptions(
58
+ privateKey=crypto.PKey.from_cryptography_key(key),
59
+ certificate=crypto.X509.from_cryptography(certificate)))
60
+
61
+ return _options[0]
62
+
63
+
64
+ class TestSSLConnections(unittest.TestCase):
65
+ timeout = 15
66
+
67
+ def setUp(self):
68
+ self.listener = reactor.listenSSL(0, FakeRedisFactory(),
69
+ selfSignedOptions(),
70
+ interface="127.0.0.1")
71
+ self.port = self.listener.getHost().port
72
+ self.addCleanup(self.listener.stopListening)
73
+
74
+ @defer.inlineCallbacks
75
+ def test_context_factory(self):
76
+ db = yield redis.Connection(
77
+ "127.0.0.1", self.port, reconnect=False,
78
+ ssl_context_factory=ssl.ClientContextFactory())
79
+ self.addCleanup(db.disconnect)
80
+
81
+ role = yield db.role()
82
+ self.assertEqual(role[0], "master")
83
+
84
+ @defer.inlineCallbacks
85
+ def test_ssl_true(self):
86
+ db = yield redis.Connection("127.0.0.1", self.port, reconnect=False,
87
+ ssl_context_factory=True)
88
+ self.addCleanup(db.disconnect)
89
+
90
+ role = yield db.role()
91
+ self.assertEqual(role[0], "master")
92
+
93
+ @defer.inlineCallbacks
94
+ def test_pool(self):
95
+ db = yield redis.ConnectionPool("127.0.0.1", self.port, poolsize=2,
96
+ reconnect=False,
97
+ ssl_context_factory=True)
98
+ self.addCleanup(db.disconnect)
99
+
100
+ role = yield db.role()
101
+ self.assertEqual(role[0], "master")
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from twisted.internet import defer, reactor
16
+ from twisted.internet.endpoints import HostnameEndpoint
16
17
  from twisted.trial import unittest
17
18
 
18
19
  import txredisapi as redis
@@ -24,8 +25,9 @@ class TestSubscriberProtocol(unittest.TestCase):
24
25
  @defer.inlineCallbacks
25
26
  def setUp(self):
26
27
  factory = redis.SubscriberFactory()
27
- factory.continueTrying = False
28
- reactor.connectTCP(REDIS_HOST, REDIS_PORT, factory)
28
+ factory.reconnect = False
29
+ factory.startConnecting(
30
+ HostnameEndpoint(reactor, REDIS_HOST, REDIS_PORT))
29
31
  self.db = yield factory.deferred
30
32
 
31
33
  @defer.inlineCallbacks
@@ -125,8 +127,9 @@ class TestAuthenticatedSubscriberProtocol(unittest.TestCase):
125
127
  factory = redis.RedisFactory(None, dbid=0, poolsize=1,
126
128
  password="password")
127
129
  factory.protocol = redis.SubscriberProtocol
128
- factory.continueTrying = False
129
- reactor.connectTCP(REDIS_HOST, REDIS_PORT, factory)
130
+ factory.reconnect = False
131
+ factory.startConnecting(
132
+ HostnameEndpoint(reactor, REDIS_HOST, REDIS_PORT))
130
133
  self.db = yield factory.deferred
131
134
 
132
135
  @defer.inlineCallbacks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: txredisapi
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: non-blocking redis client for python
5
5
  Home-page: http://github.com/IlyaSkriblovsky/txredisapi
6
6
  Author: Alexandre Fiori
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
22
  Requires-Python: >=3.5
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: twisted[tls]
25
+ Requires-Dist: twisted[tls]>=18.7.0
26
26
  Dynamic: author
27
27
  Dynamic: author-email
28
28
  Dynamic: classifier
@@ -15,6 +15,7 @@ tests/test_connection_charset.py
15
15
  tests/test_hash_ops.py
16
16
  tests/test_hyperloglog.py
17
17
  tests/test_implicit_pipelining.py
18
+ tests/test_ipv6.py
18
19
  tests/test_list_ops.py
19
20
  tests/test_multibulk.py
20
21
  tests/test_number_conversions.py
@@ -22,12 +23,14 @@ tests/test_operations.py
22
23
  tests/test_pipelining.py
23
24
  tests/test_protocol.py
24
25
  tests/test_publish.py
26
+ tests/test_reconnect.py
25
27
  tests/test_scan.py
26
28
  tests/test_scripting.py
27
29
  tests/test_sentinel.py
28
30
  tests/test_sets.py
29
31
  tests/test_sort.py
30
32
  tests/test_sortedsets.py
33
+ tests/test_ssl.py
31
34
  tests/test_subscriber.py
32
35
  tests/test_transactions.py
33
36
  tests/test_unix_connection.py
@@ -0,0 +1 @@
1
+ twisted[tls]>=18.7.0