├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.tests ├── examples ├── benchmark.py ├── cyclone_demo.py ├── monitor.py ├── sharding.py ├── ssl_connection.py ├── subscriber.py ├── test_rest.sh ├── transaction.py ├── twistedweb_server.py ├── wordfreq.README └── wordfreq.py ├── fig.yml ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── chash_distribution.py ├── mixins.py ├── test_basics.py ├── test_bitops.py ├── test_blocking.py ├── test_cancel.py ├── test_connection.py ├── test_connection_charset.py ├── test_hash_ops.py ├── test_hyperloglog.py ├── test_implicit_pipelining.py ├── test_list_ops.py ├── test_multibulk.py ├── test_number_conversions.py ├── test_operations.py ├── test_pipelining.py ├── test_protocol.py ├── test_publish.py ├── test_scan.py ├── test_scripting.py ├── test_sentinel.py ├── test_sets.py ├── test_sort.py ├── test_sortedsets.py ├── test_subscriber.py ├── test_transactions.py ├── test_unix_connection.py └── test_watch.py └── txredisapi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | build 4 | _trial_temp 5 | tmp 6 | dist 7 | txredisapi.egg-info 8 | .idea/ 9 | atlassian-ide-plugin.xml 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | services: 3 | - redis 4 | 5 | python: 6 | - 2.7 7 | - 3.5 8 | - 3.6 9 | - 3.7 10 | env: 11 | - TEST_HIREDIS=0 12 | - TEST_HIREDIS=1 13 | notifications: 14 | irc: 15 | - "irc.freenode.org#cycloneweb" 16 | install: 17 | - if [[ $TEST_HIREDIS == '1' ]]; then pip install hiredis; fi 18 | - pip install . mock 19 | script: PYTHONPATH=. trial tests/ 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Release 1.4.11 (2025-04-11) 4 | 5 | ### Bugfixes 6 | 7 | - defer.returnValue() replaced with return to fix warnings when used with newer Twisted versions 8 | 9 | --- 10 | 11 | ## Release 1.4.10 (2023-07-06) 12 | 13 | ### Bugfixes 14 | 15 | - Fix SubscriberProtocol to work with charset=None (#150) 16 | 17 | --- 18 | 19 | ## Release 1.4.9 (2023-03-18) 20 | 21 | ### Features 22 | 23 | - SSL connection support 24 | 25 | --- 26 | 27 | ## Release 1.4.7 (2019-12-03) 28 | 29 | ### Bugfixes 30 | 31 | - SentinelRedisProtocol.connectionMade not returns Deferred so subclasses might 32 | schedule interaction when connection is ready 33 | 34 | --- 35 | 36 | ## Release 1.4.6 (2019-11-20) 37 | 38 | ### Bugfixes 39 | 40 | - Fixed authentication with Sentinel 41 | 42 | - replyTimeout connection argument fixed. All query methods except `blpop()`, 43 | `brpop()`, `brpoplpush()` now raise `TimeoutError` if reply wasn't received 44 | within `replyTimeout` seconds. 45 | 46 | - allow any commands to be sent via SubscriberProtocol 47 | 48 | - Fixed bug in handling responses from Redis when MULTI is issued right after 49 | another bulk command (SMEMBERS for example) 50 | 51 | --- 52 | 53 | ## Release 1.4.5 (2017-11-08) 54 | 55 | ### Features 56 | 57 | - Python 2.6 support 58 | 59 | ### Bugfixes 60 | 61 | - Increasing memory consumption after many subscribe & unsubscribe commands 62 | 63 | --- 64 | 65 | ## Release 1.4.4 (2016-11-16) 66 | 67 | ### Features 68 | 69 | - Redis Sentinel support 70 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | The following is a list of people who have contributed to 4 | **txredisapi**. If you belong here and are missing please let us know 5 | (or send a pull request after adding yourself to the list): 6 | 7 | - Aaron Gallagher 8 | - Adam Goodman 9 | - Alexandre Fiori 10 | - Alexandr Emelin 11 | - Alexey Kryuchkov 12 | - Christoph Tavan 13 | - Dana Powers 14 | - Dan Bravender 15 | - dgvncsz0f 16 | - Evgeny Tataurov 17 | - Gleicon Moraes 18 | - Ilia Glazkov 19 | - Ilya Skriblovsky 20 | - Ingmar Steen 21 | - Jakub Matys 22 | - Jani Jappinen 23 | - Jeethu Rao 24 | - Jeremy Archer 25 | - jettify 26 | - Keith Bourgoin 27 | - Lucas Fontes 28 | - Matt Pizzimenti 29 | - Max Lavrenov 30 | - minus 31 | - Tommy Wang 32 | - Tom van Neerijnen 33 | - Tony Lazarew 34 | - tpena 35 | - Viktor Kuptsov 36 | - Zach Steindler 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Gleicon 3 | 4 | RUN apt-get update && \ 5 | apt-get -y upgrade && \ 6 | apt-get -y install -q build-essential && \ 7 | apt-get -y install -q python-dev libffi-dev libssl-dev python-pip 8 | 9 | RUN pip install service_identity pycrypto && \ 10 | pip install twisted && \ 11 | pip install hiredis 12 | 13 | ADD . /txredisapi 14 | CMD ["bash"] 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include tests/__init__.py 2 | include tests/mixins.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | txredisapi 2 | ========== 3 | 4 | [![Build Status](https://secure.travis-ci.org/IlyaSkriblovsky/txredisapi.png)](http://travis-ci.org/IlyaSkriblovsky/txredisapi) 5 | 6 | 7 | *For the latest source code, see * 8 | 9 | 10 | ``txredisapi`` is a non-blocking client driver for the [redis](http://redis.io) 11 | database, written in Python. It uses [Twisted](http://twistedmatrix.com) for 12 | the asynchronous communication with redis. 13 | 14 | It started as a fork of the original 15 | [redis protocol for twisted](http://pypi.python.org/pypi/txredis/), and evolved 16 | into a more robust, reliable, and complete solution for applications like web 17 | servers. These types of applications often need a fault-tolerant pool of 18 | connections with multiple redis servers, making it possible to easily develop 19 | and maintain distributed systems. 20 | 21 | Most of the [redis commands](http://redis.io/commands) are supported, as well 22 | as other features such as silent reconnection, connection pools, and automatic 23 | sharding. 24 | 25 | This driver is distributed as part of the [cyclone](http://cyclone.io) web 26 | framework. 27 | 28 | ### Changelog ### 29 | 30 | See [CHANGELOG.md](CHANGELOG.md) 31 | 32 | ### Features ### 33 | 34 | - Connection Pools 35 | - Lazy Connections 36 | - Automatic Sharding 37 | - Automatic Reconnection 38 | - Connection using Redis Sentinel 39 | - Publish/Subscribe (PubSub) 40 | - Transactions 41 | - Unix Socket Connections 42 | 43 | 44 | Install 45 | ------- 46 | 47 | Bear in mind that ``txredisapi.py`` is pure-python, in a single file. 48 | Thus, there's absolutely no need to install it. Instead, just copy it to your 49 | project directory and start using. 50 | 51 | Latest source code is at . 52 | 53 | If you have [cyclone](http://cyclone.io), you probably already have it too. 54 | Try the following: 55 | 56 | $ python 57 | >>> import cyclone.redis 58 | >>> cyclone.redis.version 59 | '1.0' 60 | 61 | However, if you really really insist in installing, get it from pypi: 62 | 63 | pip install txredisapi 64 | 65 | 66 | ### Unit Tests ### 67 | 68 | [Twisted Trial](http://twistedmatrix.com/trac/wiki/TwistedTrial) unit tests 69 | are available. Just start redis, and run ``trial ./tests``. 70 | If *unix sockets* are disabled in redis, it will silently skip those tests. 71 | 72 | Make sure you run `redis-cli flushall` to clean up redis after the tests. 73 | 74 | Usage 75 | ----- 76 | 77 | First thing to do is choose what type of connection you want. The driver 78 | supports single connection, connection pools, sharded connections (with 79 | automatic distribution based on a built-in consistent hashing algorithm), 80 | sharded connection pools, and all of these different types can be *lazy*, 81 | which is explained later (because I'm lazy now). 82 | 83 | Basically, you want normal connections for simple batch clients that connect 84 | to redis, execute a couple of commands and disconnect - like crawlers, etc. 85 | 86 | Example: 87 | 88 | #!/usr/bin/env python 89 | # coding: utf-8 90 | 91 | import txredisapi as redis 92 | 93 | from twisted.internet import defer 94 | from twisted.internet import reactor 95 | 96 | 97 | @defer.inlineCallbacks 98 | def main(): 99 | rc = yield redis.Connection() 100 | print rc 101 | 102 | yield rc.set("foo", "bar") 103 | v = yield rc.get("foo") 104 | print "foo:", repr(v) 105 | 106 | yield rc.disconnect() 107 | 108 | 109 | if __name__ == "__main__": 110 | main().addCallback(lambda ign: reactor.stop()) 111 | reactor.run() 112 | 113 | 114 | Easily switch between ``redis.Connection()`` and ``redis.ConnectionPool()`` 115 | with absolutely no changes to the logic of your program. 116 | 117 | These are all the supported methods for connecting to Redis:: 118 | 119 | Connection(host, port, dbid, reconnect, charset) 120 | lazyConnection(host, port, dbid, reconnect, charset) 121 | 122 | ConnectionPool(host, port, dbid, poolsize, reconnect, charset) 123 | lazyConnectionPool(host, port, dbid, poolsize, reconnect, charset) 124 | 125 | ShardedConnection(hosts, dbid, reconnect, charset) 126 | lazyShardedConnection(hosts, dbid, reconnect, charset) 127 | 128 | ShardedConnectionPool(hosts, dbid, poolsize, reconnect, charset) 129 | lazyShardedConnectionPool(hosts, dbid, poolsize, reconnect, charset) 130 | 131 | UnixConnection(path, dbid, reconnect, charset) 132 | lazyUnixConnection(path, dbid, reconnect, charset) 133 | 134 | UnixConnectionPool(unix, dbid, poolsize, reconnect, charset) 135 | lazyUnixConnectionPool(unix, dbid, poolsize, reconnect, charset) 136 | 137 | ShardedUnixConnection(paths, dbid, reconnect, charset) 138 | lazyShardedUnixConnection(paths, dbid, reconnect, charset) 139 | 140 | ShardedUnixConnectionPool(paths, dbid, poolsize, reconnect, charset) 141 | lazyShardedUnixConnectionPool(paths, dbid, poolsize, reconnect, charset) 142 | 143 | 144 | The arguments are: 145 | 146 | - host: the IP address or hostname of the redis server. [default: localhost] 147 | - port: port number of the redis server. [default: 6379] 148 | - path: path of redis server's socket [default: /tmp/redis.sock] 149 | - dbid: database id of redis server. [default: 0] 150 | - poolsize: how many connections to make. [default: 10] 151 | - reconnect: auto-reconnect if connection is lost. [default: True] 152 | - charset: string encoding. Do not decode/encode strings if None. 153 | [default: utf-8] 154 | - hosts (for sharded): list of ``host:port`` pairs. [default: None] 155 | - paths (for sharded): list of ``pathnames``. [default: None] 156 | - password: password for the redis server. [default: None] 157 | - ssl_context_factory: Either a boolean indicating wether to use SSL/TLS or a specific `ClientContextFactory`. [default: False] 158 | 159 | 160 | ### Connection Handlers ### 161 | 162 | All connection methods return a connection handler object at some point. 163 | 164 | Normal connections (not lazy) return a deferred, which is fired with the 165 | connection handler after the connection is established. 166 | 167 | In case of connection pools, it will only fire the callback after all 168 | connections are set up, and ready. 169 | 170 | Connection handler is the client interface with redis. It accepts all the 171 | commands supported by redis, such as ``get``, ``set``, etc. It is the ``rc`` 172 | object in the example below. 173 | 174 | Connection handlers will automatically select one of the available connections 175 | in connection pools, and automatically reconnect to redis when necessary. 176 | 177 | If the connection with redis is lost, all commands will raise the 178 | ``ConnectionError`` exception, to indicate that there's no active connection. 179 | However, if the ``reconnect`` argument was set to ``True`` during the 180 | initialization, it will continuosly try to reconnect, in background. 181 | 182 | Example: 183 | 184 | #!/usr/bin/env python 185 | # coding: utf-8 186 | 187 | import txredisapi as redis 188 | 189 | from twisted.internet import defer 190 | from twisted.internet import reactor 191 | 192 | 193 | def sleep(n): 194 | d = defer.Deferred() 195 | reactor.callLater(5, lambda *ign: d.callback(None)) 196 | return d 197 | 198 | 199 | @defer.inlineCallbacks 200 | def main(): 201 | rc = yield redis.ConnectionPool() 202 | print rc 203 | 204 | # set 205 | yield rc.set("foo", "bar") 206 | 207 | # sleep, so you can kill redis 208 | print "sleeping for 5s, kill redis now..." 209 | yield sleep(5) 210 | 211 | try: 212 | v = yield rc.get("foo") 213 | print "foo:", v 214 | 215 | yield rc.disconnect() 216 | except redis.ConnectionError, e: 217 | print str(e) 218 | 219 | 220 | if __name__ == "__main__": 221 | main().addCallback(lambda ign: reactor.stop()) 222 | reactor.run() 223 | 224 | 225 | ### Lazy Connections ### 226 | 227 | This type of connection will immediately return the connection handler object, 228 | even before the connection is made. 229 | 230 | It will start the connection, (or connections, in case of connection pools) in 231 | background, and automatically reconnect if necessary. 232 | 233 | You want lazy connections when you're writing servers, like web servers, or 234 | any other type of server that should not wait for the redis connection during 235 | the initialization of the program. 236 | 237 | The example below is a web application, which will expose redis set, get and 238 | delete commands over HTTP. 239 | 240 | If the database connection is down (either because redis is not running, or 241 | whatever reason), the web application will start normally. If connection is 242 | lost during the operation, nothing will change. 243 | 244 | When there's no connection, all commands will fail, therefore the web 245 | application will respond with HTTP 503 (Service Unavailable). It will resume to 246 | normal once the connection with redis is re-established. 247 | 248 | Try killing redis server after the application is running, and make a couple 249 | of requests. Then, start redis again and give it another try. 250 | 251 | Example: 252 | 253 | #!/usr/bin/env python 254 | # coding: utf-8 255 | 256 | import sys 257 | 258 | import cyclone.web 259 | import cyclone.redis 260 | from twisted.internet import defer 261 | from twisted.internet import reactor 262 | from twisted.python import log 263 | 264 | 265 | class Application(cyclone.web.Application): 266 | def __init__(self): 267 | handlers = [ (r"/text/(.+)", TextHandler) ] 268 | 269 | RedisMixin.setup() 270 | cyclone.web.Application.__init__(self, handlers, debug=True) 271 | 272 | 273 | class RedisMixin(object): 274 | redis_conn = None 275 | 276 | @classmethod 277 | def setup(self): 278 | RedisMixin.redis_conn = cyclone.redis.lazyConnectionPool() 279 | 280 | 281 | # Provide GET, SET and DELETE redis operations via HTTP 282 | class TextHandler(cyclone.web.RequestHandler, RedisMixin): 283 | @defer.inlineCallbacks 284 | def get(self, key): 285 | try: 286 | value = yield self.redis_conn.get(key) 287 | except Exception, e: 288 | log.msg("Redis failed to get('%s'): %s" % (key, str(e))) 289 | raise cyclone.web.HTTPError(503) 290 | 291 | self.set_header("Content-Type", "text/plain") 292 | self.write("%s=%s\r\n" % (key, value)) 293 | 294 | @defer.inlineCallbacks 295 | def post(self, key): 296 | value = self.get_argument("value") 297 | try: 298 | yield self.redis_conn.set(key, value) 299 | except Exception, e: 300 | log.msg("Redis failed to set('%s', '%s'): %s" % (key, value, str(e))) 301 | raise cyclone.web.HTTPError(503) 302 | 303 | self.set_header("Content-Type", "text/plain") 304 | self.write("%s=%s\r\n" % (key, value)) 305 | 306 | @defer.inlineCallbacks 307 | def delete(self, key): 308 | try: 309 | n = yield self.redis_conn.delete(key) 310 | except Exception, e: 311 | log.msg("Redis failed to del('%s'): %s" % (key, str(e))) 312 | raise cyclone.web.HTTPError(503) 313 | 314 | self.set_header("Content-Type", "text/plain") 315 | self.write("DEL %s=%d\r\n" % (key, n)) 316 | 317 | 318 | def main(): 319 | log.startLogging(sys.stdout) 320 | reactor.listenTCP(8888, Application(), interface="127.0.0.1") 321 | reactor.run() 322 | 323 | 324 | if __name__ == "__main__": 325 | main() 326 | 327 | 328 | This is the server running in one terminal:: 329 | 330 | $ ./helloworld.py 331 | 2012-02-17 15:40:25-0500 [-] Log opened. 332 | 2012-02-17 15:40:25-0500 [-] Starting factory 333 | 2012-02-17 15:40:25-0500 [-] __main__.Application starting on 8888 334 | 2012-02-17 15:40:25-0500 [-] Starting factory <__main__.Application instance at 0x100f42290> 335 | 2012-02-17 15:40:53-0500 [RedisProtocol,client] 200 POST /text/foo (127.0.0.1) 1.20ms 336 | 2012-02-17 15:41:01-0500 [RedisProtocol,client] 200 GET /text/foo (127.0.0.1) 0.97ms 337 | 2012-02-17 15:41:09-0500 [RedisProtocol,client] 200 DELETE /text/foo (127.0.0.1) 0.65ms 338 | (killed redis-server) 339 | 2012-02-17 15:48:48-0500 [HTTPConnection,0,127.0.0.1] Redis failed to get('foo'): Not connected 340 | 2012-02-17 15:48:48-0500 [HTTPConnection,0,127.0.0.1] 503 GET /text/foo (127.0.0.1) 2.99ms 341 | 342 | 343 | And these are the requests, from ``curl`` in another terminal. 344 | 345 | Set: 346 | 347 | $ curl -D - -d "value=bar" http://localhost:8888/text/foo 348 | HTTP/1.1 200 OK 349 | Content-Length: 9 350 | Content-Type: text/plain 351 | 352 | foo=bar 353 | 354 | Get: 355 | 356 | $ curl -D - http://localhost:8888/text/foo 357 | HTTP/1.1 200 OK 358 | Content-Length: 9 359 | Etag: "b63729aa7fa0e438eed735880951dcc21d733676" 360 | Content-Type: text/plain 361 | 362 | foo=bar 363 | 364 | Delete: 365 | 366 | $ curl -D - -X DELETE http://localhost:8888/text/foo 367 | HTTP/1.1 200 OK 368 | Content-Length: 11 369 | Content-Type: text/plain 370 | 371 | DEL foo=1 372 | 373 | When redis is not running: 374 | 375 | $ curl -D - http://localhost:8888/text/foo 376 | HTTP/1.1 503 Service Unavailable 377 | Content-Length: 89 378 | Content-Type: text/html; charset=UTF-8 379 | 380 | 503: Service Unavailable 381 | 503: Service Unavailable 382 | 383 | 384 | ### Sharded Connections ### 385 | 386 | They can be normal, or lazy connections. They can be sharded connection pools. 387 | Not all commands are supported on sharded connections. 388 | 389 | If the command you're trying to run is not supported on sharded connections, 390 | the connection handler will raise the ``NotImplementedError`` exception. 391 | 392 | Simple example with automatic sharding of keys between two redis servers: 393 | 394 | #!/usr/bin/env python 395 | # coding: utf-8 396 | 397 | import txredisapi as redis 398 | 399 | from twisted.internet import defer 400 | from twisted.internet import reactor 401 | 402 | 403 | @defer.inlineCallbacks 404 | def main(): 405 | rc = yield redis.ShardedConnection(["localhost:6379", "localhost:6380"]) 406 | print rc 407 | print "Supported methods on sharded connections:", rc.ShardedMethods 408 | 409 | keys = [] 410 | for x in xrange(100): 411 | key = "foo%02d" % x 412 | yield rc.set(key, "bar%02d" % x) 413 | keys.append(key) 414 | 415 | # yey! mget is supported! 416 | response = yield rc.mget(keys) 417 | for val in response: 418 | print val 419 | 420 | yield rc.disconnect() 421 | 422 | 423 | if __name__ == "__main__": 424 | main().addCallback(lambda ign: reactor.stop()) 425 | reactor.run() 426 | 427 | 428 | ### Transactions ### 429 | 430 | For obvious reasons, transactions are NOT supported on sharded connections. 431 | But they work pretty good on normal or lazy connections, and connection pools. 432 | 433 | NOTE: redis uses the following methods for transactions: 434 | 435 | - WATCH: synchronization 436 | - MULTI: start the transaction 437 | - EXEC: commit the transaction 438 | - DISCARD: you got it. 439 | 440 | Because ``exec`` is a reserved word in Python, the command to commit is 441 | ``commit``. 442 | 443 | Example: 444 | 445 | #!/usr/bin/env python 446 | # coding: utf-8 447 | 448 | import txredisapi as redis 449 | 450 | from twisted.internet import defer 451 | from twisted.internet import reactor 452 | 453 | 454 | @defer.inlineCallbacks 455 | def main(): 456 | rc = yield redis.ConnectionPool() 457 | 458 | # Remove the keys 459 | yield rc.delete(["a1", "a2", "a3"]) 460 | 461 | # Start transaction 462 | t = yield rc.multi() 463 | 464 | # These will return "QUEUED" - even t.get(key) 465 | yield t.set("a1", "1") 466 | yield t.set("a2", "2") 467 | yield t.set("a3", "3") 468 | yield t.get("a1") 469 | 470 | # Try to call get() while in a transaction. 471 | # It will fail if it's not a connection pool, or if all connections 472 | # in the pool are in a transaction. 473 | # Note that it's rc.get(), not the transaction object t.get(). 474 | try: 475 | v = yield rc.get("foo") 476 | print "foo=", v 477 | except Exception, e: 478 | print "can't get foo:", e 479 | 480 | # Commit, and get all responses from transaction. 481 | r = yield t.commit() 482 | print "commit=", repr(r) 483 | 484 | yield rc.disconnect() 485 | 486 | 487 | if __name__ == "__main__": 488 | main().addCallback(lambda ign: reactor.stop()) 489 | reactor.run() 490 | 491 | A "COUNTER" example, using WATCH/MULTI: 492 | 493 | #!/usr/bin/env python 494 | # coding: utf-8 495 | 496 | import txredisapi as redis 497 | 498 | from twisted.internet import defer 499 | from twisted.internet import reactor 500 | 501 | 502 | @defer.inlineCallbacks 503 | def main(): 504 | rc = yield redis.ConnectionPool() 505 | 506 | # Reset keys 507 | yield rc.set("a1", 0) 508 | 509 | # Synchronize and start transaction 510 | t = yield rc.watch("a1") 511 | 512 | # Load previous value 513 | a1 = yield t.get("a1") 514 | 515 | # start the transactional pipeline 516 | yield t.multi() 517 | 518 | # modify and retrieve the new a1 value 519 | yield t.set("a1", a1 + 1) 520 | yield t.get("a1") 521 | 522 | print "simulating concurrency, this will abort the transaction" 523 | yield rc.set("a1", 2) 524 | 525 | try: 526 | r = yield t.commit() 527 | print "commit=", repr(r) 528 | except redis.WatchError, e: 529 | a1 = yield rc.get("a1") 530 | print "transaction has failed." 531 | print "current a1 value: ", a1 532 | 533 | yield rc.disconnect() 534 | 535 | 536 | if __name__ == "__main__": 537 | main().addCallback(lambda ign: reactor.stop()) 538 | reactor.run() 539 | 540 | 541 | Calling ``commit`` will cause it to return a list with the return of all 542 | commands executed in the transaction. ``discard``, on the other hand, will 543 | normally return just an ``OK``. 544 | 545 | ### Pipelining ### 546 | 547 | txredisapi automatically [pipelines](http://redis.io/topics/pipelining) all commands 548 | by sending next commands without waiting for the previous one to receive reply from 549 | server. This works even on single connections and increases performance by reducing 550 | number of round-trip delays and. There are two exceptions, though: 551 | - no commands will be sent after blocking `blpop`, `brpop` or `brpoplpush` until 552 | response is received; 553 | - transaction by `multi`/`commit` are also blocking connection making all other 554 | commands to wait until transaction is executed. 555 | 556 | When you need to load tons of data to Redis it might be more effective to sent 557 | commands in batches grouping them together offline to save on TCP packets and network 558 | stack overhead. You can do this using `pipeline` method to explicitly accumulate 559 | commands and send them to server in a single batch. Be careful to not accumulate too 560 | many commands: unreasonable batch size may eat up unexpected amount of memory on both 561 | client and server side. Group commands in batches of, for example, 10k commands instead 562 | of sending all your data at once. The speed will be nearly the same, but the additional 563 | memory used will be at max the amount needed to queue this 10k commands 564 | 565 | To send commands in a batch: 566 | 567 | #!/usr/bin/env python 568 | # coding: utf-8 569 | 570 | import txredisapi as redis 571 | 572 | from twisted.internet import defer 573 | from twisted.internet import reactor 574 | 575 | @defer.inlineCallbacks 576 | def main(): 577 | rc = yield redis.ConnectionPool() 578 | 579 | # Start grouping commands 580 | pipeline = yield rc.pipeline() 581 | 582 | pipeline.set("foo", 123) 583 | pipeline.set("bar", 987) 584 | pipeline.get("foo") 585 | pipeline.get("bar") 586 | 587 | # Write those 2 sets and 2 gets to redis all at once, and wait 588 | # for all replies before continuing. 589 | results = yield pipeline.execute_pipeline() 590 | 591 | print "foo:", results[2] # should be 123 592 | print "bar:", results[3] # should be 987 593 | 594 | yield rc.disconnect() 595 | 596 | if __name__ == "__main__": 597 | main().addCallback(lambda ign: reactor.stop()) 598 | reactor.run() 599 | 600 | ### Authentication ### 601 | 602 | This is how to authenticate:: 603 | 604 | #!/usr/bin/env python 605 | 606 | import txredisapi 607 | from twisted.internet import defer 608 | from twisted.internet import reactor 609 | 610 | 611 | @defer.inlineCallbacks 612 | def main(): 613 | redis = yield txredisapi.Connection(password="foobared") 614 | yield redis.set("foo", "bar") 615 | print (yield redis.get("foo")) 616 | reactor.stop() 617 | 618 | 619 | if __name__ == "__main__": 620 | main() 621 | reactor.run() 622 | 623 | ### Connection using Redis Sentinel ### 624 | 625 | `txredisapi` can discover Redis master and slaves addresses using 626 | [Redis Sentinel](http://redis.io/topics/sentinel) and automatically failover 627 | in case of server failure. 628 | 629 | #!/usr/bin/env python 630 | 631 | from twisted.internet.task import react 632 | import txredisapi 633 | 634 | @defer.inlineCallbacks 635 | def main(reactor): 636 | sentinel = txredisapi.Sentinel([("sentinel-a", 26379), ("sentinel-b", 26379), ("sentinel-c", 26379)]) 637 | redis = sentinel.master_for("service_name") 638 | yield redis.set("foo", "bar") 639 | print (yield redis.get("foo")) 640 | yield redis.disconnect() 641 | yield sentinel.disconnect() 642 | 643 | react(main) 644 | 645 | Usual connection arguments like `dbid=N` or `poolsize=N` can be specified in 646 | `master_for()` call. Use `sentinel.slave_for()` to connect to one of the slaves 647 | instead of master. 648 | 649 | Add `min_other_sentinels=N` to `Sentinel` constructor call to make it obey information 650 | only from sentinels that currently connected to specified number of other sentinels 651 | to minimize a risk of split-brain in case of network partitioning. 652 | 653 | 654 | Credits 655 | ======= 656 | Thanks to (in no particular order): 657 | 658 | - Alexandre Fiori 659 | 660 | - Author of txredisapi 661 | 662 | - Gleicon Moraes 663 | 664 | - Bug fixes, testing, and [RestMQ](http://github.com/gleicon/restmq>). 665 | - For writing the Consistent Hashing algorithm used for sharding. 666 | 667 | - Dorian Raymer and Ludovico Magnocavallo 668 | 669 | - Authors of the original *redis protocol for twisted*. 670 | 671 | - Vanderson Mota 672 | 673 | - Initial pypi setup, and patches. 674 | 675 | - Jeethu Rao 676 | 677 | - Contributed with test cases, and other ideas like support for travis-ci 678 | 679 | - Jeremy Archer 680 | 681 | - Minor bugfixes. 682 | 683 | - Christoph Tavan (@ctavan) 684 | 685 | - Idea and test case for nested multi bulk replies, minor command enhancements. 686 | 687 | - dgvncsz0f 688 | 689 | - WATCH/UNWATCH commands 690 | 691 | - Ilia Glazkov 692 | 693 | - Free connection selection algorithm for pools. 694 | - Non-unicode charset fixes. 695 | - SCAN commands 696 | 697 | - Matt Pizzimenti (mjpizz) 698 | 699 | - pipelining support 700 | 701 | - Nickolai Novik (jettify) 702 | 703 | - update of SET command 704 | 705 | - Evgeny Tataurov (etataurov) 706 | 707 | - Ability to use hiredis protocol parser 708 | 709 | - Ilya Skriblovsky (IlyaSkriblovsky) 710 | 711 | - Sentinel support 712 | -------------------------------------------------------------------------------- /README.tests: -------------------------------------------------------------------------------- 1 | Docker and Fig testing for txredisapi: 2 | 3 | - Install Fig and its dependencies (http://www.fig.sh/install.html) 4 | - fig up 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/benchmark.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import print_function 3 | 4 | import time 5 | from twisted.internet import defer 6 | from twisted.internet import reactor 7 | import txredisapi as redis 8 | 9 | HOST = 'localhost' 10 | PORT = 6379 11 | 12 | N = 1000 13 | 14 | 15 | @defer.inlineCallbacks 16 | def test_setget(): 17 | key = 'test' 18 | conn = yield redis.Connection(HOST, PORT) 19 | start = time.time() 20 | for i in xrange(N): 21 | yield conn.set(key, 'test_data') 22 | yield conn.get(key) 23 | print("done set-get: %.4fs." % ((time.time() - start) / N)) 24 | 25 | 26 | @defer.inlineCallbacks 27 | def test_lrange(): 28 | key = 'test_list' 29 | list_length = 1000 30 | conn = yield redis.Connection(HOST, PORT) 31 | yield defer.DeferredList([conn.lpush(key, str(i)) for i in xrange(list_length)]) 32 | start = time.time() 33 | for i in xrange(N): 34 | yield conn.lrange(key, 0, 999) 35 | print("done lrange: %.4fs." % ((time.time() - start) / N)) 36 | 37 | 38 | @defer.inlineCallbacks 39 | def run(): 40 | yield test_setget() 41 | yield test_lrange() 42 | reactor.stop() 43 | 44 | run() 45 | reactor.run() 46 | -------------------------------------------------------------------------------- /examples/cyclone_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # 4 | # Copyright 2010 Alexandre Fiori 5 | # based on the original Tornado by Facebook 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | # not use this file except in compliance with the License. You may obtain 9 | # a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | # License for the specific language governing permissions and limitations 17 | # under the License. 18 | 19 | from __future__ import print_function 20 | 21 | import collections 22 | import functools 23 | import sys 24 | 25 | import cyclone.web 26 | import cyclone.redis 27 | 28 | from twisted.python import log 29 | from twisted.internet import defer, reactor 30 | 31 | 32 | class Application(cyclone.web.Application): 33 | def __init__(self): 34 | handlers = [ 35 | (r"/text/(.+)", TextHandler), 36 | (r"/queue/(.+)", QueueHandler), 37 | ] 38 | settings = dict( 39 | debug=True, 40 | static_path="./frontend/static", 41 | template_path="./frontend/template", 42 | ) 43 | RedisMixin.setup("127.0.0.1", 6379, 0, 10) 44 | cyclone.web.Application.__init__(self, handlers, **settings) 45 | 46 | 47 | class RedisMixin(object): 48 | dbconn = None 49 | psconn = None 50 | channels = collections.defaultdict(lambda: []) 51 | 52 | @classmethod 53 | def setup(self, host, port, dbid, poolsize): 54 | # PubSub client connection 55 | qf = cyclone.redis.SubscriberFactory() 56 | qf.maxDelay = 20 57 | qf.protocol = QueueProtocol 58 | reactor.connectTCP(host, port, qf) 59 | 60 | # Normal client connection 61 | RedisMixin.dbconn = cyclone.redis.lazyConnectionPool(host, port, 62 | dbid, poolsize) 63 | 64 | def subscribe(self, channel): 65 | if RedisMixin.psconn is None: 66 | raise cyclone.web.HTTPError(503) # Service Unavailable 67 | 68 | if channel not in RedisMixin.channels: 69 | log.msg("Subscribing entire server to %s" % channel) 70 | if "*" in channel: 71 | RedisMixin.psconn.psubscribe(channel) 72 | else: 73 | RedisMixin.psconn.subscribe(channel) 74 | 75 | RedisMixin.channels[channel].append(self) 76 | log.msg("Client %s subscribed to %s" % 77 | (self.request.remote_ip, channel)) 78 | 79 | def unsubscribe_all(self, ign): 80 | # Unsubscribe peer from all channels 81 | for channel, peers in RedisMixin.channels.iteritems(): 82 | try: 83 | peers.pop(peers.index(self)) 84 | except: 85 | continue 86 | 87 | log.msg("Client %s unsubscribed from %s" % 88 | (self.request.remote_ip, channel)) 89 | 90 | # Unsubscribe from channel if no peers are listening 91 | if not len(peers) and RedisMixin.psconn is not None: 92 | log.msg("Unsubscribing entire server from %s" % channel) 93 | if "*" in channel: 94 | RedisMixin.psconn.punsubscribe(channel) 95 | else: 96 | RedisMixin.psconn.unsubscribe(channel) 97 | 98 | def broadcast(self, pattern, channel, message): 99 | peers = self.channels.get(pattern or channel) 100 | if not peers: 101 | return 102 | 103 | # Broadcast the message to all peers in channel 104 | for peer in peers: 105 | # peer is an HTTP client, RequestHandler 106 | peer.write("%s: %s\r\n" % (channel, message)) 107 | peer.flush() 108 | 109 | 110 | # Provide GET, SET and DELETE redis operations via HTTP 111 | class TextHandler(cyclone.web.RequestHandler, RedisMixin): 112 | @defer.inlineCallbacks 113 | def get(self, key): 114 | try: 115 | value = yield self.dbconn.get(key) 116 | except Exception as e: 117 | log.err("Redis failed to get('%s'): %s" % (key, str(e))) 118 | raise cyclone.web.HTTPError(503) 119 | 120 | self.set_header("Content-Type", "text/plain") 121 | self.finish("%s=%s\r\n" % (key, value)) 122 | 123 | @defer.inlineCallbacks 124 | def post(self, key): 125 | value = self.get_argument("value") 126 | try: 127 | yield self.dbconn.set(key, value) 128 | except Exception as e: 129 | r = (key, value, str(e)) 130 | log.err("Redis failed to set('%s', '%s'): %s" % r) 131 | raise cyclone.web.HTTPError(503) 132 | 133 | self.set_header("Content-Type", "text/plain") 134 | self.finish("%s=%s\r\n" % (key, value)) 135 | 136 | @defer.inlineCallbacks 137 | def delete(self, key): 138 | try: 139 | n = yield self.dbconn.delete(key) 140 | except Exception as e: 141 | log.err("Redis failed to del('%s'): %s" % (key, str(e))) 142 | raise cyclone.web.HTTPError(503) 143 | 144 | self.set_header("Content-Type", "text/plain") 145 | self.finish("DEL %s=%d\r\n" % (key, n)) 146 | 147 | 148 | # GET will subscribe to channels or patterns 149 | # POST will (obviously) post messages to channels 150 | class QueueHandler(cyclone.web.RequestHandler, RedisMixin): 151 | @cyclone.web.asynchronous 152 | def get(self, channels): 153 | try: 154 | channels = channels.split(",") 155 | except Exception as e: 156 | log.err("Could not split channel names: %s" % str(e)) 157 | raise cyclone.web.HTTPError(400, str(e)) 158 | 159 | self.set_header("Content-Type", "text/plain") 160 | self.notifyFinish().addCallback( 161 | functools.partial(RedisMixin.unsubscribe_all, self)) 162 | 163 | for channel in channels: 164 | self.subscribe(channel) 165 | self.write("subscribed to %s\r\n" % channel) 166 | self.flush() 167 | 168 | @defer.inlineCallbacks 169 | def post(self, channel): 170 | message = self.get_argument("message") 171 | 172 | try: 173 | n = yield self.dbconn.publish(channel, message.encode("utf-8")) 174 | except Exception as e: 175 | log.msg("Redis failed to publish('%s', '%s'): %s" % 176 | (channel, repr(message), str(e))) 177 | raise cyclone.web.HTTPError(503) 178 | 179 | self.set_header("Content-Type", "text/plain") 180 | self.finish("OK %d\r\n" % n) 181 | 182 | 183 | class QueueProtocol(cyclone.redis.SubscriberProtocol, RedisMixin): 184 | def messageReceived(self, pattern, channel, message): 185 | # When new messages are published to Redis channels or patterns, 186 | # they are broadcasted to all HTTP clients subscribed to those 187 | # channels. 188 | RedisMixin.broadcast(self, pattern, channel, message) 189 | 190 | def connectionMade(self): 191 | RedisMixin.psconn = self 192 | 193 | # If we lost connection with Redis during operation, we 194 | # re-subscribe to all channels once the connection is re-established. 195 | for channel in self.channels: 196 | if "*" in channel: 197 | self.psubscribe(channel) 198 | else: 199 | self.subscribe(channel) 200 | 201 | def connectionLost(self, why): 202 | RedisMixin.psconn = None 203 | 204 | 205 | def main(): 206 | log.startLogging(sys.stdout) 207 | reactor.listenTCP(8888, Application(), interface="127.0.0.1") 208 | reactor.run() 209 | 210 | 211 | if __name__ == "__main__": 212 | main() 213 | -------------------------------------------------------------------------------- /examples/monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env twistd -ny 2 | # coding: utf-8 3 | # Copyright 2012 Gleicon Moraes/Alexandre Fiori 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # run: twistd -ny monitor.tac 18 | # it takes the full connection so no extra commands can be issued 19 | 20 | from __future__ import print_function 21 | 22 | import txredisapi 23 | 24 | from twisted.application import internet 25 | from twisted.application import service 26 | 27 | 28 | class myMonitor(txredisapi.MonitorProtocol): 29 | def connectionMade(self): 30 | print("waiting for monitor data") 31 | print("use the redis client to send commands in another terminal") 32 | self.monitor() 33 | 34 | def messageReceived(self, message): 35 | print(">> %s" % message) 36 | 37 | def connectionLost(self, reason): 38 | print("lost connection:", reason) 39 | 40 | 41 | class myFactory(txredisapi.MonitorFactory): 42 | # also a wapper for the ReconnectingClientFactory 43 | maxDelay = 120 44 | continueTrying = True 45 | protocol = myMonitor 46 | 47 | 48 | application = service.Application("monitor") 49 | srv = internet.TCPClient("127.0.0.1", 6379, myFactory()) 50 | srv.setServiceParent(application) 51 | -------------------------------------------------------------------------------- /examples/sharding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import print_function 5 | 6 | import txredisapi as redis 7 | 8 | from twisted.internet import defer 9 | from twisted.internet import reactor 10 | 11 | 12 | @defer.inlineCallbacks 13 | def main(): 14 | # run two redis servers, one at port 6379 and another in 6380 15 | conn = yield redis.ShardedConnection(["localhost:6379", "localhost:6380"]) 16 | print(repr(conn)) 17 | 18 | keys = ["test:%d" % x for x in xrange(100)] 19 | for k in keys: 20 | try: 21 | yield conn.set(k, "foobar") 22 | except: 23 | print('ops') 24 | 25 | result = yield conn.mget(keys) 26 | print(result) 27 | 28 | # testing tags 29 | keys = ["test{lero}:%d" % x for x in xrange(100)] 30 | for k in keys: 31 | yield conn.set(k, "foobar") 32 | 33 | result = yield conn.mget(keys) 34 | print(result) 35 | 36 | yield conn.disconnect() 37 | 38 | if __name__ == "__main__": 39 | main().addCallback(lambda ign: reactor.stop()) 40 | reactor.run() 41 | -------------------------------------------------------------------------------- /examples/ssl_connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import txredisapi as redis 3 | 4 | from twisted.internet import defer, ssl 5 | from twisted.internet import reactor 6 | 7 | 8 | class TestContextFactory(ssl.ClientContextFactory): 9 | def getContext(self): 10 | ctx = ssl.ClientContextFactory.getContext(self) 11 | # ctx.load_verify_locations('./test/ca.crt') 12 | # ctx.use_certificate_file('./test/redis.crt') 13 | # ctx.use_privatekey_file('./test/redis.key') 14 | return ctx 15 | 16 | @defer.inlineCallbacks 17 | def main(): 18 | rc = yield redis.Connection(ssl_context_factory=TestContextFactory()) 19 | print(rc) 20 | 21 | yield rc.set("foo", "bar") 22 | v = yield rc.get("foo") 23 | print("foo:", repr(v)) 24 | 25 | yield rc.disconnect() 26 | 27 | if __name__ == "__main__": 28 | main().addCallback(lambda ign: reactor.stop()) 29 | reactor.run() -------------------------------------------------------------------------------- /examples/subscriber.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env twistd -ny 2 | # coding: utf-8 3 | # Copyright 2009 Alexandre Fiori 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # See the PUBSUB documentation for details: 18 | # http://code.google.com/p/redis/wiki/PublishSubscribe 19 | # 20 | # run: twistd -ny subscriber.tac 21 | # You may not use regular commands (like get, set, etc...) on the 22 | # subscriber connection. 23 | 24 | from __future__ import print_function 25 | 26 | import txredisapi as redis 27 | 28 | from twisted.application import internet 29 | from twisted.application import service 30 | 31 | 32 | class myProtocol(redis.SubscriberProtocol): 33 | def connectionMade(self): 34 | print("waiting for messages...") 35 | print("use the redis client to send messages:") 36 | print("$ redis-cli publish zz test") 37 | print("$ redis-cli publish foo.bar hello world") 38 | 39 | #self.auth("foobared") 40 | 41 | self.subscribe("zz") 42 | self.psubscribe("foo.*") 43 | # reactor.callLater(10, self.unsubscribe, "zz") 44 | # reactor.callLater(15, self.punsubscribe, "foo.*") 45 | 46 | # self.continueTrying = False 47 | # self.transport.loseConnection() 48 | 49 | def messageReceived(self, pattern, channel, message): 50 | print("pattern=%s, channel=%s message=%s" % (pattern, channel, message)) 51 | 52 | def connectionLost(self, reason): 53 | print("lost connection:", reason) 54 | 55 | 56 | class myFactory(redis.SubscriberFactory): 57 | # SubscriberFactory is a wapper for the ReconnectingClientFactory 58 | maxDelay = 120 59 | continueTrying = True 60 | protocol = myProtocol 61 | 62 | 63 | application = service.Application("subscriber") 64 | srv = internet.TCPClient("127.0.0.1", 6379, myFactory()) 65 | srv.setServiceParent(application) 66 | -------------------------------------------------------------------------------- /examples/test_rest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | curl -X POST -d "key=test&value=foobar" http://localhost:8888/ 17 | curl http://localhost:8888/?key=test 18 | -------------------------------------------------------------------------------- /examples/transaction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import print_function 5 | 6 | import txredisapi as redis 7 | 8 | from twisted.internet import defer 9 | from twisted.internet import reactor 10 | 11 | 12 | @defer.inlineCallbacks 13 | def transactions(): 14 | """ 15 | The multi() method on txredisapi connections returns a transaction. 16 | All redis commands called on transactions are then queued by the server. 17 | Calling the transaction's .commit() method executes all the queued commands 18 | and returns the result for each queued command in a list. 19 | Calling the transaction's .discard() method flushes the queue and returns 20 | the connection to the default non-transactional state. 21 | 22 | multi() also takes an optional argument keys, which can be either a 23 | string or an iterable (list,tuple etc) of strings. If present, the keys 24 | are WATCHED on the server, and if any of the keys are modified by 25 | a different connection before the transaction is committed, 26 | commit() raises a WatchError exception. 27 | 28 | Transactions with WATCH make multi-command atomic all or nothing operations 29 | possible. If a transaction fails, you can be sure that none of the commands 30 | in it ran and you can retry it again. 31 | Read the redis documentation on Transactions for more. 32 | http://redis.io/topics/transactions 33 | 34 | Tip: Try to keep transactions as short as possible. 35 | Connections in a transaction cannot be reused until the transaction 36 | is either committed or discarded. For instance, if you have a 37 | ConnectionPool with 10 connections and all of them are in transactions, 38 | if you try to run a command on the connection pool, 39 | it'll raise a RedisError exception. 40 | """ 41 | conn = yield redis.Connection() 42 | # A Transaction with nothing to watch on 43 | txn = yield conn.multi() 44 | txn.incr('test:a') 45 | txn.lpush('test:l', 'test') 46 | r = yield txn.commit() # Commit txn,call txn.discard() to discard it 47 | print('Transaction1: %s' % r) 48 | 49 | # Another transaction with a few values to watch on 50 | txn1 = yield conn.multi(['test:l', 'test:h']) 51 | txn1.lpush('test:l', 'test') 52 | txn1.hset('test:h', 'test', 'true') 53 | # Transactions with watched keys will fail if any of the keys are modified 54 | # externally after calling .multi() and before calling transaction.commit() 55 | r = yield txn1.commit() # This will raise if WatchError if txn1 fails. 56 | print('Transaction2: %s' % r) 57 | yield conn.disconnect() 58 | 59 | 60 | def main(): 61 | transactions().addCallback(lambda ign: reactor.stop()) 62 | 63 | if __name__ == '__main__': 64 | reactor.callWhenRunning(main) 65 | reactor.run() 66 | -------------------------------------------------------------------------------- /examples/twistedweb_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env twistd -ny 2 | # coding: utf-8 3 | # Copyright 2009 Alexandre Fiori 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # run: 18 | # twistd -ny twistwedweb_server.py 19 | 20 | import txredisapi as redis 21 | 22 | from twisted.application import internet 23 | from twisted.application import service 24 | from twisted.internet import defer 25 | from twisted.web import server 26 | from twisted.web import xmlrpc 27 | from twisted.web.resource import Resource 28 | 29 | 30 | class Root(Resource): 31 | isLeaf = False 32 | 33 | 34 | class BaseHandler(object): 35 | isLeaf = True 36 | 37 | def __init__(self, db): 38 | self.db = db 39 | Resource.__init__(self) 40 | 41 | 42 | class IndexHandler(BaseHandler, Resource): 43 | def _success(self, value, request, message): 44 | request.write(message % repr(value)) 45 | request.finish() 46 | 47 | def _failure(self, error, request, message): 48 | request.write(message % str(error)) 49 | request.finish() 50 | 51 | def render_GET(self, request): 52 | try: 53 | key = request.args["key"][0] 54 | except: 55 | request.setResponseCode(404, "not found") 56 | return "" 57 | 58 | d = self.db.get(key) 59 | d.addCallback(self._success, request, "get: %s\n") 60 | d.addErrback(self._failure, request, "get failed: %s\n") 61 | return server.NOT_DONE_YET 62 | 63 | def render_POST(self, request): 64 | try: 65 | key = request.args["key"][0] 66 | value = request.args["value"][0] 67 | except: 68 | request.setResponseCode(404, "not found") 69 | return "" 70 | 71 | d = self.db.set(key, value) 72 | d.addCallback(self._success, request, "set: %s\n") 73 | d.addErrback(self._failure, request, "set failed: %s\n") 74 | return server.NOT_DONE_YET 75 | 76 | 77 | class InfoHandler(BaseHandler, Resource): 78 | def render_GET(self, request): 79 | return "redis: %s\n" % repr(self.db) 80 | 81 | 82 | class XmlrpcHandler(BaseHandler, xmlrpc.XMLRPC): 83 | allowNone = True 84 | 85 | @defer.inlineCallbacks 86 | def xmlrpc_get(self, key): 87 | value = yield self.db.get(key) 88 | return value 89 | 90 | @defer.inlineCallbacks 91 | def xmlrpc_set(self, key, value): 92 | result = yield self.db.set(key, value) 93 | return result 94 | 95 | 96 | # redis connection 97 | _db = redis.lazyConnectionPool() 98 | 99 | # http resources 100 | root = Root() 101 | root.putChild("", IndexHandler(_db)) 102 | root.putChild("info", InfoHandler(_db)) 103 | root.putChild("xmlrpc", XmlrpcHandler(_db)) 104 | 105 | application = service.Application("webredis") 106 | srv = internet.TCPServer(8888, server.Site(root), interface="127.0.0.1") 107 | srv.setServiceParent(application) 108 | -------------------------------------------------------------------------------- /examples/wordfreq.README: -------------------------------------------------------------------------------- 1 | wordfreq is a test to check consistent hashing distribution between the servers. 2 | Use it with some big text file as python wordfreq.py 3 | -------------------------------------------------------------------------------- /examples/wordfreq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | import txredisapi as redis 8 | 9 | from twisted.internet import defer 10 | from twisted.internet import reactor 11 | 12 | 13 | def wordfreq(file): 14 | try: 15 | f = open(file, 'r') 16 | words = f.read() 17 | f.close() 18 | except Exception as e: 19 | print("Exception: %s" % e) 20 | return None 21 | 22 | wf = {} 23 | wlist = words.split() 24 | for b in wlist: 25 | a = b.lower() 26 | if a in wf: 27 | wf[a] = wf[a] + 1 28 | else: 29 | wf[a] = 1 30 | return len(wf), wf 31 | 32 | 33 | @defer.inlineCallbacks 34 | def main(wordlist): 35 | db = yield redis.ShardedConnection(("localhost:6379", "localhost:6380")) 36 | for k in wordlist: 37 | yield db.set(k, 1) 38 | 39 | reactor.stop() 40 | 41 | 42 | if __name__ == '__main__': 43 | if len(sys.argv) < 2: 44 | print("Usage: wordfreq.py ") 45 | sys.exit(-1) 46 | l, wfl = wordfreq(sys.argv[1]) 47 | print("count: %d" % l) 48 | main(wfl.keys()) 49 | reactor.run() 50 | -------------------------------------------------------------------------------- /fig.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: . 3 | working_dir: /txredisapi 4 | command: trial tests 5 | ports: 6 | - "8000:8000" 7 | links: 8 | - dbredis 9 | volumes: 10 | - .:/txredisapi 11 | dbredis: 12 | image: redis 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # Copyright 2009 Alexandre Fiori 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import setuptools 18 | from pathlib import Path 19 | 20 | this_directory = Path(__file__).parent 21 | long_description = (this_directory / "README.md").read_text() 22 | 23 | 24 | setuptools.setup( 25 | name="txredisapi", 26 | version="1.4.11", 27 | py_modules=["txredisapi"], 28 | install_requires=["twisted", "six"], 29 | author="Alexandre Fiori", 30 | author_email="fiorix@gmail.com", 31 | url="http://github.com/IlyaSkriblovsky/txredisapi", 32 | license="http://www.apache.org/licenses/LICENSE-2.0", 33 | description="non-blocking redis client for python", 34 | long_description=long_description, 35 | long_description_content_type="text/markdown", 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'Intended Audience :: Developers', 39 | 'Operating System :: OS Independent', 40 | 'License :: OSI Approved :: Apache Software License', 41 | 'Programming Language :: Python', 42 | 'Programming Language :: Python :: 2.7', 43 | 'Programming Language :: Python :: 3.5', 44 | 'Programming Language :: Python :: 3.6', 45 | 'Programming Language :: Python :: 3.7', 46 | 'Programming Language :: Python :: 3.8', 47 | 'Programming Language :: Python :: 3.9', 48 | 'Programming Language :: Python :: 3.10', 49 | 'Programming Language :: Python :: 3.11', 50 | 'Programming Language :: Python :: Implementation :: PyPy', 51 | ], 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Ensure that tests are importing the local copy of txredisapi rather than 2 | # any system-installed copy of txredisapi that might exist in the path. 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) 7 | 8 | import txredisapi 9 | 10 | sys.path.pop(0) 11 | -------------------------------------------------------------------------------- /tests/chash_distribution.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import print_function 5 | 6 | from txredisapi import HashRing 7 | from collections import defaultdict 8 | 9 | if __name__ == "__main__": 10 | ch = HashRing(["server1", "server2", "server3"]) 11 | key_history = {} 12 | node_histogram = defaultdict(lambda: 0) 13 | for x in xrange(1000): 14 | key_history[x] = ch.get_node("k:%d" % x) 15 | s = key_history[x] 16 | node_histogram[s] = node_histogram[s] + 1 17 | print("server\t\tkeys:") 18 | for a in node_histogram.keys(): 19 | print("%s:\t%d" % (a, node_histogram[a])) 20 | -------------------------------------------------------------------------------- /tests/mixins.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.trial import unittest 17 | from twisted.internet import defer 18 | import os 19 | 20 | s = os.getenv("DBREDIS_1_PORT_6379_TCP_ADDR") 21 | 22 | if s is not None: 23 | REDIS_HOST = "dbredis_1" 24 | else: 25 | REDIS_HOST = "localhost" 26 | 27 | REDIS_PORT = 6379 28 | 29 | 30 | class RedisVersionCheckMixin(object): 31 | @defer.inlineCallbacks 32 | def checkVersion(self, major, minor, patch=0): 33 | d = yield self.db.info("server") 34 | if u'redis_version' not in d: 35 | return False 36 | ver = d[u'redis_version'] 37 | self.redis_version = ver 38 | ver_list = [int(x) for x in ver.split(u'.')] 39 | if len(ver_list) < 2: 40 | return False 41 | if len(ver_list) == 2: 42 | ver_list.append(0) 43 | if ver_list[0] > major: 44 | return True 45 | elif ver_list[0] == major: 46 | if ver_list[1] > minor: 47 | return True 48 | elif ver_list[1] == minor: 49 | if ver_list[2] >= patch: 50 | return True 51 | return False 52 | 53 | 54 | class Redis26CheckMixin(RedisVersionCheckMixin): 55 | def is_redis_2_6(self): 56 | """ 57 | Returns true if the Redis version >= 2.6 58 | """ 59 | return self.checkVersion(2, 6) 60 | 61 | def _skipCheck(self): 62 | if not self.redis_2_6: 63 | skipMsg = "Redis version < 2.6 (found version: %s)" 64 | raise unittest.SkipTest(skipMsg % self.redis_version) 65 | -------------------------------------------------------------------------------- /tests/test_basics.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2015 Jeethu Rao 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import txredisapi as redis 17 | from twisted.internet import defer 18 | from twisted.trial import unittest 19 | 20 | from .mixins import REDIS_HOST, REDIS_PORT 21 | 22 | 23 | class TestBasics(unittest.TestCase): 24 | _KEYS = ['_test_key1', '_test_key2', '_test_key3'] 25 | skipTeardown = False 26 | 27 | @defer.inlineCallbacks 28 | def setUp(self): 29 | self.skipTeardown = False 30 | self.db = yield redis.Connection(REDIS_HOST, REDIS_PORT, 31 | reconnect=False) 32 | 33 | @defer.inlineCallbacks 34 | def tearDown(self): 35 | if not self.skipTeardown: 36 | yield self.db.delete(*self._KEYS) 37 | yield self.db.disconnect() 38 | 39 | def test_quit(self): 40 | self.skipTeardown = True 41 | return self.db.quit() 42 | 43 | def test_auth(self): 44 | self.assertFailure(self.db.auth('test'), 45 | redis.ResponseError) 46 | 47 | @defer.inlineCallbacks 48 | def test_ping(self): 49 | r = yield self.db.ping() 50 | self.assertEqual(r, u'PONG') 51 | 52 | @defer.inlineCallbacks 53 | def test_exists(self): 54 | yield self.db.delete(self._KEYS[0]) 55 | r = yield self.db.exists(self._KEYS[0]) 56 | self.assertFalse(r) 57 | yield self.db.set(self._KEYS[0], 1) 58 | r = yield self.db.exists(self._KEYS[0]) 59 | self.assertTrue(r) 60 | 61 | @defer.inlineCallbacks 62 | def test_type(self): 63 | yield self.db.set(self._KEYS[0], "value") 64 | yield self.db.lpush(self._KEYS[1], 1) 65 | yield self.db.sadd(self._KEYS[2], 1) 66 | r = yield self.db.type(self._KEYS[0]) 67 | self.assertEqual(r, "string") 68 | r = yield self.db.type(self._KEYS[1]) 69 | self.assertEqual(r, "list") 70 | r = yield self.db.type(self._KEYS[2]) 71 | self.assertEqual(r, "set") 72 | 73 | @defer.inlineCallbacks 74 | def test_keys(self): 75 | yield self.db.set(self._KEYS[0], "value") 76 | yield self.db.set(self._KEYS[1], "value") 77 | r = yield self.db.keys('_test_key*') 78 | self.assertIn(self._KEYS[0], r) 79 | self.assertIn(self._KEYS[1], r) 80 | 81 | @defer.inlineCallbacks 82 | def test_randomkey(self): 83 | yield self.db.set(self._KEYS[0], "value") 84 | r = yield self.db.randomkey() 85 | assert r is not None 86 | 87 | @defer.inlineCallbacks 88 | def test_rename(self): 89 | yield self.db.set(self._KEYS[0], "value") 90 | yield self.db.delete(self._KEYS[1]) 91 | r = yield self.db.rename(self._KEYS[0], self._KEYS[1]) 92 | self.assertTrue(r) 93 | r = yield self.db.get(self._KEYS[1]) 94 | self.assertEqual(r, "value") 95 | r = yield self.db.exists(self._KEYS[0]) 96 | self.assertFalse(r) 97 | 98 | @defer.inlineCallbacks 99 | def test_renamenx(self): 100 | yield self.db.set(self._KEYS[0], "value") 101 | yield self.db.set(self._KEYS[1], "value1") 102 | r = yield self.db.renamenx(self._KEYS[0], self._KEYS[1]) 103 | self.assertFalse(r) 104 | r = yield self.db.renamenx(self._KEYS[1], self._KEYS[2]) 105 | self.assertTrue(r) 106 | r = yield self.db.get(self._KEYS[2]) 107 | self.assertEqual(r, "value1") 108 | 109 | @defer.inlineCallbacks 110 | def test_dbsize(self): 111 | yield self.db.set(self._KEYS[0], "value") 112 | yield self.db.set(self._KEYS[1], "value1") 113 | r = yield self.db.dbsize() 114 | assert r >= 2 115 | -------------------------------------------------------------------------------- /tests/test_bitops.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import six 17 | 18 | import sys 19 | import operator 20 | 21 | import txredisapi as redis 22 | from twisted.internet import defer 23 | from twisted.trial import unittest 24 | from twisted.python import failure 25 | 26 | from .mixins import Redis26CheckMixin, REDIS_HOST, REDIS_PORT 27 | 28 | 29 | class TestBitOps(unittest.TestCase, Redis26CheckMixin): 30 | _KEYS = ['_bitops_test_key1', '_bitops_test_key2', 31 | '_bitops_test_key3'] 32 | 33 | @defer.inlineCallbacks 34 | def setUp(self): 35 | self.db = yield redis.Connection(REDIS_HOST, REDIS_PORT, 36 | reconnect=False, 37 | charset=None) 38 | self.db1 = None 39 | self.redis_2_6 = yield self.is_redis_2_6() 40 | yield self.db.delete(*self._KEYS) 41 | yield self.db.script_flush() 42 | 43 | @defer.inlineCallbacks 44 | def tearDown(self): 45 | yield self.db.delete(*self._KEYS) 46 | yield self.db.disconnect() 47 | 48 | @defer.inlineCallbacks 49 | def test_getbit(self): 50 | key = self._KEYS[0] 51 | yield self.db.set(key, six.b('\xaa')) 52 | l = [1, 0, 1, 0, 1, 0, 1, 0] 53 | for x in range(8): 54 | r = yield self.db.getbit(key, x) 55 | self.assertEqual(r, l[x]) 56 | 57 | @defer.inlineCallbacks 58 | def test_setbit(self): 59 | key = self._KEYS[0] 60 | r = yield self.db.setbit(key, 7, 1) 61 | self.assertEqual(r, 0) 62 | r = yield self.db.setbit(key, 7, 0) 63 | self.assertEqual(r, 1) 64 | r = yield self.db.setbit(key, 7, True) 65 | self.assertEqual(r, 0) 66 | r = yield self.db.setbit(key, 7, False) 67 | self.assertEqual(r, 1) 68 | 69 | @defer.inlineCallbacks 70 | def test_bitcount(self): 71 | self._skipCheck() 72 | key = self._KEYS[0] 73 | yield self.db.set(key, "foobar") 74 | r = yield self.db.bitcount(key) 75 | self.assertEqual(r, 26) 76 | r = yield self.db.bitcount(key, 0, 0) 77 | self.assertEqual(r, 4) 78 | r = yield self.db.bitcount(key, 1, 1) 79 | self.assertEqual(r, 6) 80 | # Ensure that the error is raised 81 | d = defer.maybeDeferred(self.db.bitcount, key, start=1) 82 | self.assertFailure(d, redis.RedisError) 83 | d1 = defer.maybeDeferred(self.db.bitcount, key, end=1) 84 | self.assertFailure(d1, redis.RedisError) 85 | 86 | def test_bitop_not(self): 87 | return self._test_bitop([operator.__not__, operator.not_, 88 | 'not', 'NOT', 'NoT'], 89 | six.b('\x0f\x0f\x0f\x0f'), 90 | None, 91 | six.b('\xf0\xf0\xf0\xf0')) 92 | 93 | def test_bitop_or(self): 94 | return self._test_bitop([operator.__or__, operator.or_, 95 | 'or', 'OR', 'oR'], 96 | six.b('\x0f\x0f\x0f\x0f'), 97 | six.b('\xf0\xf0\xf0\xf0'), 98 | six.b('\xff\xff\xff\xff')) 99 | 100 | def test_bitop_and(self): 101 | return self._test_bitop([operator.__and__, operator.and_, 102 | 'and', 'AND', 'AnD'], 103 | six.b('\x0f\x0f\x0f\x0f'), 104 | six.b('\xf0\xf0\xf0\xf0'), 105 | six.b('\x00\x00\x00\x00')) 106 | 107 | def test_bitop_xor(self): 108 | return self._test_bitop([operator.__xor__, operator.xor, 109 | 'xor', 'XOR', 'XoR'], 110 | six.b('\x9c\x9c\x9c\x9c'), 111 | six.b('\x6c\x6c\x6c\x6c'), 112 | six.b('\xf0\xf0\xf0\xf0')) 113 | 114 | def test_bitop_invalid(self): 115 | self.assertFailure(self.db.bitop('test', 'test', 'test'), 116 | redis.InvalidData) 117 | 118 | @defer.inlineCallbacks 119 | def _test_bitop(self, op_list, value1, value2, expected): 120 | self._skipCheck() 121 | src_key = self._KEYS[0] 122 | src_key1 = self._KEYS[1] 123 | dest_key = self._KEYS[2] 124 | is_unary = value2 is None 125 | yield self.db.set(src_key, value1) 126 | if not is_unary: 127 | yield self.db.set(src_key1, value2) 128 | t = (src_key, src_key1) 129 | else: 130 | t = (src_key, ) 131 | for op in op_list: 132 | yield self.db.bitop(op, dest_key, *t) 133 | r = yield self.db.get(dest_key) 134 | self.assertEqual(r, expected) 135 | # Test out failure cases 136 | # Specify only dest and no src key(s) 137 | cases = [self.db.bitop(op, dest_key)] 138 | if is_unary: 139 | # Try calling unary operator with > 1 operands 140 | cases.append(self.db.bitop(op, dest_key, src_key, src_key1)) 141 | for case in cases: 142 | try: 143 | r = yield case 144 | except redis.RedisError: 145 | pass 146 | except: 147 | tb = failure.Failure().getTraceback() 148 | raise self.failureException('%s raised instead of %s:\n %s' 149 | % (sys.exc_info()[0], 150 | 'txredisapi.RedisError', 151 | tb)) 152 | else: 153 | raise self.failureException('%s not raised (%r returned)' 154 | % ('txredisapi.RedisError', 155 | r)) 156 | -------------------------------------------------------------------------------- /tests/test_blocking.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.internet import defer 17 | from twisted.trial import unittest 18 | 19 | import txredisapi as redis 20 | 21 | from tests.mixins import REDIS_HOST, REDIS_PORT 22 | 23 | 24 | class TestBlockingCommands(unittest.TestCase): 25 | QUEUE_KEY = 'txredisapi:test_queue' 26 | TEST_KEY = 'txredisapi:test_key' 27 | QUEUE_VALUE = 'queue_value' 28 | 29 | @defer.inlineCallbacks 30 | def testBlocking(self): 31 | db = yield redis.ConnectionPool(REDIS_HOST, REDIS_PORT, poolsize=2, 32 | reconnect=False) 33 | yield db.delete(self.QUEUE_KEY, self.TEST_KEY) 34 | 35 | # Block first connection. 36 | d = db.brpop(self.QUEUE_KEY, timeout=3) 37 | # Use second connection. 38 | yield db.set(self.TEST_KEY, 'somevalue') 39 | # Should use second connection again. Will block till end of 40 | # brpop otherwise. 41 | yield db.lpush('txredisapi:test_queue', self.QUEUE_VALUE) 42 | 43 | brpop_result = yield d 44 | self.assertNotEqual(brpop_result, None) 45 | 46 | yield db.delete(self.QUEUE_KEY, self.TEST_KEY) 47 | yield db.disconnect() 48 | -------------------------------------------------------------------------------- /tests/test_cancel.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.internet import defer, reactor 17 | from twisted.trial import unittest 18 | 19 | import txredisapi as redis 20 | 21 | from tests.mixins import REDIS_HOST, REDIS_PORT 22 | 23 | 24 | class TestRedisCancels(unittest.TestCase): 25 | @defer.inlineCallbacks 26 | def test_cancel(self): 27 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT) 28 | 29 | prefix = 'txredisapi:cancel' 30 | 31 | # Set + Get 32 | key = prefix + '1' 33 | value = 'first' 34 | res = yield db.set(key, value) 35 | self.assertEquals('OK', res) 36 | val = yield db.get(key) 37 | self.assertEquals(val, value) 38 | 39 | # Cancel a method 40 | d = db.time() 41 | d.addErrback(lambda _: True) 42 | d.cancel() 43 | 44 | # And Set + Get 45 | key = prefix + '2' 46 | value = 'second' 47 | res = yield db.set(key, value) 48 | #self.assertEquals('OK', res) 49 | val = yield db.get(key) 50 | self.assertEquals(val, value) 51 | 52 | yield db.disconnect() 53 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | from twisted.internet import base 16 | from twisted.internet import defer 17 | from twisted.internet.task import Clock 18 | from twisted.test.iosim import connectedServerAndClient 19 | from twisted.trial import unittest 20 | 21 | import txredisapi as redis 22 | 23 | from tests.mixins import REDIS_HOST, REDIS_PORT 24 | from tests.test_sentinel import FakeRedisProtocol, FakeRedisFactory 25 | 26 | base.DelayedCall.debug = False 27 | 28 | 29 | class TestConnectionMethods(unittest.TestCase): 30 | @defer.inlineCallbacks 31 | def test_Connection(self): 32 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 33 | self.assertEqual(isinstance(db, redis.ConnectionHandler), True) 34 | yield db.disconnect() 35 | 36 | @defer.inlineCallbacks 37 | def test_ConnectionDB1(self): 38 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, dbid=1, 39 | reconnect=False) 40 | self.assertEqual(isinstance(db, redis.ConnectionHandler), True) 41 | yield db.disconnect() 42 | 43 | @defer.inlineCallbacks 44 | def test_ConnectionPool(self): 45 | db = yield redis.ConnectionPool(REDIS_HOST, REDIS_PORT, poolsize=2, 46 | reconnect=False) 47 | self.assertEqual(isinstance(db, redis.ConnectionHandler), True) 48 | yield db.disconnect() 49 | 50 | @defer.inlineCallbacks 51 | def test_lazyConnection(self): 52 | db = redis.lazyConnection(REDIS_HOST, REDIS_PORT, reconnect=False) 53 | self.assertEqual(isinstance(db._connected, defer.Deferred), True) 54 | db = yield db._connected 55 | self.assertEqual(isinstance(db, redis.ConnectionHandler), True) 56 | yield db.disconnect() 57 | 58 | @defer.inlineCallbacks 59 | def test_lazyConnectionPool(self): 60 | db = redis.lazyConnectionPool(REDIS_HOST, REDIS_PORT, reconnect=False) 61 | self.assertEqual(isinstance(db._connected, defer.Deferred), True) 62 | db = yield db._connected 63 | self.assertEqual(isinstance(db, redis.ConnectionHandler), True) 64 | yield db.disconnect() 65 | 66 | @defer.inlineCallbacks 67 | def test_ShardedConnection(self): 68 | hosts = ["%s:%s" % (REDIS_HOST, REDIS_PORT)] 69 | db = yield redis.ShardedConnection(hosts, reconnect=False) 70 | self.assertEqual(isinstance(db, redis.ShardedConnectionHandler), True) 71 | yield db.disconnect() 72 | 73 | @defer.inlineCallbacks 74 | def test_ShardedConnectionPool(self): 75 | hosts = ["%s:%s" % (REDIS_HOST, REDIS_PORT)] 76 | db = yield redis.ShardedConnectionPool(hosts, reconnect=False) 77 | self.assertEqual(isinstance(db, redis.ShardedConnectionHandler), True) 78 | yield db.disconnect() 79 | 80 | @defer.inlineCallbacks 81 | def test_dbid(self): 82 | db1 = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False, dbid=1) 83 | db2 = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False, dbid=5) 84 | self.addCleanup(db1.disconnect) 85 | self.addCleanup(db2.disconnect) 86 | 87 | yield db1.set('x', 42) 88 | try: 89 | value = yield db2.get('x') 90 | self.assertIs(value, None) 91 | finally: 92 | yield db1.delete('x') 93 | 94 | 95 | class SlowServerProtocol(FakeRedisProtocol): 96 | def __init__(self, reactor, delay, storage): 97 | FakeRedisProtocol.__init__(self) 98 | self.callLater = reactor.callLater 99 | self.delay = delay 100 | self.storage = storage 101 | self._delayed_calls = [] 102 | 103 | def replyReceived(self, request): 104 | if isinstance(request, list): 105 | command = request[0] 106 | if command == "GET": 107 | name = request[1] 108 | if name in self.storage: 109 | self.send_reply(self.storage[name]) 110 | elif command == "BLPOP": 111 | name = request[1] 112 | timeout = request[2] 113 | 114 | def do_reply(): 115 | try: 116 | value = self.storage[name].pop(0) 117 | except (AttributeError, IndexError): 118 | value = None 119 | self.send_reply(value) 120 | self.callLater(timeout, do_reply) 121 | else: 122 | self.send_error("Command not supported") 123 | else: 124 | FakeRedisProtocol.replyReceived(self, request) 125 | 126 | def send_reply(self, reply): 127 | self._delayed_calls.append( 128 | self.callLater(self.delay, FakeRedisProtocol.send_reply, self, reply) 129 | ) 130 | 131 | def send_error(self, msg): 132 | self._delayed_calls.append( 133 | self.callLater(self.delay, FakeRedisProtocol.send_error, self, msg) 134 | ) 135 | 136 | def connectionLost(self, why): 137 | for call in self._delayed_calls: 138 | if call.active(): 139 | call.cancel() 140 | 141 | 142 | class TestTimeouts(unittest.TestCase): 143 | def setUp(self): 144 | self.clock = Clock() 145 | 146 | old = redis.LineReceiver.callLater 147 | redis.LineReceiver.callLater = self.clock.callLater 148 | 149 | def cleanup(): 150 | redis.LineReceiver.callLater = old 151 | self.addCleanup(cleanup) 152 | 153 | 154 | def _clientAndServer(self, clientTimeout, serverTimeout, initialStorage=None): 155 | storage = initialStorage or {} 156 | return connectedServerAndClient( 157 | lambda: SlowServerProtocol(self.clock, serverTimeout, storage), 158 | lambda: redis.RedisProtocol(replyTimeout=clientTimeout) 159 | ) 160 | 161 | def test_noTimeout(self): 162 | client, server, pump = self._clientAndServer(2, 1, {'x': 42}) 163 | 164 | result = client.get('x') 165 | pump.flush() 166 | self.assertFalse(result.called) 167 | self.clock.pump([0, 1.1]) 168 | pump.flush() 169 | self.assertEqual(self.successResultOf(result), 42) 170 | 171 | # ensure that idle connection is not closed by timeout 172 | self.clock.pump([0, 5]) 173 | result2 = client.get('x') 174 | pump.flush() 175 | self.clock.pump([0, 1.1]) 176 | pump.flush() 177 | self.assertEqual(self.successResultOf(result2), 42) 178 | 179 | 180 | def test_timedOut(self): 181 | client, server, pump = self._clientAndServer(0.5, 1, {'x': 42}) 182 | 183 | result = client.get('x') 184 | pump.flush() 185 | self.assertFalse(result.called) 186 | self.clock.pump([0, 0.6]) 187 | pump.flush() 188 | self.failureResultOf(result, redis.TimeoutError) 189 | 190 | 191 | def test_timeoutBreaksConnection(self): 192 | """ 193 | When the call is timed out connection to Redis is closed and all 194 | pending queries are interrupted with ConnectionError (because they 195 | are not timed out yet themselves) 196 | """ 197 | client, server, pump = self._clientAndServer(0.5, 1, {'x': 42}) 198 | 199 | result1 = client.get('x') 200 | pump.flush() 201 | self.clock.pump([0, 0.3]) 202 | result2 = client.get('x') 203 | pump.flush() 204 | self.clock.pump([0, 0.3]) 205 | pump.flush() 206 | self.failureResultOf(result1, redis.TimeoutError) 207 | self.failureResultOf(result2, redis.ConnectionError) 208 | 209 | 210 | def test_blockingOps(self): 211 | """ 212 | replyTimeout should not be applied to blocking commands: 213 | blpop, brpop, brpoplpush 214 | """ 215 | client, server, pump = self._clientAndServer(1, 0, {'x': 42}) 216 | 217 | result1 = client.blpop('x', timeout=5) 218 | pump.flush() 219 | self.clock.pump([0, 4]) 220 | pump.flush() 221 | self.assertNoResult(result1) 222 | self.clock.pump([0, 1.1]) 223 | pump.flush() 224 | self.assertEqual(self.successResultOf(result1), None) 225 | 226 | 227 | 228 | class TestTimeoutsOnRealServer(unittest.TestCase): 229 | @defer.inlineCallbacks 230 | def test_blockingOps(self): 231 | """ 232 | replyTimeout should not be applied to blocking commands: 233 | blpop, brpop, brpoplpush 234 | """ 235 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, dbid=1, 236 | reconnect=False, replyTimeout=1) 237 | self.addCleanup(db.disconnect) 238 | 239 | result1 = yield db.brpop('x', timeout=2) 240 | self.assertIs(result1, None) 241 | -------------------------------------------------------------------------------- /tests/test_connection_charset.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2013 Ilia Glazkov 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import six 17 | 18 | from twisted.internet import defer 19 | from twisted.trial import unittest 20 | 21 | import txredisapi as redis 22 | 23 | from tests.mixins import REDIS_HOST, REDIS_PORT 24 | 25 | 26 | class TestConnectionCharset(unittest.TestCase): 27 | TEST_KEY = 'txredisapi:test_key' 28 | TEST_VALUE_UNICODE = six.text_type('\u262d' * 3) 29 | TEST_VALUE_BINARY = b'\x00\x01' * 3 30 | 31 | @defer.inlineCallbacks 32 | def test_charset_None(self): 33 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, charset=None) 34 | 35 | yield db.set(self.TEST_KEY, self.TEST_VALUE_BINARY) 36 | result = yield db.get(self.TEST_KEY) 37 | self.assertTrue(type(result) == six.binary_type) 38 | self.assertEqual(result, self.TEST_VALUE_BINARY) 39 | 40 | yield db.delete(self.TEST_KEY) 41 | yield db.disconnect() 42 | 43 | @defer.inlineCallbacks 44 | def test_charset_default(self): 45 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT) 46 | 47 | yield db.set(self.TEST_KEY, self.TEST_VALUE_UNICODE) 48 | result = yield db.get(self.TEST_KEY) 49 | self.assertEqual(result, self.TEST_VALUE_UNICODE) 50 | self.assertTrue(type(result) == six.text_type) 51 | 52 | yield db.delete(self.TEST_KEY) 53 | yield db.disconnect() 54 | -------------------------------------------------------------------------------- /tests/test_hash_ops.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import six 17 | 18 | from twisted.internet import defer 19 | from twisted.trial import unittest 20 | 21 | import txredisapi as redis 22 | 23 | from tests.mixins import REDIS_HOST, REDIS_PORT 24 | 25 | 26 | class TestRedisHashOperations(unittest.TestCase): 27 | @defer.inlineCallbacks 28 | def testRedisHSetHGet(self): 29 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 30 | for hk in ("foo", "bar"): 31 | yield db.hset("txredisapi:HSetHGet", hk, 1) 32 | result = yield db.hget("txredisapi:HSetHGet", hk) 33 | self.assertEqual(result, 1) 34 | 35 | yield db.disconnect() 36 | 37 | @defer.inlineCallbacks 38 | def testRedisHMSetHMGet(self): 39 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 40 | t_dict = {} 41 | t_dict['key1'] = 'uno' 42 | t_dict['key2'] = 'dos' 43 | yield db.hmset("txredisapi:HMSetHMGet", t_dict) 44 | ks = list(t_dict.keys()) 45 | ks.reverse() 46 | vs = list(t_dict.values()) 47 | vs.reverse() 48 | res = yield db.hmget("txredisapi:HMSetHMGet", ks) 49 | self.assertEqual(vs, res) 50 | 51 | yield db.disconnect() 52 | 53 | @defer.inlineCallbacks 54 | def testRedisHKeysHVals(self): 55 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 56 | t_dict = {} 57 | t_dict['key1'] = 'uno' 58 | t_dict['key2'] = 'dos' 59 | yield db.hmset("txredisapi:HKeysHVals", t_dict) 60 | 61 | vs_u = [six.text_type(v) for v in t_dict.values()] 62 | ks_u = [six.text_type(k) for k in t_dict.keys()] 63 | k_res = yield db.hkeys("txredisapi:HKeysHVals") 64 | v_res = yield db.hvals("txredisapi:HKeysHVals") 65 | self.assertEqual(sorted(ks_u), sorted(k_res)) 66 | self.assertEqual(sorted(vs_u), sorted(v_res)) 67 | 68 | yield db.disconnect() 69 | 70 | @defer.inlineCallbacks 71 | def testRedisHIncrBy(self): 72 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 73 | yield db.hset("txredisapi:HIncrBy", "value", 1) 74 | yield db.hincr("txredisapi:HIncrBy", "value") 75 | yield db.hincrby("txredisapi:HIncrBy", "value", 2) 76 | result = yield db.hget("txredisapi:HIncrBy", "value") 77 | self.assertEqual(result, 4) 78 | 79 | yield db.hincrby("txredisapi:HIncrBy", "value", 10) 80 | yield db.hdecr("txredisapi:HIncrBy", "value") 81 | result = yield db.hget("txredisapi:HIncrBy", "value") 82 | self.assertEqual(result, 13) 83 | 84 | yield db.disconnect() 85 | 86 | @defer.inlineCallbacks 87 | def testRedisHLenHDelHExists(self): 88 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 89 | t_dict = {} 90 | t_dict['key1'] = 'uno' 91 | t_dict['key2'] = 'dos' 92 | 93 | s = yield db.hmset("txredisapi:HDelHExists", t_dict) 94 | r_len = yield db.hlen("txredisapi:HDelHExists") 95 | self.assertEqual(r_len, 2) 96 | 97 | s = yield db.hdel("txredisapi:HDelHExists", "key2") 98 | r_len = yield db.hlen("txredisapi:HDelHExists") 99 | self.assertEqual(r_len, 1) 100 | 101 | s = yield db.hexists("txredisapi:HDelHExists", "key2") 102 | self.assertEqual(s, 0) 103 | 104 | yield db.disconnect() 105 | 106 | @defer.inlineCallbacks 107 | def testRedisHLenHDelMulti(self): 108 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 109 | t_dict = {} 110 | t_dict['key1'] = 'uno' 111 | t_dict['key2'] = 'dos' 112 | 113 | s = yield db.hmset("txredisapi:HDelHExists", t_dict) 114 | r_len = yield db.hlen("txredisapi:HDelHExists") 115 | self.assertEqual(r_len, 2) 116 | 117 | s = yield db.hdel("txredisapi:HDelHExists", ["key1", "key2"]) 118 | r_len = yield db.hlen("txredisapi:HDelHExists") 119 | self.assertEqual(r_len, 0) 120 | 121 | s = yield db.hexists("txredisapi:HDelHExists", ["key1", "key2"]) 122 | self.assertEqual(s, 0) 123 | 124 | yield db.disconnect() 125 | 126 | @defer.inlineCallbacks 127 | def testRedisHGetAll(self): 128 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 129 | 130 | d = {u"key1": u"uno", u"key2": u"dos"} 131 | yield db.hmset("txredisapi:HGetAll", d) 132 | s = yield db.hgetall("txredisapi:HGetAll") 133 | 134 | self.assertEqual(d, s) 135 | yield db.disconnect() 136 | -------------------------------------------------------------------------------- /tests/test_hyperloglog.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import txredisapi as redis 17 | 18 | from twisted.internet import defer 19 | from twisted.trial import unittest 20 | 21 | from .mixins import RedisVersionCheckMixin, REDIS_HOST, REDIS_PORT 22 | 23 | 24 | class TestHyperLogLog(unittest.TestCase, RedisVersionCheckMixin): 25 | _KEYS = ['_hll_test_key1', '_hll_test_key2', 26 | '_hll_test_key3'] 27 | 28 | @defer.inlineCallbacks 29 | def setUp(self): 30 | self.db = yield redis.Connection(REDIS_HOST, REDIS_PORT, 31 | reconnect=False) 32 | self.redis_2_8_9 = yield self.checkVersion(2, 8, 9) 33 | yield self.db.delete(*self._KEYS) 34 | 35 | @defer.inlineCallbacks 36 | def tearDown(self): 37 | yield self.db.delete(*self._KEYS) 38 | yield self.db.disconnect() 39 | 40 | @defer.inlineCallbacks 41 | def test_pfadd(self): 42 | self._skipCheck() 43 | key = self._KEYS[0] 44 | r = yield self.db.pfadd(key, 'a', 'b', 'c', 'd', 'e', 'f', 'g') 45 | self.assertEqual(r, 1) 46 | cnt = yield self.db.pfcount(key) 47 | self.assertEqual(cnt, 7) 48 | 49 | @defer.inlineCallbacks 50 | def test_pfcount(self): 51 | self._skipCheck() 52 | key1 = self._KEYS[0] 53 | key2 = self._KEYS[1] 54 | r = yield self.db.pfadd(key1, 'foo', 'bar', 'zap') 55 | self.assertEqual(r, 1) 56 | r1 = yield self.db.pfadd(key1, 'zap', 'zap', 'zap') 57 | self.assertEqual(r1, 0) 58 | r2 = yield self.db.pfadd(key1, 'foo', 'bar') 59 | self.assertEqual(r2, 0) 60 | cnt1 = yield self.db.pfcount(key1) 61 | self.assertEqual(cnt1, 3) 62 | r3 = yield self.db.pfadd(key2, 1, 2, 3) 63 | self.assertEqual(r3, 1) 64 | cnt2 = yield self.db.pfcount(key1, key2) 65 | self.assertEqual(cnt2, 6) 66 | 67 | @defer.inlineCallbacks 68 | def test_pfmerge(self): 69 | self._skipCheck() 70 | key1 = self._KEYS[0] 71 | key2 = self._KEYS[1] 72 | key3 = self._KEYS[2] 73 | r = yield self.db.pfadd(key1, 'foo', 'bar', 'zap', 'a') 74 | self.assertEqual(r, 1) 75 | r1 = yield self.db.pfadd(key2, 'a', 'b', 'c', 'foo') 76 | self.assertEqual(r1, 1) 77 | yield self.db.pfmerge(key3, key1, key2) 78 | cnt = yield self.db.pfcount(key3) 79 | self.assertEqual(cnt, 6) 80 | 81 | def _skipCheck(self): 82 | if not self.redis_2_8_9: 83 | skipMsg = "Redis version < 2.8.9 (found version: %s)" 84 | raise unittest.SkipTest(skipMsg % self.redis_version) 85 | -------------------------------------------------------------------------------- /tests/test_implicit_pipelining.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2015 Ilya Skriblovsky 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.internet import defer 17 | from twisted.trial import unittest 18 | 19 | import txredisapi as redis 20 | 21 | from tests.mixins import REDIS_HOST, REDIS_PORT 22 | 23 | cnt_LineReceiver = 0 24 | cnt_HiredisProtocol = 0 25 | 26 | 27 | class _CallCounter(object): 28 | def __init__(self, original): 29 | self.call_count = 0 30 | self.original = original 31 | 32 | def get_callee(self): 33 | def callee(this, *args, **kwargs): 34 | self.call_count += 1 35 | return self.original(this, *args, **kwargs) 36 | return callee 37 | 38 | 39 | class TestImplicitPipelining(unittest.TestCase): 40 | KEY = 'txredisapi:key' 41 | VALUE = 'txredisapi:value' 42 | 43 | @defer.inlineCallbacks 44 | def testImplicitPipelining(self): 45 | """ 46 | Calling query method several times in a row without waiting should 47 | do implicit pipelining, so all requests are send immediately and 48 | all replies are received in one chunk with high probability 49 | """ 50 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 51 | 52 | cnt_LineReceiver = _CallCounter(redis.LineReceiver.dataReceived) 53 | self.patch(redis.LineReceiver, 'dataReceived', 54 | cnt_LineReceiver.get_callee()) 55 | cnt_HiredisProtocol = _CallCounter(redis.HiredisProtocol.dataReceived) 56 | self.patch(redis.HiredisProtocol, 'dataReceived', 57 | cnt_HiredisProtocol.get_callee()) 58 | 59 | for i in range(5): 60 | db.set(self.KEY, self.VALUE) 61 | 62 | yield db.get(self.KEY) 63 | 64 | total_data_chunks = cnt_LineReceiver.call_count + \ 65 | cnt_HiredisProtocol.call_count 66 | self.assertEqual(total_data_chunks, 1) 67 | 68 | yield db.disconnect() 69 | -------------------------------------------------------------------------------- /tests/test_list_ops.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.internet import defer 17 | from twisted.trial import unittest 18 | 19 | import txredisapi as redis 20 | 21 | from tests.mixins import REDIS_HOST, REDIS_PORT 22 | 23 | 24 | class TestRedisListOperations(unittest.TestCase): 25 | @defer.inlineCallbacks 26 | def testRedisLPUSHSingleValue(self): 27 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 28 | yield db.delete("txredisapi:LPUSH") 29 | yield db.lpush("txredisapi:LPUSH", "singlevalue") 30 | result = yield db.lpop("txredisapi:LPUSH") 31 | self.assertEqual(result, "singlevalue") 32 | yield db.disconnect() 33 | 34 | @defer.inlineCallbacks 35 | def testRedisLPUSHListOfValues(self): 36 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 37 | yield db.delete("txredisapi:LPUSH") 38 | yield db.lpush("txredisapi:LPUSH", [1, 2, 3]) 39 | result = yield db.lrange("txredisapi:LPUSH", 0, -1) 40 | self.assertEqual(result, [3, 2, 1]) 41 | yield db.disconnect() 42 | 43 | @defer.inlineCallbacks 44 | def testRedisRPUSHSingleValue(self): 45 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 46 | yield db.delete("txredisapi:RPUSH") 47 | yield db.lpush("txredisapi:RPUSH", "singlevalue") 48 | result = yield db.lpop("txredisapi:RPUSH") 49 | self.assertEqual(result, "singlevalue") 50 | yield db.disconnect() 51 | 52 | @defer.inlineCallbacks 53 | def testRedisRPUSHListOfValues(self): 54 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 55 | yield db.delete("txredisapi:RPUSH") 56 | yield db.lpush("txredisapi:RPUSH", [1, 2, 3]) 57 | result = yield db.lrange("txredisapi:RPUSH", 0, -1) 58 | self.assertEqual(result, [3, 2, 1]) 59 | yield db.disconnect() 60 | -------------------------------------------------------------------------------- /tests/test_multibulk.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import six 17 | 18 | import os 19 | import base64 20 | 21 | from twisted.internet import defer 22 | from twisted.trial import unittest 23 | 24 | import txredisapi as redis 25 | 26 | from tests.mixins import REDIS_HOST, REDIS_PORT 27 | 28 | 29 | class LargeMultiBulk(unittest.TestCase): 30 | _KEY = 'txredisapi:testlargemultibulk' 31 | 32 | @defer.inlineCallbacks 33 | def setUp(self): 34 | self.db = yield redis.Connection( 35 | REDIS_HOST, REDIS_PORT, reconnect=False, 36 | charset=None) 37 | 38 | @defer.inlineCallbacks 39 | def tearDown(self): 40 | yield self.db.delete(self._KEY) 41 | yield self.db.disconnect() 42 | 43 | @staticmethod 44 | def random_data(length): 45 | return base64.b64encode(os.urandom(10)) 46 | 47 | @defer.inlineCallbacks 48 | def _test_multibulk(self, data): 49 | yield defer.DeferredList([self.db.sadd(self._KEY, x) for x in data]) 50 | res = yield self.db.smembers(self._KEY) 51 | self.assertEqual(set(res), data) 52 | 53 | def test_large_multibulk_int(self): 54 | data = set(range(1000)) 55 | return self._test_multibulk(data) 56 | 57 | def test_large_multibulk_str(self): 58 | data = set([self.random_data(10) for x in range(100)]) 59 | return self._test_multibulk(data) 60 | 61 | @defer.inlineCallbacks 62 | def test_bulk_numeric(self): 63 | test_values = [ 64 | six.b(''), six.b('.hello'), six.b('+world'), six.b('123test'), 65 | +1, 0.1, 0.01, -0.1, 0, -10] 66 | for v in test_values: 67 | yield self.db.set(self._KEY, v) 68 | r = yield self.db.get(self._KEY) 69 | self.assertEqual(r, v) 70 | 71 | @defer.inlineCallbacks 72 | def test_bulk_corner_cases(self): 73 | ''' 74 | Python's float() function consumes '+inf', '-inf' & 'nan' values. 75 | Currently, we only convert bulk strings floating point numbers 76 | if there's a '.' in the string. 77 | This test is to ensure this behavior isn't broken in the future. 78 | ''' 79 | values = [six.b('+inf'), six.b('-inf'), six.b('NaN')] 80 | for x in values: 81 | yield self.db.set(self._KEY, x) 82 | r = yield self.db.get(self._KEY) 83 | self.assertEqual(r, x) 84 | 85 | 86 | class NestedMultiBulk(unittest.TestCase): 87 | @defer.inlineCallbacks 88 | def testNestedMultiBulkTransaction(self): 89 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 90 | 91 | test1 = {u"foo1": u"bar1", u"something": u"else"} 92 | test2 = {u"foo2": u"bar2", u"something": u"else"} 93 | 94 | t = yield db.multi() 95 | yield t.hmset("txredisapi:nmb:test1", test1) 96 | yield t.hgetall("txredisapi:nmb:test1") 97 | yield t.hmset("txredisapi:nmb:test2", test2) 98 | yield t.hgetall("txredisapi:nmb:test2") 99 | r = yield t.commit() 100 | 101 | self.assertEqual(r[0], "OK") 102 | self.assertEqual(sorted(r[1].keys()), sorted(test1.keys())) 103 | self.assertEqual(sorted(r[1].values()), sorted(test1.values())) 104 | self.assertEqual(r[2], "OK") 105 | self.assertEqual(sorted(r[3].keys()), sorted(test2.keys())) 106 | self.assertEqual(sorted(r[3].values()), sorted(test2.values())) 107 | 108 | yield db.disconnect() 109 | -------------------------------------------------------------------------------- /tests/test_number_conversions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2015 Jeethu Rao 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import six 17 | 18 | import txredisapi as redis 19 | from twisted.internet import defer 20 | from twisted.trial import unittest 21 | 22 | from .mixins import REDIS_HOST, REDIS_PORT 23 | 24 | 25 | class TestNumberConversions(unittest.TestCase): 26 | CONVERT_NUMBERS = True 27 | 28 | TEST_KEY = 'txredisapi:test_key' 29 | 30 | TEST_VECTORS = [ 31 | # Integers 32 | (u'100', 100), 33 | (100, 100), 34 | (u'0', 0), 35 | (0, 0), 36 | 37 | # Floats 38 | (u'.1', 0.1), 39 | (0.1, 0.1), 40 | 41 | # +inf and -inf aren't handled by automatic conversions 42 | # test this behavior anyway for the sake of completeness. 43 | (u'+inf', u'+inf'), 44 | (u'-inf', u'-inf'), 45 | (u'NaN', u'NaN') 46 | ] 47 | 48 | @defer.inlineCallbacks 49 | def test_number_conversions(self): 50 | for k, v in self.TEST_VECTORS: 51 | yield self.db.set(self.TEST_KEY, k) 52 | result = yield self.db.get(self.TEST_KEY) 53 | if self.CONVERT_NUMBERS: 54 | self.assertIsInstance(result, type(v)) 55 | self.assertEqual(result, v) 56 | else: 57 | if isinstance(k, float): 58 | expected = format(k, "f") 59 | else: 60 | expected = str(k) 61 | self.assertIsInstance(result, six.string_types) 62 | self.assertEqual(result, expected) 63 | 64 | @defer.inlineCallbacks 65 | def setUp(self): 66 | self.db = yield redis.Connection(REDIS_HOST, REDIS_PORT, 67 | convertNumbers=self.CONVERT_NUMBERS) 68 | 69 | @defer.inlineCallbacks 70 | def tearDown(self): 71 | yield self.db.delete(self.TEST_KEY) 72 | yield self.db.disconnect() 73 | 74 | 75 | class TestNoNumberConversions(TestNumberConversions): 76 | CONVERT_NUMBERS = False 77 | 78 | @defer.inlineCallbacks 79 | def test_hashes(self): 80 | d = { 81 | 'a': 1, 82 | 'b': '2' 83 | } 84 | expected = { 85 | 'a': '1', 86 | 'b': '2' 87 | } 88 | yield self.db.hmset(self.TEST_KEY, d) 89 | r = yield self.db.hgetall(self.TEST_KEY) 90 | self.assertEqual(r, expected) 91 | -------------------------------------------------------------------------------- /tests/test_operations.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.internet import defer, reactor 17 | from twisted.trial import unittest 18 | 19 | import txredisapi as redis 20 | 21 | from tests.mixins import REDIS_HOST, REDIS_PORT 22 | 23 | 24 | class TestRedisConnections(unittest.TestCase): 25 | @defer.inlineCallbacks 26 | def testRedisOperations1(self): 27 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 28 | 29 | # test set() operation 30 | kvpairs = (("txredisapi:test1", "foo"), ("txredisapi:test2", "bar")) 31 | for key, value in kvpairs: 32 | yield db.set(key, value) 33 | result = yield db.get(key) 34 | self.assertEqual(result, value) 35 | 36 | yield db.disconnect() 37 | 38 | @defer.inlineCallbacks 39 | def testRedisOperations2(self): 40 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 41 | 42 | k = ["txredisapi:a", "txredisapi:b"] 43 | v = [1, 2] 44 | yield db.mset(dict(zip(k, v))) 45 | values = yield db.mget(k) 46 | self.assertEqual(values, v) 47 | 48 | k = ['txredisapi:a', 'txredisapi:notset', 'txredisapi:b'] 49 | values = yield db.mget(k) 50 | self.assertEqual(values, [1, None, 2]) 51 | 52 | yield db.disconnect() 53 | 54 | @defer.inlineCallbacks 55 | def testRedisError(self): 56 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 57 | yield db.set('txredisapi:a', 'test') 58 | try: 59 | yield db.sort('txredisapi:a', end='a') 60 | except redis.RedisError: 61 | pass 62 | else: 63 | yield db.disconnect() 64 | self.fail('RedisError not raised') 65 | 66 | try: 67 | yield db.incr('txredisapi:a') 68 | except redis.ResponseError: 69 | pass 70 | else: 71 | yield db.disconnect() 72 | self.fail('ResponseError not raised on redis error') 73 | yield db.disconnect() 74 | try: 75 | yield db.get('txredisapi:a') 76 | except redis.ConnectionError: 77 | pass 78 | else: 79 | self.fail('ConnectionError not raised') 80 | 81 | @defer.inlineCallbacks 82 | def testRedisOperationsSet1(self): 83 | 84 | def sleep(secs): 85 | d = defer.Deferred() 86 | reactor.callLater(secs, d.callback, None) 87 | return d 88 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 89 | key, value = "txredisapi:test1", "foo" 90 | # test expiration in milliseconds 91 | yield db.set(key, value, pexpire=10) 92 | result_1 = yield db.get(key) 93 | self.assertEqual(result_1, value) 94 | yield sleep(0.015) 95 | result_2 = yield db.get(key) 96 | self.assertEqual(result_2, None) 97 | 98 | # same thing but timeout in seconds 99 | yield db.set(key, value, expire=1) 100 | result_3 = yield db.get(key) 101 | self.assertEqual(result_3, value) 102 | yield sleep(1.001) 103 | result_4 = yield db.get(key) 104 | self.assertEqual(result_4, None) 105 | yield db.disconnect() 106 | 107 | @defer.inlineCallbacks 108 | def testRedisOperationsSet2(self): 109 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 110 | key, value = "txredisapi:test_exists", "foo" 111 | # ensure value does not exits and new value sets 112 | yield db.delete(key) 113 | yield db.set(key, value, only_if_not_exists=True) 114 | result_1 = yield db.get(key) 115 | self.assertEqual(result_1, value) 116 | 117 | # new values not set cos, values exists 118 | yield db.set(key, "foo2", only_if_not_exists=True) 119 | result_2 = yield db.get(key) 120 | # nothing changed result is same "foo" 121 | self.assertEqual(result_2, value) 122 | yield db.disconnect() 123 | 124 | @defer.inlineCallbacks 125 | def testRedisOperationsSet3(self): 126 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 127 | key, value = "txredisapi:test_not_exists", "foo_not_exists" 128 | # ensure that such key does not exits, and value not sets 129 | yield db.delete(key) 130 | yield db.set(key, value, only_if_exists=True) 131 | result_1 = yield db.get(key) 132 | self.assertEqual(result_1, None) 133 | 134 | # ensure key exits, and value updates 135 | yield db.set(key, value) 136 | yield db.set(key, "foo", only_if_exists=True) 137 | result_2 = yield db.get(key) 138 | self.assertEqual(result_2, "foo") 139 | yield db.disconnect() 140 | 141 | @defer.inlineCallbacks 142 | def testRedisOperationTime(self): 143 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 144 | 145 | time = yield db.time() 146 | self.assertIsInstance(time, list) 147 | self.assertEqual(len(time), 2) 148 | self.assertIsInstance(time[0], int) 149 | self.assertIsInstance(time[1], int) 150 | 151 | yield db.disconnect() 152 | -------------------------------------------------------------------------------- /tests/test_pipelining.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2013 Matt Pizzimenti 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | 18 | import six 19 | 20 | from twisted.trial import unittest 21 | from twisted.internet import defer 22 | from twisted.python import log 23 | 24 | import txredisapi 25 | 26 | from tests.mixins import REDIS_HOST, REDIS_PORT 27 | 28 | # log.startLogging(sys.stdout) 29 | 30 | 31 | class InspectableTransport(object): 32 | 33 | def __init__(self, transport): 34 | self.original_transport = transport 35 | self.write_history = [] 36 | 37 | def __getattr__(self, method): 38 | 39 | if method == "write": 40 | def write(data, *args, **kwargs): 41 | self.write_history.append(data) 42 | return self.original_transport.write(data, *args, **kwargs) 43 | return write 44 | return getattr(self.original_transport, method) 45 | 46 | 47 | class TestRedisConnections(unittest.TestCase): 48 | 49 | @defer.inlineCallbacks 50 | def _assert_simple_sets_on_pipeline(self, db): 51 | 52 | pipeline = yield db.pipeline() 53 | self.assertTrue(pipeline.pipelining) 54 | 55 | # Hook into the transport so we can inspect what is happening 56 | # at the protocol level. 57 | pipeline.transport = InspectableTransport(pipeline.transport) 58 | 59 | pipeline.set("txredisapi:test_pipeline", "foo") 60 | pipeline.set("txredisapi:test_pipeline", "bar") 61 | pipeline.set("txredisapi:test_pipeline2", "zip") 62 | 63 | yield pipeline.execute_pipeline() 64 | self.assertFalse(pipeline.pipelining) 65 | 66 | result = yield db.get("txredisapi:test_pipeline") 67 | self.assertEqual(result, "bar") 68 | 69 | result = yield db.get("txredisapi:test_pipeline2") 70 | self.assertEqual(result, "zip") 71 | 72 | # Make sure that all SET commands were sent in a single pipelined write. 73 | write_history = pipeline.transport.write_history 74 | lines_in_first_write = write_history[0].split(six.b("\n")) 75 | sets_in_first_write = sum(1 for w in lines_in_first_write if six.b("SET") in w) 76 | self.assertEqual(sets_in_first_write, 3) 77 | 78 | @defer.inlineCallbacks 79 | def _wait_for_lazy_connection(self, db): 80 | 81 | # For lazy connections, wait for the internal deferred to indicate 82 | # that the connection is established. 83 | yield db._connected 84 | 85 | @defer.inlineCallbacks 86 | def test_Connection(self): 87 | 88 | db = yield txredisapi.Connection(REDIS_HOST, REDIS_PORT, 89 | reconnect=False) 90 | yield self._assert_simple_sets_on_pipeline(db=db) 91 | yield db.disconnect() 92 | 93 | @defer.inlineCallbacks 94 | def test_ConnectionDB1(self): 95 | 96 | db = yield txredisapi.Connection(REDIS_HOST, REDIS_PORT, dbid=1, 97 | reconnect=False) 98 | yield self._assert_simple_sets_on_pipeline(db=db) 99 | yield db.disconnect() 100 | 101 | @defer.inlineCallbacks 102 | def test_ConnectionPool(self): 103 | 104 | db = yield txredisapi.ConnectionPool(REDIS_HOST, REDIS_PORT, poolsize=2, 105 | reconnect=False) 106 | yield self._assert_simple_sets_on_pipeline(db=db) 107 | yield db.disconnect() 108 | 109 | @defer.inlineCallbacks 110 | def test_ConnectionPool_managed_correctly(self): 111 | 112 | db = yield txredisapi.ConnectionPool(REDIS_HOST, REDIS_PORT, poolsize=1, 113 | reconnect=False) 114 | 115 | yield db.set('key1', 'value1') 116 | 117 | pipeline = yield db.pipeline() 118 | pipeline.get('key1') 119 | 120 | # We will yield after we finish the pipeline so we won't block here 121 | d = db.set('key2', 'value2') 122 | 123 | results = yield pipeline.execute_pipeline() 124 | 125 | # If the pipeline is managed correctly, there should only be one 126 | # response here 127 | self.assertEqual(len(results), 1) 128 | 129 | yield d 130 | yield db.disconnect() 131 | 132 | @defer.inlineCallbacks 133 | def test_lazyConnection(self): 134 | 135 | db = txredisapi.lazyConnection(REDIS_HOST, REDIS_PORT, reconnect=False) 136 | yield self._wait_for_lazy_connection(db) 137 | yield self._assert_simple_sets_on_pipeline(db=db) 138 | yield db.disconnect() 139 | 140 | @defer.inlineCallbacks 141 | def test_lazyConnectionPool(self): 142 | 143 | db = txredisapi.lazyConnectionPool(REDIS_HOST, REDIS_PORT, reconnect=False) 144 | yield self._wait_for_lazy_connection(db) 145 | yield self._assert_simple_sets_on_pipeline(db=db) 146 | yield db.disconnect() 147 | 148 | @defer.inlineCallbacks 149 | def test_ShardedConnection(self): 150 | 151 | hosts = ["%s:%s" % (REDIS_HOST, REDIS_PORT)] 152 | db = yield txredisapi.ShardedConnection(hosts, reconnect=False) 153 | try: 154 | yield db.pipeline() 155 | raise self.failureException("Expected sharding to disallow pipelining") 156 | except NotImplementedError as e: 157 | self.assertTrue("not supported" in str(e).lower()) 158 | yield db.disconnect() 159 | 160 | @defer.inlineCallbacks 161 | def test_ShardedConnectionPool(self): 162 | 163 | hosts = ["%s:%s" % (REDIS_HOST, REDIS_PORT)] 164 | db = yield txredisapi.ShardedConnectionPool(hosts, reconnect=False) 165 | try: 166 | yield db.pipeline() 167 | raise self.failureException("Expected sharding to disallow pipelining") 168 | except NotImplementedError as e: 169 | self.assertTrue("not supported" in str(e).lower()) 170 | yield db.disconnect() 171 | -------------------------------------------------------------------------------- /tests/test_protocol.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2015 Jeethu Rao 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import six 17 | 18 | import txredisapi as redis 19 | 20 | from twisted.trial import unittest 21 | from twisted.internet.protocol import ClientFactory 22 | from twisted.test.proto_helpers import StringTransportWithDisconnection 23 | from twisted.internet import task 24 | 25 | 26 | class MockFactory(ClientFactory): 27 | pass 28 | 29 | 30 | class LineReceiverSubclass(redis.LineReceiver): 31 | def lineReceived(self, line): 32 | self._rcvd_line = line 33 | 34 | def rawDataReceived(self, data): 35 | self._rcvd_data = data 36 | 37 | 38 | class TestLineReciever(unittest.TestCase): 39 | S = six.b('TEST') 40 | 41 | def setUp(self): 42 | self.proto = LineReceiverSubclass() 43 | self.transport = StringTransportWithDisconnection() 44 | self.proto.makeConnection(self.transport) 45 | self.transport.protocol = self.proto 46 | self.proto.factory = MockFactory() 47 | 48 | def test_excess_line_length(self): 49 | self.assertTrue(self.transport.connected) 50 | self.proto.dataReceived(six.b('\x00') * (self.proto.MAX_LENGTH + 1)) 51 | self.assertFalse(self.transport.connected) 52 | 53 | def test_excess_delimited_line(self): 54 | self.assertTrue(self.transport.connected) 55 | self.proto.dataReceived(self.S + self.proto.delimiter) 56 | self.assertEqual(self.proto._rcvd_line, self.S.decode()) 57 | s = (six.b('\x00') * (self.proto.MAX_LENGTH + 1)) + self.proto.delimiter 58 | self.proto._rcvd_line = None 59 | self.proto.dataReceived(s) 60 | self.assertFalse(self.transport.connected) 61 | self.assertIs(self.proto._rcvd_line, None) 62 | 63 | def test_clear_line_buffer(self): 64 | self.proto.dataReceived(self.S) 65 | self.assertEqual(self.proto.clearLineBuffer(), self.S) 66 | 67 | def test_send_line(self): 68 | self.proto.dataReceived(self.S + self.proto.delimiter) 69 | self.assertEqual(self.proto._rcvd_line, self.S.decode()) 70 | 71 | def test_raw_data(self): 72 | clock = task.Clock() 73 | self.proto.callLater = clock.callLater 74 | self.proto.setRawMode() 75 | s = self.S + self.proto.delimiter 76 | self.proto.dataReceived(s) 77 | self.assertEqual(self.proto._rcvd_data, s) 78 | self.proto._rcvd_line = None 79 | self.proto.setLineMode(s) 80 | clock.advance(1) 81 | self.assertEqual(self.proto._rcvd_line, self.S.decode()) 82 | self.proto.dataReceived(s) 83 | self.assertEqual(self.proto._rcvd_line, self.S.decode()) 84 | 85 | def test_sendline(self): 86 | self.proto.sendLine(self.S) 87 | value = self.transport.value() 88 | self.assertEqual(value, self.S + self.proto.delimiter) 89 | 90 | 91 | class TestBaseRedisProtocol(unittest.TestCase): 92 | def setUp(self): 93 | self._protocol = redis.BaseRedisProtocol() 94 | 95 | def test_build_ping(self): 96 | s = self._protocol._build_command("PING") 97 | self.assertEqual(s, six.b('*1\r\n$4\r\nPING\r\n')) 98 | -------------------------------------------------------------------------------- /tests/test_publish.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.internet import defer 17 | from twisted.trial import unittest 18 | 19 | import txredisapi as redis 20 | 21 | from tests.mixins import REDIS_HOST, REDIS_PORT 22 | 23 | 24 | class TestRedisConnections(unittest.TestCase): 25 | @defer.inlineCallbacks 26 | def testRedisPublish(self): 27 | db = yield redis.Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 28 | 29 | for value in ("foo", "bar"): 30 | yield db.publish("test_publish", value) 31 | 32 | yield db.disconnect() 33 | -------------------------------------------------------------------------------- /tests/test_scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # 4 | # Copyright 2014 Ilia Glazkov 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | from txredisapi import Connection 20 | 21 | from twisted.internet.defer import inlineCallbacks 22 | from twisted.trial import unittest 23 | 24 | from .mixins import RedisVersionCheckMixin, REDIS_HOST, REDIS_PORT 25 | 26 | 27 | class TestScan(unittest.TestCase, RedisVersionCheckMixin): 28 | KEYS = ['_scan_test_' + str(v).zfill(4) for v in range(100)] 29 | SKEY = ['_scan_test_set'] 30 | SUFFIX = '12' 31 | PATTERN = '_scan_test_*' + SUFFIX 32 | 33 | @inlineCallbacks 34 | def setUp(self): 35 | self.FILTERED_KEYS = [k for k in self.KEYS if k.endswith(self.SUFFIX)] 36 | self.db = yield Connection(REDIS_HOST, REDIS_PORT, reconnect=False) 37 | self.redis_2_8_0 = yield self.checkVersion(2, 8, 0) 38 | yield self.db.delete(*self.KEYS) 39 | yield self.db.delete(self.SKEY) 40 | 41 | @inlineCallbacks 42 | def tearDown(self): 43 | yield self.db.delete(*self.KEYS) 44 | yield self.db.delete(self.SKEY) 45 | yield self.db.disconnect() 46 | 47 | @inlineCallbacks 48 | def test_scan(self): 49 | self._skipCheck() 50 | yield self.db.mset(dict((k, 'value') for k in self.KEYS)) 51 | 52 | cursor, result = yield self.db.scan(pattern=self.PATTERN) 53 | 54 | while cursor != 0: 55 | cursor, keys = yield self.db.scan(cursor, pattern=self.PATTERN) 56 | result.extend(keys) 57 | 58 | self.assertEqual(set(result), set(self.FILTERED_KEYS)) 59 | 60 | @inlineCallbacks 61 | def test_sscan(self): 62 | self._skipCheck() 63 | yield self.db.sadd(self.SKEY, self.KEYS) 64 | 65 | cursor, result = yield self.db.sscan(self.SKEY, pattern=self.PATTERN) 66 | 67 | while cursor != 0: 68 | cursor, keys = yield self.db.sscan(self.SKEY, cursor, 69 | pattern=self.PATTERN) 70 | result.extend(keys) 71 | 72 | self.assertEqual(set(result), set(self.FILTERED_KEYS)) 73 | 74 | def _skipCheck(self): 75 | if not self.redis_2_8_0: 76 | skipMsg = "Redis version < 2.8.0 (found version: %s)" 77 | raise unittest.SkipTest(skipMsg % self.redis_version) 78 | -------------------------------------------------------------------------------- /tests/test_scripting.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | import hashlib 18 | 19 | import six 20 | 21 | import txredisapi as redis 22 | 23 | from twisted.internet import defer 24 | from twisted.trial import unittest 25 | from twisted.internet import reactor 26 | from twisted.python import failure 27 | 28 | from tests.mixins import Redis26CheckMixin, REDIS_HOST, REDIS_PORT 29 | 30 | 31 | class TestScripting(unittest.TestCase, Redis26CheckMixin): 32 | _SCRIPT = "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" # From redis example 33 | 34 | @defer.inlineCallbacks 35 | def setUp(self): 36 | self.db = yield redis.Connection(REDIS_HOST, REDIS_PORT, 37 | reconnect=False) 38 | self.db1 = None 39 | self.redis_2_6 = yield self.is_redis_2_6() 40 | yield self.db.script_flush() 41 | 42 | @defer.inlineCallbacks 43 | def tearDown(self): 44 | yield self.db.disconnect() 45 | if self.db1 is not None: 46 | yield self.db1.disconnect() 47 | 48 | @defer.inlineCallbacks 49 | def test_eval(self): 50 | self._skipCheck() 51 | keys = ('key1', 'key2') 52 | args = ('first', 'second') 53 | r = yield self.db.eval(self._SCRIPT, keys, args) 54 | self._check_eval_result(keys, args, r) 55 | r = yield self.db.eval("return 10") 56 | self.assertEqual(r, 10) 57 | r = yield self.db.eval("return {1,2,3.3333,'foo',nil,'bar'}") 58 | self.assertEqual(r, [1, 2, 3, 'foo']) 59 | # Test the case where the hash is in script_hashes, 60 | # but redis doesn't have it 61 | h = self._hash_script(self._SCRIPT) 62 | yield self.db.script_flush() 63 | conn = yield self.db._factory.getConnection(True) 64 | conn.script_hashes.add(h) 65 | r = yield self.db.eval(self._SCRIPT, keys, args) 66 | self._check_eval_result(keys, args, r) 67 | 68 | @defer.inlineCallbacks 69 | def test_eval_keys_only(self): 70 | self._skipCheck() 71 | keys = ['foo', 'bar'] 72 | args = [] 73 | 74 | r = yield self.db.eval("return {KEYS[1],KEYS[2]}", keys, args) 75 | self.assertEqual(r, keys) 76 | 77 | r = yield self.db.eval("return {KEYS[1],KEYS[2]}", keys=keys) 78 | self.assertEqual(r, keys) 79 | 80 | @defer.inlineCallbacks 81 | def test_eval_args_only(self): 82 | self._skipCheck() 83 | keys = [] 84 | args = ['first', 'second'] 85 | 86 | r = yield self.db.eval("return {ARGV[1],ARGV[2]}", keys, args) 87 | self.assertEqual(r, args) 88 | 89 | r = yield self.db.eval("return {ARGV[1],ARGV[2]}", args=args) 90 | self.assertEqual(r, args) 91 | 92 | @defer.inlineCallbacks 93 | def test_eval_error(self): 94 | self._skipCheck() 95 | try: 96 | result = yield self.db.eval('return {err="My Error"}') 97 | except redis.ResponseError: 98 | pass 99 | except: 100 | raise self.failureException('%s raised instead of %s:\n %s' 101 | % (sys.exc_info()[0], 102 | 'txredisapi.ResponseError', 103 | failure.Failure().getTraceback())) 104 | else: 105 | raise self.failureException('%s not raised (%r returned)' 106 | % ('txredisapi.ResponseError', result)) 107 | 108 | @defer.inlineCallbacks 109 | def test_evalsha(self): 110 | self._skipCheck() 111 | r = yield self.db.eval(self._SCRIPT) 112 | h = self._hash_script(self._SCRIPT) 113 | r = yield self.db.evalsha(h) 114 | self._check_eval_result([], [], r) 115 | 116 | @defer.inlineCallbacks 117 | def test_evalsha_error(self): 118 | self._skipCheck() 119 | h = self._hash_script(self._SCRIPT) 120 | try: 121 | result = yield self.db.evalsha(h) 122 | except redis.ScriptDoesNotExist: 123 | pass 124 | except: 125 | raise self.failureException('%s raised instead of %s:\n %s' 126 | % (sys.exc_info()[0], 127 | 'txredisapi.ScriptDoesNotExist', 128 | failure.Failure().getTraceback())) 129 | else: 130 | raise self.failureException('%s not raised (%r returned)' 131 | % ('txredisapi.ResponseError', result)) 132 | 133 | @defer.inlineCallbacks 134 | def test_script_load(self): 135 | self._skipCheck() 136 | h = self._hash_script(self._SCRIPT) 137 | r = yield self.db.script_exists(h) 138 | self.assertFalse(r) 139 | r = yield self.db.script_load(self._SCRIPT) 140 | self.assertEqual(r, h) 141 | r = yield self.db.script_exists(h) 142 | self.assertTrue(r) 143 | 144 | @defer.inlineCallbacks 145 | def test_script_exists(self): 146 | self._skipCheck() 147 | h = self._hash_script(self._SCRIPT) 148 | script1 = "return 1" 149 | h1 = self._hash_script(script1) 150 | r = yield self.db.script_exists(h) 151 | self.assertFalse(r) 152 | r = yield self.db.script_exists(h, h1) 153 | self.assertEqual(r, [False, False]) 154 | yield self.db.script_load(script1) 155 | r = yield self.db.script_exists(h, h1) 156 | self.assertEqual(r, [False, True]) 157 | yield self.db.script_load(self._SCRIPT) 158 | r = yield self.db.script_exists(h, h1) 159 | self.assertEqual(r, [True, True]) 160 | 161 | @defer.inlineCallbacks 162 | def test_script_kill(self): 163 | self._skipCheck() 164 | try: 165 | result = yield self.db.script_kill() 166 | except redis.NoScriptRunning: 167 | pass 168 | except: 169 | raise self.failureException('%s raised instead of %s:\n %s' 170 | % (sys.exc_info()[0], 171 | 'txredisapi.NoScriptRunning', 172 | failure.Failure().getTraceback())) 173 | else: 174 | raise self.failureException('%s not raised (%r returned)' 175 | % ('txredisapi.ResponseError', result)) 176 | # Run an infinite loop script from one connection 177 | # and kill it from another. 178 | inf_loop = "while 1 do end" 179 | self.db1 = yield redis.Connection(REDIS_HOST, REDIS_PORT, 180 | reconnect=False) 181 | eval_deferred = self.db1.eval(inf_loop) 182 | reactor.iterate() 183 | r = yield self.db.script_kill() 184 | self.assertEqual(r, 'OK') 185 | try: 186 | result = yield eval_deferred 187 | except redis.ResponseError: 188 | pass 189 | except: 190 | raise self.failureException('%s raised instead of %s:\n %s' 191 | % (sys.exc_info()[0], 192 | 'txredisapi.ResponseError', 193 | failure.Failure().getTraceback())) 194 | else: 195 | raise self.failureException('%s not raised (%r returned)' 196 | % ('txredisapi.ResponseError', result)) 197 | 198 | def _check_eval_result(self, keys, args, r): 199 | self.assertEqual(r, list(keys) + list(args)) 200 | 201 | def _hash_script(self, script): 202 | return hashlib.sha1(script.encode()).hexdigest() 203 | -------------------------------------------------------------------------------- /tests/test_sentinel.py: -------------------------------------------------------------------------------- 1 | import six 2 | import sys 3 | from twisted.internet import defer, reactor 4 | from twisted.internet.protocol import Factory 5 | from twisted.trial.unittest import TestCase 6 | 7 | from txredisapi import BaseRedisProtocol, Sentinel, MasterNotFoundError 8 | 9 | if sys.version_info >= (3, 3): 10 | from unittest.mock import Mock 11 | else: 12 | from mock import Mock 13 | 14 | 15 | class FakeRedisProtocol(BaseRedisProtocol): 16 | @classmethod 17 | def _encode_value(cls, value): 18 | if value is None: 19 | return b'$-1\r\n' 20 | 21 | if isinstance(value, (list, tuple)): 22 | parts = [b'*', str(len(value)).encode('ascii'), b'\r\n'] 23 | parts.extend(cls._encode_value(x) for x in value) 24 | return b''.join(parts) 25 | 26 | if isinstance(value, six.text_type): 27 | binary = value.encode("utf-8") 28 | elif isinstance(value, bytes): 29 | binary = value 30 | else: 31 | binary = six.text_type(value).encode("utf-8") 32 | return b''.join([b'$', str(len(binary)).encode('ascii'), b'\r\n', binary, b'\r\n']) 33 | 34 | def send_reply(self, reply): 35 | self.transport.write(self._encode_value(reply)) 36 | 37 | def send_error(self, msg): 38 | self.transport.write(b''.join([b"-ERR ", msg.encode("utf-8"), b"\r\n"])) 39 | 40 | def replyReceived(self, request): 41 | if isinstance(request, list): 42 | if request[0] == "ROLE": 43 | self.send_reply(self.factory.role) 44 | 45 | else: 46 | self.send_error("Command not supported") 47 | else: 48 | BaseRedisProtocol.replyReceived(self, request) 49 | 50 | 51 | class FakeAuthenticatedRedisProtocol(FakeRedisProtocol): 52 | def __init__(self, requirepass): 53 | FakeRedisProtocol.__init__(self) 54 | self.requirepass = requirepass 55 | self.authenticated = False 56 | 57 | def replyReceived(self, request): 58 | if isinstance(request, list): 59 | if request[0] == "AUTH": 60 | if request[1] == self.requirepass: 61 | self.authenticated = True 62 | self.send_reply("OK") 63 | else: 64 | self.send_error("invalid password") 65 | else: 66 | if self.authenticated: 67 | FakeRedisProtocol.replyReceived(self, request) 68 | else: 69 | self.send_error("authentication required") 70 | else: 71 | FakeRedisProtocol.replyReceived(self, request) 72 | 73 | 74 | class FakeSentinelProtocol(FakeRedisProtocol): 75 | def replyReceived(self, request): 76 | if request == ["SENTINEL", "MASTERS"]: 77 | host, port = self.factory.master_addr 78 | response = [["name", "test", 79 | "ip", host, 80 | "port", port, 81 | "flags", self.factory.master_flags, 82 | "num-other-sentinels", self.factory.num_other_sentinels]] 83 | self.send_reply(response) 84 | 85 | elif request[:2] == ["SENTINEL", "SLAVES"]: 86 | service_name = request[2] 87 | if service_name == self.factory.service_name: 88 | response = [ 89 | ["name", "{0}:{1}".format(host, port), 90 | "ip", host, 91 | "port", port, 92 | "flags", flags, 93 | "num-other-sentinels", self.factory.num_other_sentinels] 94 | for (host, port), flags in zip(self.factory.slave_addrs, self.factory.slave_flags) 95 | ] 96 | self.send_reply(response) 97 | else: 98 | self.send_error("No such master with that name") 99 | 100 | else: 101 | FakeRedisProtocol.replyReceived(self, request) 102 | 103 | 104 | class FakeRedisFactory(Factory): 105 | protocol = FakeRedisProtocol 106 | 107 | role = ["master", 0, ["127.0.0.1", 63791, 0]] 108 | 109 | 110 | class FakeAuthenticatedRedisFactory(FakeRedisFactory): 111 | def __init__(self, requirepass): 112 | self.requirepass = requirepass 113 | 114 | def buildProtocol(self, addr): 115 | proto = FakeAuthenticatedRedisProtocol(self.requirepass) 116 | proto.factory = self 117 | return proto 118 | 119 | 120 | class FakeSentinelFactory(FakeRedisFactory): 121 | protocol = FakeSentinelProtocol 122 | 123 | role = ["sentinel", ["test"]] 124 | 125 | service_name = "test" 126 | 127 | master_addr = ("127.0.0.1", 6379) 128 | master_flags = "master" 129 | slave_addrs = (("127.0.0.1", 63791), ("127.0.0.1", 63792)) 130 | slave_flags = ("slave", "slave") 131 | 132 | num_other_sentinels = 0 133 | 134 | 135 | class TestSentinelDiscovery(TestCase): 136 | 137 | sentinel_ports = [46379, 46380, 46381] 138 | 139 | def setUp(self): 140 | self.fake_sentinels = [FakeSentinelFactory() for _ in self.sentinel_ports] 141 | self.listeners = [reactor.listenTCP(port, fake_sentinel) 142 | for fake_sentinel, port in zip(self.fake_sentinels, self.sentinel_ports)] 143 | 144 | self.client = Sentinel([("127.0.0.1", port) for port in self.sentinel_ports]) 145 | self.client.discovery_timeout = 1 146 | 147 | @defer.inlineCallbacks 148 | def tearDown(self): 149 | yield self.client.disconnect() 150 | for listener in self.listeners: 151 | listener.stopListening() 152 | 153 | @defer.inlineCallbacks 154 | def test_master(self): 155 | addr = yield self.client.discover_master("test") 156 | self.assertEqual(addr, FakeSentinelFactory.master_addr) 157 | 158 | @defer.inlineCallbacks 159 | def test_master_invalid_name(self): 160 | with self.assertRaises(MasterNotFoundError): 161 | yield self.client.discover_master("invalid") 162 | 163 | @defer.inlineCallbacks 164 | def test_master_fail_one_sentinel(self): 165 | # If a sentinel says it's master is down, discover should 166 | # be successful using another sentinels 167 | self.fake_sentinels[0].master_flags = "master,s_down" 168 | yield self.client.discover_master("test") 169 | 170 | @defer.inlineCallbacks 171 | def test_master_fail_all_sentinels(self): 172 | # If all sentinels claim the master is down, discover should fail 173 | for sentinel in self.fake_sentinels: 174 | sentinel.master_flags = "master,s_down" 175 | 176 | with self.assertRaises(MasterNotFoundError): 177 | yield self.client.discover_master("test") 178 | 179 | @defer.inlineCallbacks 180 | def test_master_no_min_sentinels(self): 181 | # Obey responses only from sentinels that talk to >= min_other_sentinels 182 | self.client.min_other_sentinels = 2 183 | with self.assertRaises(MasterNotFoundError): 184 | yield self.client.discover_master("test") 185 | 186 | for i, sentinel in enumerate(self.fake_sentinels): 187 | sentinel.num_other_sentinels = i 188 | sentinel.master_addr = (sentinel.master_addr, i) 189 | 190 | addr = yield self.client.discover_master("test") 191 | self.assertEqual(addr[1], 2) 192 | 193 | 194 | @defer.inlineCallbacks 195 | def test_slaves(self): 196 | addrs = yield self.client.discover_slaves("test") 197 | self.assertEqual(addrs, list(FakeSentinelFactory.slave_addrs)) 198 | 199 | @defer.inlineCallbacks 200 | def test_slaves_invalid_name(self): 201 | addrs = yield self.client.discover_slaves("invalid") 202 | self.assertEqual(addrs, []) 203 | 204 | @defer.inlineCallbacks 205 | def test_slaves_fail_one_sentinel(self): 206 | self.fake_sentinels[0].slave_flags = ["slave,s_down", "slave,s_down"] 207 | yield self.client.discover_slaves("test") 208 | 209 | @defer.inlineCallbacks 210 | def test_slaves_fail_all_sentinels(self): 211 | for sentinel in self.fake_sentinels: 212 | sentinel.slave_flags = ["slave,s_down", "slave,s_down"] 213 | 214 | addrs = yield self.client.discover_slaves("test") 215 | self.assertEqual(addrs, []) 216 | 217 | 218 | class TestConnectViaSentinel(TestCase): 219 | 220 | master_port = 36379 221 | slave_port = 36380 222 | sentinel_port = 46379 223 | 224 | def setUp(self): 225 | self.fake_master = FakeRedisFactory() 226 | self.master_listener = reactor.listenTCP(self.master_port, self.fake_master) 227 | self.fake_slave = FakeRedisFactory() 228 | self.fake_slave.role = ["slave", "127.0.0.1", self.master_port, "connected", 0] 229 | self.slave_listener = reactor.listenTCP(self.slave_port, self.fake_slave) 230 | 231 | self.fake_sentinel = FakeSentinelFactory() 232 | self.fake_sentinel.master_addr = ("127.0.0.1", self.master_port) 233 | self.fake_sentinel.slave_addrs = [("127.0.0.1", self.slave_port)] 234 | self.fake_sentinel.slave_flags = ["slave"] 235 | self.sentinel_listener = reactor.listenTCP(self.sentinel_port, self.fake_sentinel) 236 | 237 | self.client = Sentinel([("127.0.0.1", self.sentinel_port)]) 238 | self.client.discovery_timeout = 1 239 | 240 | @defer.inlineCallbacks 241 | def tearDown(self): 242 | yield self.client.disconnect() 243 | self.sentinel_listener.stopListening() 244 | self.master_listener.stopListening() 245 | self.slave_listener.stopListening() 246 | 247 | @defer.inlineCallbacks 248 | def test_master(self): 249 | conn = self.client.master_for("test") 250 | reply = yield conn.role() 251 | self.assertEqual(reply[0], "master") 252 | yield conn.disconnect() 253 | 254 | @defer.inlineCallbacks 255 | def test_retry_on_error(self): 256 | self.client.discover_master = Mock(side_effect=[defer.fail(MasterNotFoundError()), 257 | defer.fail(MasterNotFoundError()), 258 | defer.succeed(self.fake_sentinel.master_addr)]) 259 | conn = self.client.master_for("test") 260 | yield conn.role() 261 | self.assertEqual(self.client.discover_master.call_count, 3) 262 | yield conn.disconnect() 263 | 264 | @defer.inlineCallbacks 265 | def test_retry_unexpected_role(self): 266 | self.fake_master.role = ["slave", "127.0.0.1", self.slave_port, "connected", 0] 267 | 268 | def side_effect(*args, **kwargs): 269 | if self.client.discover_master.call_count > 2: 270 | self.fake_master.role = FakeRedisFactory.role 271 | return defer.succeed(self.fake_sentinel.master_addr) 272 | 273 | self.client.discover_master = Mock(side_effect=side_effect) 274 | conn = self.client.master_for("test") 275 | reply = yield conn.role() 276 | self.assertEqual(reply[0], "master") 277 | self.assertEqual(self.client.discover_master.call_count, 3) 278 | yield conn.disconnect() 279 | 280 | @defer.inlineCallbacks 281 | def test_slave(self): 282 | conn = self.client.slave_for("test") 283 | reply = yield conn.role() 284 | self.assertEqual(reply[0], "slave") 285 | yield conn.disconnect() 286 | 287 | @defer.inlineCallbacks 288 | def test_fallback_to_master_if_no_slaves(self): 289 | self.client.discover_slaves = Mock(return_value=defer.succeed([])) 290 | conn = self.client.slave_for("test") 291 | reply = yield conn.role() 292 | self.assertEqual(reply[0], "master") 293 | yield conn.disconnect() 294 | 295 | @staticmethod 296 | def _delay(secs): 297 | d = defer.Deferred() 298 | reactor.callLater(secs, d.callback, None) 299 | return d 300 | 301 | @defer.inlineCallbacks 302 | def test_drop_all_when_master_changes(self): 303 | # When master address change detected, factory should drop and reestablish 304 | # all its connections 305 | 306 | conn = self.client.master_for("test", poolsize=3) 307 | yield conn.role() # wait for connection 308 | 309 | addrs = [proto.transport.getPeer() for proto in conn._factory.pool] 310 | self.assertTrue(len(addrs), 3) 311 | self.assertTrue(all(addr.port == self.master_port for addr in addrs)) 312 | 313 | # Change master address at sentinel and change role of the slave to master 314 | self.fake_sentinel.master_addr = ("127.0.0.1", self.slave_port) 315 | self.fake_slave.role = ["master", 0, ["127.0.0.1", self.slave_port, 0]] 316 | 317 | # Force reconnection of one connection 318 | conn._factory.pool[0].transport.loseConnection() 319 | 320 | # After a short time all connections should be to the new master 321 | yield self._delay(0.2) 322 | addrs = [proto.transport.getPeer() for proto in conn._factory.pool] 323 | self.assertTrue(len(addrs), 3) 324 | self.assertTrue(all(addr.port == self.slave_port for addr in addrs)) 325 | 326 | yield conn.disconnect() 327 | 328 | 329 | class TestAuthViaSentinel(TestCase): 330 | master_port = 36379 331 | sentinel_port = 46379 332 | 333 | def setUp(self): 334 | self.fake_master = FakeAuthenticatedRedisFactory('secret!') 335 | self.master_listener = reactor.listenTCP(self.master_port, self.fake_master) 336 | 337 | self.fake_sentinel = FakeSentinelFactory() 338 | self.fake_sentinel.master_addr = ("127.0.0.1", self.master_port) 339 | self.fake_sentinel.slave_addrs = [] 340 | self.fake_sentinel.slave_flags = [] 341 | self.sentinel_listener = reactor.listenTCP(self.sentinel_port, self.fake_sentinel) 342 | 343 | self.client = Sentinel([("127.0.0.1", self.sentinel_port)]) 344 | self.client.discovery_timeout = 1 345 | 346 | @defer.inlineCallbacks 347 | def tearDown(self): 348 | yield self.client.disconnect() 349 | self.sentinel_listener.stopListening() 350 | self.master_listener.stopListening() 351 | 352 | 353 | @defer.inlineCallbacks 354 | def test_auth(self): 355 | conn = self.client.master_for("test", password='secret!') 356 | reply = yield conn.role() 357 | self.assertEqual(reply[0], "master") 358 | yield conn.disconnect() 359 | -------------------------------------------------------------------------------- /tests/test_sets.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import random 17 | 18 | from twisted.internet import defer 19 | from twisted.trial import unittest 20 | 21 | import txredisapi as redis 22 | 23 | from tests.mixins import REDIS_HOST, REDIS_PORT 24 | 25 | 26 | class SetsTests(unittest.TestCase): 27 | ''' 28 | Tests to ensure that set returning operations return sets 29 | ''' 30 | _KEYS = ['txredisapi:testsets1', 'txredisapi:testsets2', 31 | 'txredisapi:testsets3', 'txredisapi:testsets4'] 32 | N = 1024 33 | 34 | @defer.inlineCallbacks 35 | def setUp(self): 36 | self.db = yield redis.Connection(REDIS_HOST, REDIS_PORT, 37 | reconnect=False) 38 | 39 | @defer.inlineCallbacks 40 | def tearDown(self): 41 | yield defer.gatherResults([self.db.delete(x) for x in self._KEYS]) 42 | yield self.db.disconnect() 43 | 44 | @defer.inlineCallbacks 45 | def test_saddrem(self): 46 | s = set(range(self.N)) 47 | r = yield self.db.sadd(self._KEYS[0], s) 48 | self.assertEqual(r, len(s)) 49 | a = s.pop() 50 | r = yield self.db.srem(self._KEYS[0], a) 51 | self.assertEqual(r, 1) 52 | l = [s.pop() for x in range(self.N >> 2)] 53 | r = yield self.db.srem(self._KEYS[0], l) 54 | self.assertEqual(r, len(l)) 55 | r = yield self.db.srem(self._KEYS[0], self.N + 1) 56 | self.assertEqual(r, 0) 57 | r = yield self.db.smembers(self._KEYS[0]) 58 | self.assertIsInstance(r, set) 59 | self.assertEqual(r, s) 60 | 61 | @defer.inlineCallbacks 62 | def _test_set(self, key, s): 63 | ''' 64 | Check if the Redis set and the Python set are identical 65 | ''' 66 | r = yield self.db.scard(key) 67 | self.assertEqual(r, len(s)) 68 | r = yield self.db.smembers(key) 69 | self.assertEqual(r, s) 70 | 71 | @defer.inlineCallbacks 72 | def test_sunion(self): 73 | s = set(range(self.N)) 74 | s1 = set() 75 | for x in range(4): 76 | ss = set(s.pop() for x in range(self.N >> 2)) 77 | s1.update(ss) 78 | r = yield self.db.sadd(self._KEYS[x], ss) 79 | self.assertEqual(r, len(ss)) 80 | r = yield self.db.sunion(self._KEYS[:4]) 81 | self.assertIsInstance(r, set) 82 | self.assertEqual(r, s1) 83 | # Test sunionstore 84 | r = yield self.db.sunionstore(self._KEYS[0], self._KEYS[:4]) 85 | self.assertEqual(r, len(s1)) 86 | yield self._test_set(self._KEYS[0], s1) 87 | 88 | @defer.inlineCallbacks 89 | def test_sdiff(self): 90 | l = list(range(self.N)) 91 | random.shuffle(l) 92 | p1 = set(l[:self.N >> 1]) 93 | random.shuffle(l) 94 | p2 = set(l[:self.N >> 1]) 95 | r = yield self.db.sadd(self._KEYS[0], p1) 96 | self.assertEqual(r, len(p1)) 97 | r = yield self.db.sadd(self._KEYS[1], p2) 98 | self.assertEqual(r, len(p2)) 99 | r = yield self.db.sdiff(self._KEYS[:2]) 100 | self.assertIsInstance(r, set) 101 | a = p1 - p2 102 | self.assertEqual(r, a) 103 | # Test sdiffstore 104 | r = yield self.db.sdiffstore(self._KEYS[0], self._KEYS[:2]) 105 | self.assertEqual(r, len(a)) 106 | yield self._test_set(self._KEYS[0], a) 107 | 108 | @defer.inlineCallbacks 109 | def test_sinter(self): 110 | l = list(range(self.N)) 111 | random.shuffle(l) 112 | p1 = set(l[:self.N >> 1]) 113 | random.shuffle(l) 114 | p2 = set(l[:self.N >> 1]) 115 | r = yield self.db.sadd(self._KEYS[0], p1) 116 | self.assertEqual(r, len(p1)) 117 | r = yield self.db.sadd(self._KEYS[1], p2) 118 | self.assertEqual(r, len(p2)) 119 | r = yield self.db.sinter(self._KEYS[:2]) 120 | self.assertIsInstance(r, set) 121 | a = p1.intersection(p2) 122 | self.assertEqual(r, a) 123 | # Test sinterstore 124 | r = yield self.db.sinterstore(self._KEYS[0], self._KEYS[:2]) 125 | self.assertEqual(r, len(a)) 126 | yield self._test_set(self._KEYS[0], a) 127 | 128 | @defer.inlineCallbacks 129 | def test_smembers(self): 130 | s = set(range(self.N)) 131 | r = yield self.db.sadd(self._KEYS[0], s) 132 | self.assertEqual(r, len(s)) 133 | r = yield self.db.smembers(self._KEYS[0]) 134 | self.assertIsInstance(r, set) 135 | self.assertEqual(r, s) 136 | 137 | @defer.inlineCallbacks 138 | def test_sismemember(self): 139 | yield self.db.sadd(self._KEYS[0], 1) 140 | r = yield self.db.sismember(self._KEYS[0], 1) 141 | self.assertIsInstance(r, bool) 142 | self.assertEqual(r, True) 143 | yield self.db.srem(self._KEYS[0], 1) 144 | r = yield self.db.sismember(self._KEYS[0], 1) 145 | self.assertIsInstance(r, bool) 146 | self.assertEqual(r, False) 147 | 148 | @defer.inlineCallbacks 149 | def test_smove(self): 150 | yield self.db.sadd(self._KEYS[0], [1, 2, 3]) 151 | # Test moving an existing element 152 | r = yield self.db.smove(self._KEYS[0], self._KEYS[1], 1) 153 | self.assertIsInstance(r, bool) 154 | self.assertEqual(r, True) 155 | r = yield self.db.smembers(self._KEYS[1]) 156 | self.assertEqual(r, set([1])) 157 | # Test moving an non existing element 158 | r = yield self.db.smove(self._KEYS[0], self._KEYS[1], 4) 159 | self.assertIsInstance(r, bool) 160 | self.assertEqual(r, False) 161 | r = yield self.db.smembers(self._KEYS[1]) 162 | self.assertEqual(r, set([1])) 163 | 164 | @defer.inlineCallbacks 165 | def test_srandmember(self): 166 | l = range(10) 167 | yield self.db.sadd(self._KEYS[0], l) 168 | for i in l: 169 | r = yield self.db.srandmember(self._KEYS[0]) 170 | self.assertIn(r, l) 171 | 172 | @defer.inlineCallbacks 173 | def test_spop(self): 174 | l = range(10) 175 | yield self.db.sadd(self._KEYS[0], l) 176 | popped = set() 177 | for i in l: 178 | r = yield self.db.spop(self._KEYS[0]) 179 | self.assertNotIn(r, popped) 180 | popped.add(r) 181 | self.assertEqual(set(l), popped) 182 | -------------------------------------------------------------------------------- /tests/test_sort.py: -------------------------------------------------------------------------------- 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 16 | from twisted.trial import unittest 17 | 18 | import txredisapi as redis 19 | 20 | from tests.mixins import REDIS_HOST, REDIS_PORT 21 | 22 | 23 | class TestRedisSort(unittest.TestCase): 24 | 25 | @defer.inlineCallbacks 26 | def setUp(self): 27 | self.db = yield redis.Connection(REDIS_HOST, REDIS_PORT, 28 | reconnect=False) 29 | yield self.db.delete('txredisapi:values') 30 | yield self.db.lpush('txredisapi:values', [5, 3, 19, 2, 4, 34, 12]) 31 | 32 | @defer.inlineCallbacks 33 | def tearDown(self): 34 | yield self.db.disconnect() 35 | 36 | @defer.inlineCallbacks 37 | def testSort(self): 38 | r = yield self.db.sort('txredisapi:values') 39 | self.assertEqual([2, 3, 4, 5, 12, 19, 34], r) 40 | 41 | @defer.inlineCallbacks 42 | def testSortWithEndOnly(self): 43 | try: 44 | yield self.db.sort('txredisapi:values', end=3) 45 | except redis.RedisError: 46 | pass 47 | else: 48 | self.fail('RedisError not raised: no start parameter given') 49 | 50 | @defer.inlineCallbacks 51 | def testSortWithStartOnly(self): 52 | try: 53 | yield self.db.sort('txredisapi:values', start=3) 54 | except redis.RedisError: 55 | pass 56 | else: 57 | self.fail('RedisError not raised: no end parameter given') 58 | 59 | @defer.inlineCallbacks 60 | def testSortWithLimits(self): 61 | r = yield self.db.sort('txredisapi:values', start=2, end=4) 62 | self.assertEqual([4, 5, 12, 19], r) 63 | 64 | @defer.inlineCallbacks 65 | def testSortAlpha(self): 66 | yield self.db.delete('txredisapi:alphavals') 67 | yield self.db.lpush('txredisapi:alphavals', ['dog', 'cat', 'apple']) 68 | 69 | r = yield self.db.sort('txredisapi:alphavals', alpha=True) 70 | self.assertEquals(['apple', 'cat', 'dog'], r) 71 | -------------------------------------------------------------------------------- /tests/test_sortedsets.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.internet import defer 17 | from twisted.trial import unittest 18 | from twisted.python.failure import Failure 19 | 20 | import txredisapi as redis 21 | 22 | from tests.mixins import REDIS_HOST, REDIS_PORT 23 | 24 | 25 | class SortedSetsTests(unittest.TestCase): 26 | ''' 27 | Tests for sorted sets 28 | ''' 29 | _KEYS = ['txredisapi:testssets1', 'txredisapi:testssets2', 30 | 'txredisapi:testssets3', 'txredisapi:testssets4'] 31 | 32 | _NUMBERS = ["zero", "one", "two", "three", 33 | "four", "five", "six", "seven", 34 | "eight", "nine"] 35 | 36 | @defer.inlineCallbacks 37 | def test_zaddrem(self): 38 | key = self._getKey() 39 | t = self.assertEqual 40 | r = yield self.db.zadd(key, 1, "one") 41 | t(r, 1) 42 | r = yield self.db.zadd(key, 2, "two") 43 | t(r, 1) 44 | # Try adding multiple items 45 | r = yield self.db.zadd(key, 3, "three", 4, "four", 5, "five") 46 | t(r, 3) 47 | r = yield self.db.zcount(key, '-inf', '+inf') 48 | t(r, 5) 49 | # Try deleting one item 50 | r = yield self.db.zrem(key, "one") 51 | # Try deleting some items 52 | r = yield self.db.zrem(key, "two", "three") 53 | # Test if calling zadd with odd number of arguments errors out 54 | yield self.db.zadd(key, 1, "one", 2).addBoth( 55 | self._check_invaliddata_error, shouldError=True) 56 | # Now try doing it the right way 57 | yield self.db.zadd(key, 1, "one", 2, "two").addBoth( 58 | self._check_invaliddata_error) 59 | 60 | @defer.inlineCallbacks 61 | def test_zcard_zcount(self): 62 | key = self._getKey() 63 | t = self.assertEqual 64 | yield self._make_sorted_set(key) 65 | r = yield self.db.zcard(key) # Check ZCARD 66 | t(r, 10) 67 | r = yield self.db.zcount(key) # ZCOUNT with default args 68 | t(r, 10) 69 | r = yield self.db.zcount(key, 1, 5) # ZCOUNT with args 70 | t(r, 5) 71 | r = yield self.db.zcount(key, '(1', 5) # Exclude arg1 72 | t(r, 4) 73 | r = yield self.db.zcount(key, '(1', '(3') # Exclue arg1 & arg2 74 | t(r, 1) 75 | 76 | @defer.inlineCallbacks 77 | def test_zincrby(self): 78 | key = self._getKey() 79 | t = self.assertEqual 80 | yield self._make_sorted_set(key, 1, 3) 81 | r = yield self.db.zincrby(key, 2, "one") 82 | t(r, 3) 83 | r = yield self.db.zrange(key, withscores=True) 84 | t(r, [('two', 2), ('one', 3)]) 85 | # Also test zincr 86 | r = yield self.db.zincr(key, "one") 87 | t(r, 4) 88 | r = yield self.db.zrange(key, withscores=True) 89 | t(r, [('two', 2), ('one', 4)]) 90 | # And zdecr 91 | r = yield self.db.zdecr(key, "one") 92 | t(r, 3) 93 | r = yield self.db.zrange(key, withscores=True) 94 | t(r, [('two', 2), ('one', 3)]) 95 | 96 | def test_zrange(self): 97 | return self._test_zrange(False) 98 | 99 | def test_zrevrange(self): 100 | return self._test_zrange(True) 101 | 102 | def test_zrank(self): 103 | return self._test_zrank(False) 104 | 105 | def test_zrevrank(self): 106 | return self._test_zrank(True) 107 | 108 | @defer.inlineCallbacks 109 | def test_zscore(self): 110 | key = self._getKey() 111 | r, l = yield self._make_sorted_set(key) 112 | for k, s in l: 113 | r = yield self.db.zscore(key, k) 114 | self.assertEqual(r, s) 115 | r = yield self.db.zscore(key, 'none') 116 | self.assertTrue(r is None) 117 | r = yield self.db.zscore('none', 'one') 118 | self.assertTrue(r is None) 119 | 120 | @defer.inlineCallbacks 121 | def test_zremrangebyrank(self): 122 | key = self._getKey() 123 | t = self.assertEqual 124 | r, l = yield self._make_sorted_set(key) 125 | r = yield self.db.zremrangebyrank(key) 126 | t(r, len(l)) 127 | r = yield self.db.zrange(key) 128 | t(r, []) # Check default args 129 | yield self._make_sorted_set(key, begin=1, end=4) 130 | r = yield self.db.zremrangebyrank(key, 0, 1) 131 | t(r, 2) 132 | r = yield self.db.zrange(key, withscores=True) 133 | t(r, [('three', 3)]) 134 | 135 | @defer.inlineCallbacks 136 | def test_zremrangebyscore(self): 137 | key = self._getKey() 138 | t = self.assertEqual 139 | r, l = yield self._make_sorted_set(key, end=4) 140 | r = yield self.db.zremrangebyscore(key) 141 | t(r, len(l)) 142 | r = yield self.db.zrange(key) 143 | t(r, []) # Check default args 144 | yield self._make_sorted_set(key, begin=1, end=4) 145 | r = yield self.db.zremrangebyscore(key, '-inf', '(2') 146 | t(r, 1) 147 | r = yield self.db.zrange(key, withscores=True) 148 | t(r, [('two', 2), ('three', 3)]) 149 | 150 | def test_zrangebyscore(self): 151 | return self._test_zrangebyscore(False) 152 | 153 | def test_zrevrangebyscore(self): 154 | return self._test_zrangebyscore(True) 155 | 156 | def test_zinterstore(self): 157 | agg_map = { 158 | 'min': (('min', min), { 159 | -1: [('three', -3)], 160 | 0: [('three', 0)], 161 | 1: [('three', 3)], 162 | 2: [('three', 3)], 163 | }), 164 | 'max': (('max', max), { 165 | -1: [('three', 3)], 166 | 0: [('three', 3)], 167 | 1: [('three', 3)], 168 | 2: [('three', 6)], 169 | }), 170 | 'sum': (('sum', sum), { 171 | -1: [('three', 0)], 172 | 0: [('three', 3)], 173 | 1: [('three', 6)], 174 | 2: [('three', 9)], 175 | }) 176 | } 177 | return self._test_zunion_inter_store(agg_map) 178 | 179 | def test_zunionstore(self): 180 | agg_map = { 181 | 'min': (('min', min), { 182 | -1: [('five', -5), ('four', -4), 183 | ('three', -3), ('one', 1), 184 | ('two', 2)], 185 | 0: [('five', 0), ('four', 0), ('three', 0), 186 | ('one', 1), ('two', 2)], 187 | 1: [('one', 1), ('two', 2), ('three', 3), 188 | ('four', 4), ('five', 5)], 189 | 2: [('one', 1), ('two', 2), ('three', 3), 190 | ('four', 8), ('five', 10)] 191 | }), 192 | 'max': (('max', max), { 193 | -1: [('five', -5), ('four', -4), 194 | ('one', 1), ('two', 2), 195 | ('three', 3)], 196 | 0: [('five', 0), ('four', 0), ('one', 1), 197 | ('two', 2), ('three', 3)], 198 | 1: [('one', 1), ('two', 2), ('three', 3), 199 | ('four', 4), ('five', 5)], 200 | 2: [('one', 1), ('two', 2), ('three', 6), 201 | ('four', 8), ('five', 10)] 202 | }), 203 | 'sum': (('sum', sum), { 204 | -1: [('five', -5), ('four', -4), 205 | ('three', 0), ('one', 1), ('two', 2)], 206 | 0: [('five', 0), ('four', 0), ('one', 1), 207 | ('two', 2), ('three', 3)], 208 | 1: [('one', 1), ('two', 2), ('four', 4), 209 | ('five', 5), ('three', 6)], 210 | 2: [('one', 1), ('two', 2), ('four', 8), 211 | ('three', 9), ('five', 10)] 212 | }) 213 | } 214 | return self._test_zunion_inter_store(agg_map, True) 215 | 216 | @defer.inlineCallbacks 217 | def _test_zunion_inter_store(self, agg_function_map, union=False): 218 | if union: 219 | cmd = self.db.zunionstore 220 | else: 221 | cmd = self.db.zinterstore 222 | key = self._getKey() 223 | t = self.assertEqual 224 | key1 = self._getKey(1) 225 | destKey = self._getKey(2) 226 | r, l = yield self._make_sorted_set(key, begin=1, end=4) 227 | r1, l1 = yield self._make_sorted_set(key1, begin=3, end=6) 228 | for agg_fn_name in agg_function_map: 229 | for agg_fn in agg_function_map[agg_fn_name][0]: 230 | for key1_weight in range(-1, 3): 231 | if key1_weight == 1: 232 | keys = [key, key1] 233 | else: 234 | keys = {key: 1, key1: key1_weight} 235 | r = yield cmd(destKey, keys, aggregate=agg_fn) 236 | if union: 237 | t(r, len(set(l + l1))) 238 | else: 239 | t(r, len(set(l) & set(l1))) 240 | r = yield self.db.zrange(destKey, withscores=True) 241 | t(r, agg_function_map[agg_fn_name][1][key1_weight]) 242 | yield self.db.delete(destKey) 243 | # Finally, test for invalid aggregate functions 244 | yield self.db.delete(key, key1) 245 | yield self._make_sorted_set(key, begin=1, end=4) 246 | yield self._make_sorted_set(key1, begin=3, end=6) 247 | yield cmd(destKey, [key, key1], aggregate='SIN').addBoth( 248 | self._check_invaliddata_error, shouldError=True) 249 | yield cmd(destKey, [key, key1], aggregate=lambda a, b: a + b).addBoth( 250 | self._check_invaliddata_error, shouldError=True) 251 | yield self.db.delete(destKey) 252 | 253 | @defer.inlineCallbacks 254 | def _test_zrangebyscore(self, reverse): 255 | key = self._getKey() 256 | t = self.assertEqual 257 | if reverse: 258 | command = self.db.zrevrangebyscore 259 | else: 260 | command = self.db.zrangebyscore 261 | for ws in [True, False]: 262 | r, l = yield self._make_sorted_set(key, begin=1, end=4) 263 | if reverse: 264 | l.reverse() 265 | r = yield command(key, withscores=ws) 266 | if ws: 267 | t(r, l) 268 | else: 269 | t(r, [x[0] for x in l]) 270 | r = yield command(key, withscores=ws, offset=1, count=1) 271 | if ws: 272 | t(r, [('two', 2)]) 273 | else: 274 | t(r, ['two']) 275 | yield self.db.delete(key) 276 | # Test for invalid offset and count 277 | yield self._make_sorted_set(key, begin=1, end=4) 278 | yield command(key, offset=1).addBoth( 279 | self._check_invaliddata_error, shouldError=True) 280 | yield command(key, count=1).addBoth( 281 | self._check_invaliddata_error, shouldError=True) 282 | 283 | @defer.inlineCallbacks 284 | def _test_zrank(self, reverse): 285 | key = self._getKey() 286 | r, l = yield self._make_sorted_set(key) 287 | if reverse: 288 | command = self.db.zrevrank 289 | l.reverse() 290 | else: 291 | command = self.db.zrank 292 | for k, s in l: 293 | r = yield command(key, k) 294 | self.assertEqual(l[r][0], k) 295 | r = yield command(key, 'none') # non-existant member 296 | self.assertTrue(r is None) 297 | r = yield command('none', 'one') 298 | self.assertTrue(r is None) 299 | 300 | @defer.inlineCallbacks 301 | def _test_zrange(self, reverse): 302 | key = self._getKey() 303 | t = self.assertEqual 304 | r, l = yield self._make_sorted_set(key) 305 | if reverse: 306 | command = self.db.zrevrange 307 | l.reverse() 308 | else: 309 | command = self.db.zrange 310 | r = yield command(key) 311 | t(r, [x[0] for x in l]) 312 | r = yield command(key, withscores=True) 313 | # Ensure that WITHSCORES returns tuples 314 | t(r, l) 315 | # Test with args 316 | r = yield command(key, start='5', end='8', withscores=True) 317 | t(r, l[5:9]) 318 | # Test to ensure empty results return empty lists 319 | r = yield command(key, start=-20, end=-40, withscores=True) 320 | t(r, []) 321 | 322 | def _getKey(self, n=0): 323 | return self._KEYS[n] 324 | 325 | def _to_words(self, n): 326 | l = [] 327 | while True: 328 | n, r = divmod(n, 10) 329 | l.append(self._NUMBERS[r]) 330 | if n == 0: 331 | break 332 | return ' '.join(l) 333 | 334 | def _sorted_set_check(self, r, l): 335 | self.assertEqual(r, len(l)) 336 | return r, l 337 | 338 | def _make_sorted_set(self, key, begin=0, end=10): 339 | l = [] 340 | for x in range(begin, end): 341 | l.extend((x, self._to_words(x))) 342 | return self.db.zadd(key, *l).addCallback( 343 | self._sorted_set_check, list(zip(l[1::2], l[::2]))) 344 | 345 | @defer.inlineCallbacks 346 | def setUp(self): 347 | self.db = yield redis.Connection(REDIS_HOST, REDIS_PORT, 348 | reconnect=False) 349 | 350 | def tearDown(self): 351 | return defer.gatherResults( 352 | [self.db.delete(x) for x in self._KEYS]).addCallback( 353 | lambda ign: self.db.disconnect()) 354 | 355 | def _check_invaliddata_error(self, response, shouldError=False): 356 | if shouldError: 357 | self.assertIsInstance(response, Failure) 358 | self.assertIsInstance(response.value, redis.InvalidData) 359 | else: 360 | self.assertNotIsInstance(response, Failure) 361 | -------------------------------------------------------------------------------- /tests/test_subscriber.py: -------------------------------------------------------------------------------- 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 six 16 | 17 | from twisted.internet import defer, reactor 18 | from twisted.trial import unittest 19 | 20 | import txredisapi as redis 21 | 22 | from tests.mixins import REDIS_HOST, REDIS_PORT 23 | 24 | 25 | class TestSubscriberProtocol(unittest.TestCase): 26 | @defer.inlineCallbacks 27 | def setUp(self): 28 | factory = redis.SubscriberFactory() 29 | factory.continueTrying = False 30 | reactor.connectTCP(REDIS_HOST, REDIS_PORT, factory) 31 | self.db = yield factory.deferred 32 | 33 | @defer.inlineCallbacks 34 | def tearDown(self): 35 | yield self.db.disconnect() 36 | 37 | @defer.inlineCallbacks 38 | def testDisconnectErrors(self): 39 | # Slightly dirty, but we want a reference to the actual 40 | # protocol instance 41 | conn = yield self.db._factory.getConnection(True) 42 | 43 | # This should return a deferred from the replyQueue; then 44 | # loseConnection will make it do an errback with a 45 | # ConnectionError instance 46 | d = self.db.subscribe('foo') 47 | 48 | conn.transport.loseConnection() 49 | try: 50 | yield d 51 | self.fail() 52 | except redis.ConnectionError: 53 | pass 54 | 55 | # This should immediately errback with a ConnectionError 56 | # instance when getConnection finds 0 active instances in the 57 | # factory 58 | try: 59 | yield self.db.subscribe('bar') 60 | self.fail() 61 | except redis.ConnectionError: 62 | pass 63 | 64 | # This should immediately raise a ConnectionError instance 65 | # when execute_command() finds that the connection is not 66 | # connected 67 | try: 68 | yield conn.subscribe('baz') 69 | self.fail() 70 | except redis.ConnectionError: 71 | pass 72 | 73 | @defer.inlineCallbacks 74 | def testSubscribe(self): 75 | reply = yield self.db.subscribe("test_subscribe1") 76 | self.assertEqual(reply, ["subscribe", "test_subscribe1", 1]) 77 | 78 | reply = yield self.db.subscribe("test_subscribe2") 79 | self.assertEqual(reply, ["subscribe", 80 | "test_subscribe2", 2]) 81 | 82 | @defer.inlineCallbacks 83 | def testUnsubscribe(self): 84 | yield self.db.subscribe("test_unsubscribe1") 85 | yield self.db.subscribe("test_unsubscribe2") 86 | 87 | reply = yield self.db.unsubscribe("test_unsubscribe1") 88 | self.assertEqual(reply, ["unsubscribe", 89 | "test_unsubscribe1", 1]) 90 | reply = yield self.db.unsubscribe("test_unsubscribe2") 91 | self.assertEqual(reply, ["unsubscribe", 92 | "test_unsubscribe2", 0]) 93 | 94 | @defer.inlineCallbacks 95 | def testPSubscribe(self): 96 | reply = yield self.db.psubscribe("test_psubscribe1.*") 97 | self.assertEqual(reply, ["psubscribe", 98 | "test_psubscribe1.*", 1]) 99 | 100 | reply = yield self.db.psubscribe("test_psubscribe2.*") 101 | self.assertEqual(reply, ["psubscribe", 102 | "test_psubscribe2.*", 2]) 103 | 104 | @defer.inlineCallbacks 105 | def testPUnsubscribe(self): 106 | yield self.db.psubscribe("test_punsubscribe1.*") 107 | yield self.db.psubscribe("test_punsubscribe2.*") 108 | 109 | reply = yield self.db.punsubscribe("test_punsubscribe1.*") 110 | self.assertEqual(reply, ["punsubscribe", 111 | "test_punsubscribe1.*", 1]) 112 | reply = yield self.db.punsubscribe("test_punsubscribe2.*") 113 | self.assertEqual(reply, ["punsubscribe", 114 | "test_punsubscribe2.*", 0]) 115 | 116 | 117 | class TestAuthenticatedSubscriberProtocol(unittest.TestCase): 118 | timeout = 5 119 | 120 | @defer.inlineCallbacks 121 | def setUp(self): 122 | meta = yield redis.Connection(REDIS_HOST, REDIS_PORT) 123 | yield meta.execute_command("config", "set", "requirepass", "password") 124 | yield meta.disconnect() 125 | self.addCleanup(self.removePassword) 126 | 127 | factory = redis.RedisFactory(None, dbid=0, poolsize=1, 128 | password="password") 129 | factory.protocol = redis.SubscriberProtocol 130 | factory.continueTrying = False 131 | reactor.connectTCP(REDIS_HOST, REDIS_PORT, factory) 132 | self.db = yield factory.deferred 133 | 134 | @defer.inlineCallbacks 135 | def removePassword(self): 136 | meta = yield redis.Connection(REDIS_HOST, REDIS_PORT, 137 | password="password") 138 | yield meta.execute_command("config", "set", "requirepass", "") 139 | yield meta.disconnect() 140 | 141 | @defer.inlineCallbacks 142 | def tearDown(self): 143 | yield self.db.disconnect() 144 | 145 | @defer.inlineCallbacks 146 | def testSubscribe(self): 147 | reply = yield self.db.subscribe("test_subscribe1") 148 | self.assertEqual(reply, [u"subscribe", u"test_subscribe1", 1]) 149 | 150 | reply = yield self.db.subscribe("test_subscribe2") 151 | self.assertEqual(reply, [u"subscribe", u"test_subscribe2", 2]) 152 | -------------------------------------------------------------------------------- /tests/test_transactions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | 18 | from twisted.trial import unittest 19 | from twisted.internet import defer 20 | from twisted.python import log 21 | 22 | import txredisapi 23 | 24 | from tests.mixins import REDIS_HOST, REDIS_PORT 25 | 26 | # log.startLogging(sys.stdout) 27 | 28 | 29 | class TestRedisConnections(unittest.TestCase): 30 | @defer.inlineCallbacks 31 | def testRedisConnection(self): 32 | rapi = yield txredisapi.Connection(REDIS_HOST, REDIS_PORT) 33 | 34 | # test set() operation 35 | transaction = yield rapi.multi("txredisapi:test_transaction") 36 | self.assertTrue(transaction.inTransaction) 37 | for key, value in (("txredisapi:test_transaction", "foo"), 38 | ("txredisapi:test_transaction", "bar")): 39 | yield transaction.set(key, value) 40 | yield transaction.commit() 41 | self.assertFalse(transaction.inTransaction) 42 | result = yield rapi.get("txredisapi:test_transaction") 43 | self.assertEqual(result, "bar") 44 | 45 | yield rapi.disconnect() 46 | 47 | @defer.inlineCallbacks 48 | def testRedisWithOnlyWatchUnwatch(self): 49 | rapi = yield txredisapi.Connection(REDIS_HOST, REDIS_PORT) 50 | 51 | k = "txredisapi:testRedisWithOnlyWatchAndUnwatch" 52 | tx = yield rapi.watch(k) 53 | self.assertTrue(tx.inTransaction) 54 | yield tx.set(k, "bar") 55 | v = yield tx.get(k) 56 | self.assertEqual("bar", v) 57 | yield tx.unwatch() 58 | self.assertFalse(tx.inTransaction) 59 | 60 | yield rapi.disconnect() 61 | 62 | @defer.inlineCallbacks 63 | def testRedisWithWatchAndMulti(self): 64 | rapi = yield txredisapi.Connection(REDIS_HOST, REDIS_PORT) 65 | 66 | tx = yield rapi.watch("txredisapi:testRedisWithWatchAndMulti") 67 | yield tx.multi() 68 | yield tx.unwatch() 69 | self.assertTrue(tx.inTransaction) 70 | yield tx.commit() 71 | self.assertFalse(tx.inTransaction) 72 | 73 | yield rapi.disconnect() 74 | 75 | # some sort of probabilistic test 76 | @defer.inlineCallbacks 77 | def testWatchAndPools_1(self): 78 | rapi = yield txredisapi.ConnectionPool(REDIS_HOST, REDIS_PORT, 79 | poolsize=2, reconnect=False) 80 | tx1 = yield rapi.watch("foobar") 81 | tx2 = yield tx1.watch("foobaz") 82 | self.assertTrue(id(tx1) == id(tx2)) 83 | yield rapi.disconnect() 84 | 85 | # some sort of probabilistic test 86 | @defer.inlineCallbacks 87 | def testWatchAndPools_2(self): 88 | rapi = yield txredisapi.ConnectionPool(REDIS_HOST, REDIS_PORT, 89 | poolsize=2, reconnect=False) 90 | tx1 = yield rapi.watch("foobar") 91 | tx2 = yield rapi.watch("foobaz") 92 | self.assertTrue(id(tx1) != id(tx2)) 93 | yield rapi.disconnect() 94 | 95 | @defer.inlineCallbacks 96 | def testWatchEdgeCase_1(self): 97 | rapi = yield txredisapi.Connection(REDIS_HOST, REDIS_PORT) 98 | 99 | tx = yield rapi.multi("foobar") 100 | yield tx.unwatch() 101 | self.assertTrue(tx.inTransaction) 102 | yield tx.discard() 103 | self.assertFalse(tx.inTransaction) 104 | 105 | yield rapi.disconnect() 106 | 107 | @defer.inlineCallbacks 108 | def testWatchEdgeCase_2(self): 109 | rapi = yield txredisapi.Connection(REDIS_HOST, REDIS_PORT) 110 | 111 | tx = yield rapi.multi() 112 | try: 113 | yield tx.watch("foobar") 114 | except txredisapi.ResponseError: 115 | pass 116 | yield tx.unwatch() 117 | self.assertTrue(tx.inTransaction) 118 | yield tx.discard() 119 | self.assertFalse(tx.inTransaction) 120 | yield rapi.disconnect() 121 | 122 | @defer.inlineCallbacks 123 | def testWatchEdgeCase_3(self): 124 | rapi = yield txredisapi.Connection(REDIS_HOST, REDIS_PORT) 125 | 126 | tx = yield rapi.watch("foobar") 127 | tx = yield tx.multi("foobaz") 128 | yield tx.unwatch() 129 | self.assertTrue(tx.inTransaction) 130 | yield tx.discard() 131 | self.assertFalse(tx.inTransaction) 132 | 133 | yield rapi.disconnect() 134 | -------------------------------------------------------------------------------- /tests/test_unix_connection.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | 18 | import txredisapi as redis 19 | 20 | from twisted.internet import base 21 | from twisted.internet import defer 22 | from twisted.trial import unittest 23 | 24 | base.DelayedCall.debug = False 25 | redis_sock = "/tmp/redis.sock" 26 | 27 | 28 | class TestUnixConnectionMethods(unittest.TestCase): 29 | @defer.inlineCallbacks 30 | def test_UnixConnection(self): 31 | db = yield redis.UnixConnection(redis_sock, reconnect=False) 32 | self.assertEqual(isinstance(db, redis.UnixConnectionHandler), True) 33 | yield db.disconnect() 34 | 35 | @defer.inlineCallbacks 36 | def test_UnixConnectionDB1(self): 37 | db = yield redis.UnixConnection(redis_sock, dbid=1, reconnect=False) 38 | self.assertEqual(isinstance(db, redis.UnixConnectionHandler), True) 39 | yield db.disconnect() 40 | 41 | @defer.inlineCallbacks 42 | def test_UnixConnectionPool(self): 43 | db = yield redis.UnixConnectionPool(redis_sock, poolsize=2, 44 | reconnect=False) 45 | self.assertEqual(isinstance(db, redis.UnixConnectionHandler), True) 46 | yield db.disconnect() 47 | 48 | @defer.inlineCallbacks 49 | def test_lazyUnixConnection(self): 50 | db = redis.lazyUnixConnection(redis_sock, reconnect=False) 51 | self.assertEqual(isinstance(db._connected, defer.Deferred), True) 52 | db = yield db._connected 53 | self.assertEqual(isinstance(db, redis.UnixConnectionHandler), True) 54 | yield db.disconnect() 55 | 56 | @defer.inlineCallbacks 57 | def test_lazyUnixConnectionPool(self): 58 | db = redis.lazyUnixConnectionPool(redis_sock, reconnect=False) 59 | self.assertEqual(isinstance(db._connected, defer.Deferred), True) 60 | db = yield db._connected 61 | self.assertEqual(isinstance(db, redis.UnixConnectionHandler), True) 62 | yield db.disconnect() 63 | 64 | @defer.inlineCallbacks 65 | def test_ShardedUnixConnection(self): 66 | paths = [redis_sock] 67 | db = yield redis.ShardedUnixConnection(paths, reconnect=False) 68 | self.assertEqual(isinstance(db, 69 | redis.ShardedUnixConnectionHandler), True) 70 | yield db.disconnect() 71 | 72 | @defer.inlineCallbacks 73 | def test_ShardedUnixConnectionPool(self): 74 | paths = [redis_sock] 75 | db = yield redis.ShardedUnixConnectionPool(paths, reconnect=False) 76 | self.assertEqual(isinstance(db, 77 | redis.ShardedUnixConnectionHandler), True) 78 | yield db.disconnect() 79 | 80 | if not os.path.exists(redis_sock): 81 | TestUnixConnectionMethods.skip = True 82 | -------------------------------------------------------------------------------- /tests/test_watch.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright 2009 Alexandre Fiori 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from twisted.internet import defer 17 | from twisted.python.failure import Failure 18 | from twisted.trial import unittest 19 | 20 | import txredisapi as redis 21 | 22 | from tests.mixins import REDIS_HOST, REDIS_PORT 23 | 24 | 25 | class TestRedisConnections(unittest.TestCase): 26 | _KEYS = ['txredisapi:testwatch1', 'txredisapi:testwatch2'] 27 | 28 | @defer.inlineCallbacks 29 | def setUp(self): 30 | self.connections = [] 31 | self.db = yield self._getRedisConnection() 32 | yield self.db.delete(self._KEYS) 33 | 34 | @defer.inlineCallbacks 35 | def tearDown(self): 36 | for connection in self.connections: 37 | l = [connection.delete(k) for k in self._KEYS] 38 | yield defer.DeferredList(l) 39 | yield connection.disconnect() 40 | 41 | def _db_connected(self, connection): 42 | self.connections.append(connection) 43 | return connection 44 | 45 | def _getRedisConnection(self, host=REDIS_HOST, port=REDIS_PORT, db=0): 46 | return redis.Connection( 47 | host, port, dbid=db, reconnect=False).addCallback( 48 | self._db_connected) 49 | 50 | def _check_watcherror(self, response, shouldError=False): 51 | if shouldError: 52 | self.assertIsInstance(response, Failure) 53 | self.assertIsInstance(response.value, redis.WatchError) 54 | else: 55 | self.assertNotIsInstance(response, Failure) 56 | 57 | @defer.inlineCallbacks 58 | def testRedisWatchFail(self): 59 | db1 = yield self._getRedisConnection() 60 | yield self.db.set(self._KEYS[0], 'foo') 61 | t = yield self.db.multi(self._KEYS[0]) 62 | self.assertIsInstance(t, redis.RedisProtocol) 63 | yield t.set(self._KEYS[1], 'bar') 64 | # This should trigger a failure 65 | yield db1.set(self._KEYS[0], 'bar1') 66 | yield t.commit().addBoth(self._check_watcherror, shouldError=True) 67 | 68 | @defer.inlineCallbacks 69 | def testRedisWatchSucceed(self): 70 | yield self.db.set(self._KEYS[0], 'foo') 71 | t = yield self.db.multi(self._KEYS[0]) 72 | self.assertIsInstance(t, redis.RedisProtocol) 73 | yield t.set(self._KEYS[0], 'bar') 74 | yield t.commit().addBoth(self._check_watcherror, shouldError=False) 75 | 76 | @defer.inlineCallbacks 77 | def testRedisMultiNoArgs(self): 78 | yield self.db.set(self._KEYS[0], 'foo') 79 | t = yield self.db.multi() 80 | self.assertIsInstance(t, redis.RedisProtocol) 81 | yield t.set(self._KEYS[1], 'bar') 82 | yield t.commit().addBoth(self._check_watcherror, shouldError=False) 83 | 84 | @defer.inlineCallbacks 85 | def testRedisWithBulkCommands_transactions(self): 86 | t = yield self.db.watch(self._KEYS) 87 | yield t.mget(self._KEYS) 88 | t = yield t.multi() 89 | yield t.commit() 90 | self.assertEqual(0, t.transactions) 91 | self.assertFalse(t.inTransaction) 92 | 93 | @defer.inlineCallbacks 94 | def testRedisWithBulkCommands_inTransaction(self): 95 | t = yield self.db.watch(self._KEYS) 96 | yield t.mget(self._KEYS) 97 | self.assertTrue(t.inTransaction) 98 | yield t.unwatch() 99 | 100 | @defer.inlineCallbacks 101 | def testRedisWithBulkCommands_mget(self): 102 | yield self.db.set(self._KEYS[0], "foo") 103 | yield self.db.set(self._KEYS[1], "bar") 104 | 105 | m0 = yield self.db.mget(self._KEYS) 106 | t = yield self.db.watch(self._KEYS) 107 | m1 = yield t.mget(self._KEYS) 108 | t = yield t.multi() 109 | yield t.mget(self._KEYS) 110 | (m2,) = yield t.commit() 111 | 112 | self.assertEqual(["foo", "bar"], m0) 113 | self.assertEqual(m0, m1) 114 | self.assertEqual(m0, m2) 115 | 116 | @defer.inlineCallbacks 117 | def testRedisWithBulkCommands_hgetall(self): 118 | yield self.db.hset(self._KEYS[0], "foo", "bar") 119 | yield self.db.hset(self._KEYS[0], "bar", "foo") 120 | 121 | h0 = yield self.db.hgetall(self._KEYS[0]) 122 | t = yield self.db.watch(self._KEYS[0]) 123 | h1 = yield t.hgetall(self._KEYS[0]) 124 | t = yield t.multi() 125 | yield t.hgetall(self._KEYS[0]) 126 | (h2,) = yield t.commit() 127 | 128 | self.assertEqual({"foo": "bar", 129 | "bar": "foo"}, h0) 130 | self.assertEqual(h0, h1) 131 | self.assertEqual(h0, h2) 132 | 133 | @defer.inlineCallbacks 134 | def testRedisWithAsyncCommandsDuringWatch(self): 135 | yield self.db.hset(self._KEYS[0], "foo", "bar") 136 | yield self.db.hset(self._KEYS[0], "bar", "foo") 137 | 138 | h0 = yield self.db.hgetall(self._KEYS[0]) 139 | t = yield self.db.watch(self._KEYS[0]) 140 | (h1, h2) = yield defer.gatherResults([ 141 | t.hgetall(self._KEYS[0]), 142 | t.hgetall(self._KEYS[0]), 143 | ], consumeErrors=True) 144 | yield t.unwatch() 145 | 146 | self.assertEqual({"foo": "bar", 147 | "bar": "foo"}, h0) 148 | self.assertEqual(h0, h1) 149 | self.assertEqual(h0, h2) 150 | --------------------------------------------------------------------------------