"""
This module defines base classes corresponding to Redis types as well
as Redis model.
"""
from collections import defaultdict
from six import with_metaclass
from .base import RedisModelRegister, RedisModelCreator, MapModelBase, MapModelException
__all__ = ['RedisModel', 'RedisSortedSet', 'RedisModelException', 'RedisHash', 'RedisList']
[docs]class RedisModelException(MapModelException):
"""
Exception raised when errors related to Redis handling are encountered.
"""
pass
[docs]class RedisModel(with_metaclass(RedisModelCreator, MapModelBase)):
"""
This is the base class for Redis models. Internally they are just a Redis hash.
This class enables reading object with given id, saving object and data
(de)serialization.
Redis connection is available in connect property.
Dict of fields is available in _fields property.
Reserved property names, apart from methods, are _fields, id_field and connect.
:type connect: redis.Redis
"""
__metaclass__ = RedisModelCreator
MapModelException = RedisModelException
RedisModelException = RedisModelException
namespace = 'redis'
connect = None
[docs] def save(self, create_id=True):
"""
Let's save instance's current state to Redis.
:param create_id: whether id should be created automatically if it's not set yet.
:returns: self
"""
self._save(create_id)
self.connect.hmset(self.get_instance_key(), self.serialize())
return self
@classmethod
[docs] def get_key(cls, oid):
"""
This function creates a key in which Redis will save the instance with given id.
:param oid: id of object for which a key should be created.
:returns: Redis key.
"""
return "{0.__module__}.{0.__name__}.{1}".format(cls, oid)
@classmethod
[docs] def get(cls, oid):
"""
This method gets a model instance with given id from Redis.
:param oid: id of object to get.
:returns: hydrated model instance.
"""
data = cls.connect.hgetall(cls.get_key(oid))
if data:
return cls(**cls.pythonize(data))
raise RedisModelException('No object with primary key {} of class {}'.format(cls.get_key(oid), cls.__name__))
class RedisSortedSetSlice(object):
"""
An inner class proxying ranges returned by ZRANGEBYSCORE to enable indexing by count.
It does not enable changing elements' values.
:type connect: redis.Redis
"""
def __init__(self, connect, key, start, end):
"""
This method sets up the properties required by object to work.
:param connect: Redis connection.
:param key: key where sorted set is kept.
:param start: starting SCORE
:param end: ending SCORE
"""
self.connect = connect
self.key = key
self.start = start or '-inf'
self.end = end or '+inf'
def __getitem__(self, item):
"""
This function translates Python index and slice into ZRANGEBYSCORE and returns Redis's response.
:param item: index or slice to get.
:returns: element or a list of elements.
"""
if isinstance(item, slice):
if item.start is None:
start = 0
else:
start = item.start
if item.stop is None:
return self.connect.zrangebyscore(self.key, self.start, self.end, start, len(self))
return self.connect.zrangebyscore(self.key, self.start, self.end, start, item.stop-item.start)
else:
return self.connect.zrangebyscore(self.key, self.start, self.end, item, 1)[0]
def __len__(self):
"""
Returns Redis-counted number of elements in range.
:returns: number of elements in range.
"""
return self.connect.zcount(self.key, self.start, self.end)
[docs]class RedisSortedSet(object):
"""
This class is used to proxy Redis's Sorted Sets. It allows value search with
pagination and delayed (lazy) key alterations. Indexing works with SCORE,
not MEMBER or RANK.
set_score and delete_item methods don't interface with Redis directly, but are
queued in a change list.
:type connect: redis.Redis
:type changes: dict
"""
namespace = 'redis'
def __init__(self, name, namespace=None):
"""
This function creates changelist and remembers the name of this sorted set.
By default name is used as Redis key for this instance.
:param namespace: name of connection used for this instance.
:param name: name of sorted set.
"""
self.connect = RedisModelRegister(namespace or self.namespace).connect()
self.name = name
self.changes = defaultdict(list)
[docs] def clear(self):
"""
I'm tired of you, off you go. Disappear from Redis. NOW.
"""
self.connect.delete(self.get_instance_key())
def __getitem__(self, item):
"""
Returns RedisSortedSetSlice for given SCORE or its range passed as a slice.
:param item: SCORE or slice [SCORE MIN, SCORE MAX].
:returns: RedisSortedSetSlice for given SCORE or its range.
"""
if isinstance(item, slice):
return RedisSortedSetSlice(self.connect, self.get_instance_key(), item.start, item.stop)
return RedisSortedSetSlice(self.connect, self.get_instance_key(), item, item)
def __delitem__(self, item):
"""
Removes elements with given SCORE or in given SCORE range.
:param item: SCORE or slice [SCORE MIN, SCORE MAX]
"""
if isinstance(item, slice):
start = item.start or '-inf'
stop = item.stop or '+inf'
self.connect.zremrangebyscore(self.get_instance_key(), start, stop)
else:
self.connect.zremrangebyscore(self.get_instance_key(), item, item)
def __len__(self):
"""
Let's see how many items do we have in our set. As returned by Redis.
:returns: number of elements in set.
"""
return self.connect.zcard(self.get_instance_key())
[docs] def set_score(self, item, score):
"""
This function adds a new element if it's not in Redis and sets its SCORE.
You need to call save() to propagate changes to Redis.
:param item: element to be added or modified.
:param score: element's SCORE.
"""
self.changes[item].append(float(score))
[docs] def delete_item(self, item):
"""
This method deletes given element.
You need to call save() to propagate changes to Redis.
:param item: element to be removed.
"""
self.changes[item].append(None)
[docs] def lowest(self):
"""
Returns element with lowest SCORE and its SCORE.
:returns: element with lowest SCORE and its SCORE.
"""
return (self.connect.zrange(self.get_instance_key(), 0, 0, withscores=True) or [(None, 0)])[0]
[docs] def highest(self):
"""
Returns element with highest SCORE and its SCORE.
:returns: element with highest SCORE and its SCORE.
"""
return (self.connect.zrevrange(self.get_instance_key(), 0, 0, withscores=True)or [(None, 0)])[0]
[docs] def save(self):
"""
This method analyzes changelist and using as few operations as possible propagates
changes to Redis's Sorted Set representing this instance.
"""
to_remove = []
to_add = {}
for key, value in self.changes.items():
if value[-1] is None:
to_remove.append(key)
else:
to_add[key] = value[-1]
if to_remove:
self.connect.zrem(self.get_instance_key(), *to_remove)
if to_add:
self.connect.zadd(self.get_instance_key(), **to_add)
self.changes.clear()
[docs] def get_instance_key(self):
"""
This function creates Redis's instance key.
:returns: key in which instance will be saved.
"""
return self.get_key(self.name)
@classmethod
[docs] def get_key(cls, name):
"""
This method creates a Redis key in which instance with given name will be saved.
:param name: name of object for which a key is to be made.
:returns: key used in Redis for given name.
"""
return name
[docs]class RedisHash(object):
"""
This class acts as a proxy for Redis Hash. It enables delayed modifications.
__setitem__ and __delitem__ methods don't modify Redis immediately, but are instead
queued in a changelist.
:type connect: redis.Redis
:type changes: dict
"""
namespace = 'redis'
def __init__(self, name, namespace=None):
"""
This function initializes changelist and remembers name of the hash.
By default name is used as Redis key for this instance.
:param namespace: name of connection used by this instance.
:param name: name of the hash.
"""
self.connect = RedisModelRegister(namespace or self.namespace).connect()
self.name = name
self.changes = defaultdict(list)
[docs] def clear(self):
"""
This removes whole hash from Redis.
"""
self.connect.delete(self.get_instance_key())
[docs] def get(self, *fields):
"""
This gets one or many items from Redis.
:param fields: list of fields to get.
"""
return self.connect.hmget(self.get_instance_key(), *fields)
def __getitem__(self, item):
"""
This function returns value assigned to given key.
:param item: key belonging to this hash.
:returns: given key's value.
"""
return self.connect.hget(self.get_instance_key(), item)
def __delitem__(self, item):
"""
Removes given key from hash.
:param item: key to be removed.
"""
self.changes[item].append(None)
def __len__(self):
"""
How many elements are in hash - as Redis says.
:returns: number of elements in hash.
"""
return self.connect.hlen(self.get_instance_key())
[docs] def keys(self):
"""
This returns list of keys in hash.
:returns: list of keys in hash.
"""
return self.connect.hkeys(self.get_instance_key())
[docs] def items(self):
"""
This returns key, value pairs available in this hash.
:returns: hash's key, value pairs.
"""
return self.connect.hgetall(self.get_instance_key())
def __contains__(self, item):
"""
This functions checks for given key's existence in hash.
:param item: key to be checked.
:returns: boolean
"""
return self.connect.hexists(self.get_instance_key(), item)
def __setitem__(self, item, value):
"""
Assigns value to key in the hash.
:param item: key
:param value: value
:returns:
"""
self.changes[item].append(value)
[docs] def save(self):
"""
This method analyzes changelist and using as few operations as possible propagates
changes to Redis's Hash representing this instance.
"""
to_remove = []
to_add = {}
for key, value in self.changes.items():
if value[-1] is None:
to_remove.append(key)
else:
to_add[key] = value[-1]
if to_remove:
self.connect.hdel(self.get_instance_key(), *to_remove)
if to_add:
self.connect.hmset(self.get_instance_key(), to_add)
self.changes.clear()
[docs] def get_instance_key(self):
"""
This function creates Redis's instance key.
:returns: key in which instance will be saved.
"""
return self.get_key(self.name)
@classmethod
[docs] def get_key(cls, name):
"""
This method creates a Redis key in which instance with given name will be saved.
:param name: name of object for which a key is to be made.
:returns: key used in Redis for given name.
"""
return name
[docs]class RedisList(object):
"""
This class is a proxy for Redis List. It enables instant modifications to
Redis entity. It has only basic operations pythonized at the moment.
:type connect: redis.Redis
"""
namespace = 'redis'
def __init__(self, name, namespace=None):
"""
This function initializes and remembers name of the hash.
By default name is used as Redis key for this instance.
:param namespace: name of connection used by this instance.
:param name: name of hash.
"""
self.connect = RedisModelRegister(namespace or self.namespace).connect()
self.name = name
[docs] def clear(self):
"""
This removes the list from Redis.
"""
self.connect.delete(self.get_instance_key())
def __getitem__(self, item):
"""
Returns value(s) for given index or slice [min, max].
:param item: index or slice.
:returns: value or values.
"""
if isinstance(item, slice):
if item.start is None:
start = 0
else:
start = item.start
if item.stop is None:
return self.connect.lrange(self.get_instance_key(), start, -1)
return self.connect.lrange(self.get_instance_key(), start, item.stop)
else:
return self.connect.lrange(self.get_instance_key(), item, item)[0]
[docs] def remove(self, item):
"""
Removes all elements with given value.
:param item: value to be removed.
"""
self.connect.lrem(self.get_instance_key(), item, 0)
[docs] def append(self, item):
"""
Adds element at the end of the list.
:param item: element to be appended.
"""
return self.connect.rpush(self.get_instance_key(), item)
[docs] def prepend(self, item):
"""
Adds element at the beginning of the list.
:param item: element to be prepended.
"""
return self.connect.lpush(self.get_instance_key(), item)
[docs] def pop(self, first=False):
"""
Gets and removes an element from list's edge. By default it's the last element.
:param first: Should the first element be popped instead of the last.
"""
if first:
return self.connect.lpop(self.get_instance_key())
else:
return self.connect.rpop(self.get_instance_key())
def __len__(self):
"""
List length as returned by Redis.
:returns: number of elements in the list.
"""
return self.connect.llen(self.get_instance_key())
def __setitem__(self, item, value):
"""
Assigns a value to given index.
:param item: index
:param value: value
"""
self.connect.lset(self.get_instance_key(), item, value)
[docs] def get_instance_key(self):
"""
This function creates Redis's instance key.
:returns: key in which instance will be saved.
"""
return self.get_key(self.name)
@classmethod
[docs] def get_key(cls, name):
"""
This method creates a Redis key in which instance with given name will be saved.
:param name: name of object for which a key is to be made.
:returns: key used in Redis for given name.
"""
return name