├── .coveragerc ├── .gitignore ├── README.rst ├── demos ├── chat.html └── chat.py ├── gredis ├── __init__.py ├── client.py └── connection.py ├── requirements.txt ├── setup.py ├── tests ├── __init__.py └── test_gredis.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | 4 | include = gredis/* 5 | timid = true 6 | 7 | [report] 8 | fail_under = 90 9 | show_missing = true 10 | skip_covered = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.so 4 | *.swp 5 | .idea/ 6 | build/ 7 | /dist/ 8 | MANIFEST 9 | .tox/ 10 | .vagrant 11 | /.coverage 12 | gredis.egg-info/ 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | gredis 2 | ====== 3 | gRedis is an asynchronous client library of Redis written with `Tornado `_ coroutine. 4 | 5 | gRedis is standing on the shoulders of giants: 6 | 7 | * `redis-py `_ 8 | * `Tornado `_ 9 | 10 | Installation 11 | ============ 12 | 13 | .. code-block:: shell 14 | 15 | pip install gredis 16 | 17 | OR 18 | 19 | .. code-block:: shell 20 | 21 | easy_install gredis 22 | 23 | 24 | OR 25 | 26 | .. code-block:: shell 27 | 28 | git clone https://github.com/coldnight/gredis 29 | cd gredis 30 | python setup.py install 31 | 32 | Usage 33 | ===== 34 | 35 | .. code-block:: python 36 | 37 | from tornado import gen 38 | from tornado import web 39 | from gredis.client import AsyncRedis 40 | 41 | client = AsyncRedis("ip.or.host", 6379) 42 | 43 | class DemoHandler(web.RequestHandler): 44 | 45 | @gen.coroutine 46 | def get(self): 47 | ret = yield client.incr("key") 48 | redis = client.to_blocking_client() 49 | ret2 = redis.incr("key") 50 | self.write(str(ret + ret2)) 51 | 52 | Pub/Sub 53 | ------- 54 | .. code-block:: python 55 | 56 | from tornado import gen 57 | from tornado import web 58 | from gredis.client import AsyncRedis 59 | 60 | client = AsyncRedis("ip.or.host", 6379) 61 | 62 | class PubSubHandler(web.RequestHandler): 63 | 64 | @gen.coroutine 65 | def get(self): 66 | pubsub = client.pubsub() 67 | channel = "test" 68 | yield pubsub.subscribe(channel) 69 | response = yield pubsub.get_message(True) 70 | assert response["type"] == "subscribe" 71 | response = yield pubsub.get_message(True) 72 | assert response["type"] == "message" 73 | self.write(response['data']) 74 | 75 | @gen.coroutine 76 | def post(self): 77 | yield client.publish(channel, "test") 78 | 79 | 80 | Not Implementation 81 | ================== 82 | 83 | * pipeline 84 | -------------------------------------------------------------------------------- /demos/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chat 4 | 5 | 6 | 51 | 52 | Send {{url}} to someone and begin chat. 53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /demos/chat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 15/04/18 13:21:25 7 | # Desc : A Chat Demo written with gredis 8 | # 9 | """ A chat demo based on list type of Redis, written with gredis """ 10 | from __future__ import absolute_import, print_function, division, with_statement 11 | 12 | import uuid 13 | 14 | from tornado import gen 15 | from tornado import web 16 | from tornado import ioloop 17 | from gredis.client import AsyncRedis 18 | 19 | client = AsyncRedis("192.168.1.50") 20 | 21 | 22 | CHAT_PEER_KEY = "chat::peer" 23 | 24 | 25 | class ChatHandler(web.RequestHandler): 26 | @gen.coroutine 27 | def get(self, chat_id=None): 28 | if not chat_id: 29 | chat_id = uuid.uuid1().get_hex() 30 | dist_id = uuid.uuid1().get_hex() 31 | yield client.hmset(CHAT_PEER_KEY, {chat_id: dist_id}) 32 | yield client.hmset(CHAT_PEER_KEY, {dist_id: chat_id}) 33 | self.redirect("/chat/{0}".format(chat_id)) 34 | else: 35 | dist_id = yield client.hget(CHAT_PEER_KEY, chat_id) 36 | 37 | self.render("chat.html", 38 | url="{0}://{1}/chat/{2}".format(self.request.protocol, 39 | self.request.host, dist_id), 40 | chat_id=chat_id) 41 | 42 | @gen.coroutine 43 | def post(self, chat_id): 44 | message = self.get_argument("message") 45 | recv_key = "chat::{0}::message".format(chat_id) 46 | yield client.rpush(recv_key, message) 47 | self.write("success") 48 | 49 | 50 | class ChatMessageHandler(web.RequestHandler): 51 | @gen.coroutine 52 | def post(self): 53 | chat_id = self.get_argument("chat_id") 54 | dist_id = yield client.hget(CHAT_PEER_KEY, chat_id) 55 | 56 | send_key = "chat::{0}::message".format(dist_id) 57 | 58 | key, message = yield client.blpop(send_key) 59 | 60 | self.write(message) 61 | 62 | 63 | # from tornado import gen 64 | # from tornado import web 65 | # from gredis.client import AsyncRedis 66 | 67 | # client = AsyncRedis("ip.or.host", 6379) 68 | 69 | class DemoHandler(web.RequestHandler): 70 | 71 | @gen.coroutine 72 | def get(self): 73 | ret = yield client.incr("key") 74 | redis = client.to_blocking_client() 75 | ret2 = redis.incr("key") 76 | self.write(str(ret + ret2)) 77 | 78 | 79 | 80 | if __name__ == "__main__": 81 | app = web.Application([ 82 | (r'/chat/message', ChatMessageHandler), 83 | (r'/chat/?(.*)', ChatHandler), 84 | (r'/', DemoHandler), 85 | ], debug=True) 86 | 87 | app.listen(8888) 88 | ioloop.IOLoop.instance().start() 89 | -------------------------------------------------------------------------------- /gredis/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 15/04/18 16:47:54 7 | # Desc : 8 | # 9 | from __future__ import absolute_import, print_function, division, with_statement 10 | 11 | version = "0.0.9" 12 | -------------------------------------------------------------------------------- /gredis/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 15/04/15 17:16:34 7 | # Desc : Asynchronous 8 | # 9 | from __future__ import absolute_import, print_function, division, with_statement 10 | 11 | from tornado import gen 12 | 13 | from redis.client import StrictRedis, Redis, PubSub 14 | from redis.exceptions import ConnectionError, TimeoutError 15 | from redis.connection import Connection, ConnectionPool 16 | 17 | from gredis.connection import AsyncConnection 18 | 19 | 20 | def _construct_connection_pool(pool): 21 | """ Construct a blocking socket connection pool based on asynchronous pool 22 | """ 23 | _pool = ConnectionPool(Connection, pool.max_connections, **pool.connection_kwargs) 24 | 25 | return _pool 26 | 27 | 28 | class AsyncStrictRedis(StrictRedis): 29 | 30 | def __init__(self, *args, **kwargs): 31 | StrictRedis.__init__(self, *args, **kwargs) 32 | self.connection_pool.connection_class = AsyncConnection 33 | 34 | # COMMAND EXECUTION AND PROTOCOL PARSING 35 | @gen.coroutine 36 | def execute_command(self, *args, **options): 37 | "Execute a command and return a parsed response" 38 | pool = self.connection_pool 39 | command_name = args[0] 40 | connection = pool.get_connection(command_name, **options) 41 | try: 42 | yield connection.send_command(*args) 43 | result = yield self.parse_response(connection, command_name, **options) 44 | raise gen.Return(result) 45 | except (ConnectionError, TimeoutError) as e: 46 | connection.disconnect() 47 | if not connection.retry_on_timeout and isinstance(e, TimeoutError): 48 | raise 49 | connection.send_command(*args) 50 | result = yield self.parse_response(connection, command_name, **options) 51 | raise gen.Return(result) 52 | finally: 53 | pool.release(connection) 54 | 55 | @gen.coroutine 56 | def parse_response(self, connection, command_name, **options): 57 | response = yield connection.read_response() 58 | if command_name in self.response_callbacks: 59 | raise gen.Return(self.response_callbacks[command_name](response, **options)) 60 | raise gen.Return(response) 61 | 62 | def pipeline(self, *args, **kwargs): 63 | obj = self.to_blocking_client() 64 | return obj.pipeline(*args, **kwargs) 65 | 66 | def to_blocking_client(self): 67 | """ Convert asynchronous client to blocking socket client 68 | """ 69 | obj = StrictRedis() 70 | obj.connection_pool = _construct_connection_pool(self.connection_pool) 71 | return obj 72 | 73 | 74 | class AsyncRedis(AsyncStrictRedis): 75 | def pubsub(self, **kwargs): 76 | return AsyncPubSub(self.connection_pool, **kwargs) 77 | 78 | def to_blocking_client(self): 79 | """ Convert asynchronous client to blocking socket client 80 | """ 81 | obj = Redis() 82 | obj.connection_pool = _construct_connection_pool(self.connection_pool) 83 | return obj 84 | 85 | 86 | class AsyncPubSub(PubSub): 87 | def execute_command(self, *args, **kwargs): 88 | "Execute a publish/subscribe command" 89 | 90 | # NOTE: don't parse the response in this function. it could pull a 91 | # legitmate message off the stack if the connection is already 92 | # subscribed to one or more channels 93 | 94 | if self.connection is None: 95 | self.connection = self.connection_pool.get_connection( 96 | 'pubsub', 97 | self.shard_hint 98 | ) 99 | # register a callback that re-subscribes to any channels we 100 | # were listening to when we were disconnected 101 | self.connection.register_connect_callback(self.on_connect) 102 | connection = self.connection 103 | return self._execute(connection, connection.send_command, *args) 104 | 105 | def listen(self): 106 | "Listen for messages on channels this client has been subscribed to" 107 | raise NotImplementedError 108 | 109 | @gen.coroutine 110 | def get_message(self, block=False, ignore_subscribe_messages=False): 111 | """ 112 | Get the next message if one is available, otherwise None. 113 | 114 | If timeout is specified, the system will wait for `timeout` seconds 115 | before returning. Timeout should be specified as a floating point 116 | number. 117 | """ 118 | response = yield self.parse_response(block) 119 | if response: 120 | raise gen.Return(self.handle_message(response, ignore_subscribe_messages)) 121 | raise gen.Return(None) 122 | -------------------------------------------------------------------------------- /gredis/connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """Inheritance from :class:`redis.connection.Connection` and make it 4 | asynchronous. 5 | """ 6 | from __future__ import unicode_literals, print_function, division 7 | 8 | import sys 9 | import socket 10 | import weakref 11 | 12 | from select import select 13 | 14 | from tornado import gen 15 | from tornado.tcpclient import TCPClient 16 | from tornado.iostream import StreamClosedError 17 | from redis.connection import ( 18 | Connection, ConnectionError, PythonParser, SocketBuffer, 19 | SERVER_CLOSED_CONNECTION_ERROR, TimeoutError, SYM_CRLF, DefaultParser) 20 | from redis._compat import byte_to_chr, nativestr 21 | from redis.exceptions import ( 22 | InvalidResponse, RedisError, AuthenticationError, 23 | ResponseError) 24 | 25 | 26 | class StreamBuffer(SocketBuffer): 27 | def __init__(self, stream, socket_read_size): 28 | super(StreamBuffer, self).__init__(None, socket_read_size) 29 | self._stream = stream 30 | 31 | @gen.coroutine 32 | def _read_from_stream(self, length=None): 33 | buf = self._buffer 34 | buf.seek(self.bytes_written) 35 | marker = 0 36 | 37 | try: 38 | while True: 39 | try: 40 | data = yield self._stream.read_until_regex(b'\r\n') 41 | except StreamClosedError: 42 | raise socket.error(SERVER_CLOSED_CONNECTION_ERROR) 43 | 44 | buf.write(data) 45 | 46 | data_length = len(data) 47 | self.bytes_written += data_length 48 | marker += data_length 49 | 50 | if length is not None and length > marker: 51 | continue 52 | break 53 | except socket.timeout: 54 | raise TimeoutError("Timeout reading from stream") 55 | except socket.error: 56 | e = sys.exc_info()[1] 57 | raise ConnectionError( 58 | "Error while reading from stream: %s" % (e.args, )) 59 | 60 | @gen.coroutine 61 | def read(self, length): 62 | length = length + 2 63 | 64 | if length > self.length: 65 | yield self._read_from_stream(length - self.length) 66 | 67 | self._buffer.seek(self.bytes_read) 68 | data = self._buffer.read(length) 69 | self.bytes_read += len(data) 70 | 71 | if self.bytes_read == self.bytes_written: 72 | self.purge() 73 | 74 | raise gen.Return(data[:-2]) 75 | 76 | @gen.coroutine 77 | def readline(self): 78 | buf = self._buffer 79 | buf.seek(self.bytes_read) 80 | data = buf.readline() 81 | 82 | while not data.endswith(SYM_CRLF): 83 | yield self._read_from_stream() 84 | buf.seek(self.bytes_read) 85 | data = buf.readline() 86 | 87 | self.bytes_read += len(data) 88 | 89 | if self.bytes_read == self.bytes_written: 90 | self.purge() 91 | 92 | raise gen.Return(data[:-2]) 93 | 94 | 95 | class AsyncParser(PythonParser): 96 | """ Parser class for connections using tornado asynchronous """ 97 | 98 | def __init__(self, socket_read_size): 99 | self.socket_read_size = socket_read_size 100 | self.encoder = None 101 | self._stream = None 102 | self._buffer = None 103 | 104 | def __del__(self): 105 | try: 106 | self.on_disconnect() 107 | except Exception: 108 | pass 109 | 110 | def on_connect(self, connection): 111 | """ Called when stream connects """ 112 | self._stream = weakref.proxy(connection._stream) 113 | self._buffer = StreamBuffer(self._stream, self.socket_read_size) 114 | self.encoder = connection.encoder 115 | 116 | def on_disconnect(self): 117 | if self._stream is not None: 118 | self._stream.close() 119 | self._stream = None 120 | 121 | if self._buffer is not None: 122 | self._buffer.close() 123 | self._buffer = None 124 | 125 | self.encoding = None 126 | 127 | def can_read(self): 128 | return self._buffer and bool(self._buffer.length) 129 | 130 | @gen.coroutine 131 | def read_response(self): 132 | response = yield self._buffer.readline() 133 | 134 | if not response: 135 | raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) 136 | 137 | byte, response = byte_to_chr(response[0]), response[1:] 138 | 139 | if byte not in ('-', '+', ':', '$', '*'): 140 | raise InvalidResponse("Protocol Error: %s, %s" % 141 | (str(byte), str(response))) 142 | 143 | # server returned an error 144 | if byte == '-': 145 | response = nativestr(response) 146 | error = self.parse_error(response) 147 | # if the error is a ConnectionError, raise immediately so the user 148 | # is notified 149 | if isinstance(error, ConnectionError): 150 | raise error 151 | # otherwise, we're dealing with a ResponseError that might belong 152 | # inside a pipeline response. the connection's read_response() 153 | # and/or the pipeline's execute() will raise this error if 154 | # necessary, so just return the exception instance here. 155 | raise gen.Return(error) 156 | # single value 157 | elif byte == '+': 158 | pass 159 | # int value 160 | elif byte == ':': 161 | response = int(response) 162 | # bulk response 163 | elif byte == '$': 164 | length = int(response) 165 | if length == -1: 166 | raise gen.Return(None) 167 | response = yield self._buffer.read(length) 168 | # multi-bulk response 169 | elif byte == '*': 170 | length = int(response) 171 | if length == -1: 172 | raise gen.Return(None) 173 | response = [] 174 | 175 | for i in range(length): 176 | res = yield self.read_response() 177 | response.append(res) 178 | 179 | if isinstance(response, bytes): 180 | response = self.encoder.decode(response) 181 | 182 | raise gen.Return(response) 183 | 184 | 185 | class AsyncConnection(Connection, TCPClient): 186 | def __init__(self, *args, **kwargs): 187 | 188 | TCPClient.__init__(self, kwargs.pop("resolver", None), 189 | kwargs.pop("io_loop", None)) 190 | 191 | Connection.__init__(self, parser_class=AsyncParser, *args, **kwargs) 192 | 193 | self._stream = None 194 | 195 | @gen.coroutine 196 | def connect(self): 197 | 198 | if self._stream: 199 | return 200 | try: 201 | stream = yield self._connect() 202 | except socket.error: 203 | e = sys.exc_info()[1] 204 | raise ConnectionError(self._error_message(e)) 205 | 206 | self._stream = stream 207 | 208 | try: 209 | yield self.on_connect() 210 | except RedisError: 211 | self.disconnect() 212 | raise 213 | 214 | @gen.coroutine 215 | def _connect(self): 216 | stream = yield TCPClient.connect(self, self.host, self.port) 217 | raise gen.Return(stream) 218 | 219 | @gen.coroutine 220 | def on_connect(self): 221 | self._parser.on_connect(self) 222 | 223 | if self.password: 224 | yield self.send_command('AUTH', self.password) 225 | 226 | response = yield self.read_response() 227 | if nativestr(response) != 'OK': 228 | raise AuthenticationError('Invalid Password') 229 | 230 | if self.db: 231 | yield self.send_command('SELECT', self.db) 232 | response = yield self.read_response() 233 | if nativestr(response) != 'OK': 234 | raise ConnectionError('Invalid Database') 235 | 236 | def disconnect(self): 237 | "Disconnects from the Redis server" 238 | self._parser.on_disconnect() 239 | if self._stream is None: 240 | return 241 | try: 242 | self._stream.close() 243 | except StreamClosedError: 244 | pass 245 | self._stream = None 246 | 247 | @gen.coroutine 248 | def send_packed_command(self, command): 249 | "Send an already packed command to the Redis server" 250 | if not self._sock: 251 | yield self.connect() 252 | try: 253 | if isinstance(command, str): 254 | command = [command] 255 | for item in command: 256 | yield self._stream.write(item) 257 | 258 | except StreamClosedError: 259 | self.disconnect() 260 | raise TimeoutError("Timeout writing to socket") 261 | except socket.error: 262 | e = sys.exc_info()[1] 263 | self.disconnect() 264 | if len(e.args) == 1: 265 | _errno, errmsg = 'UNKNOWN', e.args[0] 266 | else: 267 | _errno, errmsg = e.args 268 | raise ConnectionError("Error %s while writing to socket. %s." % 269 | (_errno, errmsg)) 270 | except: 271 | self.disconnect() 272 | raise 273 | 274 | @gen.coroutine 275 | def send_command(self, *args): 276 | "Pack and send a command to the Redis server" 277 | yield self.send_packed_command(self.pack_command(*args)) 278 | 279 | @gen.coroutine 280 | def can_read(self, timeout=0): 281 | "Poll the socket to see if there's data that can be read." 282 | sock = self._stream 283 | if not sock: 284 | yield self.connect() 285 | sock = self._stream.socket 286 | raise gen.Return(self._parser.can_read() or 287 | bool(select([sock], [], [], timeout)[0])) 288 | 289 | @gen.coroutine 290 | def read_response(self): 291 | "Read the response from a previously sent command" 292 | try: 293 | response = yield self._parser.read_response() 294 | except: 295 | self.disconnect() 296 | raise 297 | 298 | if isinstance(response, ResponseError): 299 | raise response 300 | 301 | raise gen.Return(response) 302 | 303 | def to_blocking_connection(self, socket_read_size=65536): 304 | """ Convert asynchronous connection to blocking socket connection 305 | """ 306 | conn = Connection( 307 | self.host, self.port, self.db, self.password, self.socket_timeout, 308 | self.socket_connect_timeout, self.socket_keepalive, 309 | self.socket_keepalive_options, self.retry_on_timeout, 310 | self.encoding, self.encoding_errors, self.decode_responses, 311 | DefaultParser, socket_read_size) 312 | 313 | conn._sock = self._stream.socket 314 | return conn 315 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado>=4.0 2 | redis 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | from __future__ import unicode_literals, print_function, division 4 | 5 | from gredis import version 6 | 7 | from setuptools import setup 8 | 9 | setup( 10 | name="gredis", 11 | version=version, 12 | packages=["gredis"], 13 | package_data={ 14 | }, 15 | author="cold", 16 | author_email="wh_linux@126.com", 17 | url="https://github.com/coldnight/gredis", 18 | license="http://www.apache.org/licenses/LICENSE-2.0", 19 | description=("gRedis is an asynchronous client library of Redis written " 20 | "with Tornado coroutine."), 21 | long_description=open("README.rst").read(), 22 | install_requires=["tornado>=4.0", "redis"], 23 | classifiers=[ 24 | 'License :: OSI Approved :: Apache Software License', 25 | 'Programming Language :: Python :: 2', 26 | 'Programming Language :: Python :: 2.6', 27 | 'Programming Language :: Python :: 2.7', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.2', 30 | 'Programming Language :: Python :: 3.3', 31 | 'Programming Language :: Python :: 3.4', 32 | 'Programming Language :: Python :: Implementation :: CPython', 33 | 'Programming Language :: Python :: Implementation :: PyPy', 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldnight/gredis/6642df5bfe2e82b72e5afe10c9f12a6b60b3d55b/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_gredis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author : cold 5 | # E-mail : wh_linux@126.com 6 | # Date : 15/04/15 17:10:57 7 | # Desc : 8 | # 9 | from __future__ import absolute_import, print_function, division, with_statement 10 | 11 | import os 12 | from tornado.testing import gen_test, AsyncTestCase 13 | 14 | from gredis.client import AsyncRedis 15 | 16 | 17 | class GRedisTest(AsyncTestCase): 18 | def setUp(self): 19 | super(GRedisTest, self).setUp() 20 | self.client = AsyncRedis( 21 | "192.168.1.50", 6379, encoding="utf8", 22 | decode_responses=True, 23 | ) 24 | 25 | @gen_test 26 | def test_get_set(self): 27 | key = "g_test_key" 28 | yield self.client.set(key, "w") 29 | # print("set") 30 | future = self.client.get(key) 31 | response = yield future 32 | self.assertEqual(response, "w") 33 | 34 | @gen_test 35 | def test_list(self): 36 | key = "g_test_list_key" 37 | self.client.delete(key) 38 | yield self.client.lpush(key, "w") 39 | key, response = yield self.client.blpop(key) 40 | print(response) 41 | self.assertEqual(response, "w") 42 | lst = ['a', 'b', 'c'] 43 | for i in lst: 44 | yield self.client.rpush(key, i) 45 | _lst = yield self.client.lrange(key, 0, -1) 46 | self.assertListEqual(lst, _lst) 47 | 48 | @gen_test 49 | def test_to_blocking_client(self): 50 | key = "g_test_1_key" 51 | 52 | yield self.client.set(key, "gredis") 53 | client = self.client.to_blocking_client() 54 | result = client.get(key) 55 | self.assertEqual(result, "gredis") 56 | asyc_result = yield self.client.get(key) 57 | self.assertEqual(asyc_result, "gredis") 58 | 59 | @gen_test 60 | def test_blocking_pipeline(self): 61 | key = "g_pipeline_test" 62 | key1 = "g_pipeline_test1" 63 | pipeline = self.client.pipeline() 64 | 65 | pipeline.set(key, "gredis") 66 | pipeline.set(key1, "gredis1") 67 | pipeline.execute() 68 | 69 | ret = yield self.client.get(key) 70 | self.assertEqual(ret, "gredis") 71 | ret1 = yield self.client.get(key1) 72 | self.assertEqual(ret1, "gredis1") 73 | 74 | @gen_test 75 | def test_async_pubsub(self): 76 | pubsub = self.client.pubsub() 77 | channel = "test" 78 | yield pubsub.subscribe(channel) 79 | response = yield pubsub.get_message(True) 80 | self.assertEqual(response["type"], "subscribe") 81 | yield self.client.publish(channel, "test") 82 | response = yield pubsub.get_message(True) 83 | self.assertEqual(response["type"], "message") 84 | self.assertEqual(response["data"], "test") 85 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,py35,py36,flake8,pylint 3 | 4 | [testenv] 5 | deps = 6 | nose 7 | coverage 8 | modcov 9 | commands = 10 | nosetests -vv --with-coverage 11 | modcov --git --fail-under 90 --exclude tests/*,setup.py 12 | --------------------------------------------------------------------------------