#!/usr/bin/env python # -*- coding:utf-8 -*-from aioredis import create_redis_pool import datetime import time as mod_time from redis.exceptions import ( ConnectionError, DataError, ExecAbortError, NoScriptError, PubSubError, RedisError, ResponseError, TimeoutError, WatchError, ) _NOTSET = 'UTF-8' try: from sanic.log import logger except ImportError: from sanic.log import log as logger __version__ = '0.1.0' class BaseRedis: __coll__ = None # KeyGroup name __dbkey__ = None # which database connected to? __unique_fields__ = [] __motor_redis_client__ = None __motor_redis_db__ = None __motor_redis_clients__ = {} __motor_redis_dbs__ = {} __app__ = None __apps__ = {} __timezone__ = None def __init__(self, *args, **kwargs): self.__dict__.update(kwargs) @classmethod async def Execute(cls, command, *args, **kwargs): kwargs['encoding'] = kwargs.pop('encoding', _NOTSET) return await cls.__motor_redis_client__.execute(command, *args, **kwargs) @staticmethod def init_app(app, open_listener='before_server_start', close_listener='before_server_stop', name=None, uri=None): BaseRedis.__app__ = app BaseRedis.__apps__[name or app.name] = app if open_listener: @app.listener(open_listener) async def open_connection(app, loop): await BaseRedis.default_open_connection(app, loop, name, uri) if close_listener: @app.listener(close_listener) async def close_connection(app, loop): await BaseRedis.default_close_connection(app, loop) @staticmethod async def default_open_connection(app, loop, name=None, *args, **kwargs): if not name: name = app.name db = kwargs.pop('db', None) logger.info('opening motor connection for redis [{}]'.format(name)) client = await create_redis_pool((app.config.redishost, app.config.redisport), db=db or app.config.redisdb,password=app.config.redispwd, loop=loop) app.motor_redis_client = client BaseRedis.__motor_redis_client__ = client BaseRedis.__motor_redis_db__ = db if not hasattr(app, 'motor_redis_clients'): app.motor_redis_clients = {} app.motor_redis_clients[name] = client BaseRedis.__motor_redis_clients__[name] = client BaseRedis.__motor_redis_dbs__[name] = db @staticmethod async def default_close_connection(app, loop): if hasattr(app, 'motor_clients'): for name, client in app.motor_redis_clients.items(): logger.info('closing motor redis connection for [{}]'.format(name)) client.close() await client.wait_closed() @classmethod async def set_keys(cls,dict_obj=None,*args,**kwargs): for key,item in dict_obj.items(): await cls.set(key,item) return True def __delitem__(self, key): self.delete(key) @classmethod async def delete(cls,*keys): keys = [cls.__coll__ + item for item in keys] return await cls.Execute('DEL', *keys) @classmethod async def exists(cls, name): "Returns a boolean indicating whether key ``name`` exists" key = cls.__coll__ + name return await cls.Execute('EXISTS', key) __contains__ = exists @classmethod async def expire(cls, name, time): """ Set an expire flag on key ``name`` for ``time`` seconds. ``time`` can be represented by an integer or a Python timedelta object. """ name = cls.__coll__ + name if isinstance(time, datetime.timedelta): time = time.seconds + time.days * 24 * 3600 return await cls.Execute('EXPIRE', name, time) @classmethod async def expireat(cls, name, when): """ Set an expire flag on key ``name``. ``when`` can be represented as an integer indicating unix time or a Python datetime object. """ name = cls.__coll__ + name if isinstance(when, datetime.datetime): when = int(mod_time.mktime(when.timetuple())) return await cls.Execute('EXPIREAT', name, when) @classmethod async def keys(cls, pattern='*'): "Returns a list of keys matching ``pattern``" return await cls.Execute('KEYS', *pattern,encoding='utf-8') # SERVER INFORMATION @classmethod async def bgrewriteaof(cls): "Tell the Redis server to rewrite the AOF file from data in memory." return cls.Execute('BGREWRITEAOF') @classmethod async def bgsave(cls): """ Tell the Redis server to save its data to disk. Unlike save(), this method is asynchronous and returns immediately. """ return cls.Execute('BGSAVE') @classmethod async def client_kill(cls, address): "Disconnects the client at ``address`` (ip:port)" return cls.Execute('CLIENT KILL', address) @classmethod async def client_list(cls): "Returns a list of currently connected clients" return cls.Execute('CLIENT LIST') @classmethod async def client_getname(cls): "Returns the current connection name" return cls.Execute('CLIENT GETNAME') @classmethod async def client_setname(cls, name): "Sets the current connection name" return cls.Execute('CLIENT SETNAME', name) @classmethod async def config_get(cls, pattern="*"): "Return a dictionary of configuration based on the ``pattern``" return cls.Execute('CONFIG GET', *pattern,encoding='utf-8') @classmethod async def config_set(cls, name, value): "Set config item ``name`` with ``value``" return cls.Execute('CONFIG SET', name, value) @classmethod async def config_resetstat(self): "Reset runtime statistics" return self.Execute('CONFIG RESETSTAT') @classmethod async def config_rewrite(cls): "Rewrite config file with the minimal change to reflect running config" return await cls.Execute('CONFIG REWRITE') @classmethod async def dbsize(self): "Returns the number of keys in the current database" return self.Execute('DBSIZE') @classmethod async def debug_object(cls, key): "Returns version specific meta information about a given key" return await cls.Execute('DEBUG OBJECT', key) @classmethod async def echo(cls, value): "Echo the string back from the server" return await cls.Execute('ECHO', value) @classmethod async def flushall(cls): "Delete all keys in all databases on the current host" return await cls.Execute('FLUSHALL') @classmethod async def flushdb(cls): "Delete all keys in the current database" return await cls.Execute('FLUSHDB') @classmethod async def info(cls, section=None): """ Returns a dictionary containing information about the Redis server The ``section`` option can be used to select a specific section of information The section option is not supported by older versions of Redis Server, and will generate ResponseError """ if section is None: return await cls.Execute('INFO') else: return await cls.Execute('INFO', section) async def lastsave(self): """ Return a Python datetime object representing the last time the Redis database was saved to disk """ return await self.Execute('LASTSAVE') async def ping(self): "Ping the Redis server" return await self.Execute('PING') async def save(self): """ Tell the Redis server to save its data to disk, blocking until the save is complete """ return await self.Execute('SAVE') async def time(self): """ Returns the server time as a 2-item tuple of ints: (seconds since epoch, microseconds into this second). """ return await self.Execute('TIME') # BASIC KEY COMMANDS @classmethod async def append(cls, key, value): """ Appends the string ``value`` to the value at ``key``. If ``key`` doesn't already exist, create it with a value of ``value``. Returns the new length of the value at ``key``. """ return await cls.Execute('APPEND', key, value) def bitcount(self, key, start=None, end=None): """ Returns the count of set bits in the value of ``key``. Optional ``start`` and ``end`` paramaters indicate which bytes to consider """ params = [key] if start is not None and end is not None: params.append(start) params.append(end) elif (start is not None and end is None) or \ (end is not None and start is None): raise RedisError("Both start and end must be specified") return self.Execute('BITCOUNT', *params) def bitop(self, operation, dest, *keys): """ Perform a bitwise operation using ``operation`` between ``keys`` and store the result in ``dest``. """ return self.Execute('BITOP', operation, dest, *keys) @classmethod async def bitpos(cls, key, bit, start=None, end=None): """ Return the position of the first bit set to 1 or 0 in a string. ``start`` and ``end`` difines search range. The range is interpreted as a range of bytes and not a range of bits, so start=0 and end=2 means to look at the first three bytes. """ key = cls.__coll__ + key if bit not in (0, 1): raise RedisError('bit must be 0 or 1') params = [key, bit] start is not None and params.append(start) if start is not None and end is not None: params.append(end) elif start is None and end is not None: raise RedisError("start argument is not set, " "when end is specified") return await cls.Execute('BITPOS', *params) @classmethod async def decr(cls, name, amount=1): """ Decrements the value of ``key`` by ``amount``. If no key exists, the value will be initialized as 0 - ``amount`` """ name = cls.__coll__ + name return await cls.Execute('DECRBY', name, amount) @classmethod async def dump(cls, name): """ Return a serialized version of the value stored at the specified key. If key does not exist a nil bulk reply is returned. """ name = cls.__coll__ + name return await cls.Execute('DUMP', name) @classmethod async def get(cls, name): """ Return the value at key ``name``, or None if the key doesn't exist """ name = cls.__coll__ + name return await cls.Execute('GET', name) def __getitem__(self, name): """ Return the value at key ``name``, raises a KeyError if the key doesn't exist. """ value = self.get(name) if value is not None: return value raise KeyError(name) @classmethod async def getbit(cls, name, offset): "Returns a boolean indicating the value of ``offset`` in ``name``" return await cls.Execute('GETBIT', name, offset) @classmethod async def getrange(cls, key, start, end): """ Returns the substring of the string value stored at ``key``, determined by the offsets ``start`` and ``end`` (both are inclusive) """ return await cls.Execute('GETRANGE', key, start, end) @classmethod async def getset(cls, name, value): """ Sets the value at key ``name`` to ``value`` and returns the old value at key ``name`` atomically. """ return await cls.Execute('GETSET', name, value) @classmethod async def incr(cls, name, amount=1): """ Increments the value of ``key`` by ``amount``. If no key exists, the value will be initialized as ``amount`` """ return await cls.Execute('INCRBY', name, amount) @classmethod async def incrby(cls, name, amount=1): """ Increments the value of ``key`` by ``amount``. If no key exists, the value will be initialized as ``amount`` """ # An alias for ``incr()``, because it is already implemented # as INCRBY redis command. return await cls.incr(name, amount) @classmethod async def incrbyfloat(cls, name, amount=1.0): """ Increments the value at key ``name`` by floating ``amount``. If no key exists, the value will be initialized as ``amount`` """ return await cls.Execute('INCRBYFLOAT', name, amount) @classmethod async def rename(cls, src, dst): """ Rename key ``src`` to ``dst`` """ return await cls.Execute('RENAME', src, dst) @classmethod async def renamenx(cls, src, dst): "Rename key ``src`` to ``dst`` if ``dst`` doesn't already exist" return await cls.Execute('RENAMENX', src, dst) @classmethod async def restore(cls, name, ttl, value, replace=False): """ Create a key using the provided serialized value, previously obtained using DUMP. """ params = [name, ttl, value] if replace: params.append('REPLACE') return await cls.Execute('RESTORE', *params) @classmethod async def set(cls, name, value, ex=None, px=None, nx=False, xx=False): """ Set the value at key ``name`` to ``value`` ``ex`` sets an expire flag on key ``name`` for ``ex`` seconds. ``px`` sets an expire flag on key ``name`` for ``px`` milliseconds. ``nx`` if set to True, set the value at key ``name`` to ``value`` only if it does not exist. ``xx`` if set to True, set the value at key ``name`` to ``value`` only if it already exists. """ name = cls.__coll__ + name pieces = [name, value] if ex is not None: pieces.append('EX') if isinstance(ex, datetime.timedelta): ex = ex.seconds + ex.days * 24 * 3600 pieces.append(ex) if px is not None: pieces.append('PX') if isinstance(px, datetime.timedelta): ms = int(px.microseconds / 1000) px = (px.seconds + px.days * 24 * 3600) * 1000 + ms pieces.append(px) if nx: pieces.append('NX') if xx: pieces.append('XX') return await cls.Execute('SET', *pieces,encoding='utf-8') def __setitem__(self, name, value): self.set(name, value) @classmethod async def setbit(cls, name, offset, value): """ Flag the ``offset`` in ``name`` as ``value``. Returns a boolean indicating the previous value of ``offset``. """ value = value and 1 or 0 return await cls.Execute('SETBIT', name, offset, value)