Source code for redisORM.redis_model

#!/usr/bin/env python
"""
This module provides a basic object mapper for groups of Redis keys.

.. note::
    The model stores all its data with a Redis key structure like so:
    namespace:key:part

    Where:
        #. namespace - the key prefix
        #. key - the actual name or id of this object
        #. part - the specific element of the model

Basic use is like so:

>>> import redis
>>> from redisORM import RedisModel
>>> redis_instance = redis.StrictRedis("localhost", db=0)

Lets create a new model:

>>> sample1 = RedisModel(namespace="test", key="sample1", conn=redis_instance)
>>> sample1 #doctest: +ELLIPSIS
<RedisModel.RedisModel ...>

And now lets add some data to it. Two strings and a list:

>>> sample1.name = "Ludwig Van Beethoven"
>>> sample1["era"] = "Classical" # you can also treat it like a dictionary (with some missing features)
>>> sample1.famous_works = ["Symphony No.5", "Symphony No.7", "Symphony No.9"] # Lists have limited support also

Along with setting data you can also access data, both like an object property
or by using the dictionary index style:

>>> sample1["name"]
'Ludwig Van Beethoven'
>>> sample1.era
'Classical'
>>> sample1.famous_works
['Symphony No.5', 'Symphony No.7', 'Symphony No.9']

You can also check for a property in the model:

>>> "name" in sample1
True
>>> "age" in sample1
False
>>> "Symphony No.9" in sample1.famous_works
True
"""

redis = None
"""
Global RedisModel connection which can be set before hand to avoid passing a
redis instance around everywhere all the time.
"""


[docs]class RedisORMException(Exception): """ The general exception class which is raised by this module. Nothing special. """
[docs]class RedisKeys(object): """ Where the realtime syncing and updating takes place. A `dict` like object which is used as the backing data store for :py:class:`.RedisModel`. Aka: The Source of Magic """ def __init__(self, key, namespace="", conn=None): """ Creates a new `dict` like object which is used to actually store data in Redis. Under all normal circumstances, you should not need to use this class in any way shape or form, as it is the backing datastore for the model. :param key: The key section of the Redis key. :param namespace: The key namespace. :param conn: The Redis connection to use. :raises RedisORMException: If no key was provided. """ self._data = dict() self.conn = conn self.namespace = namespace # Key prefix self.key = key if not self.key: raise RedisORMException("RedisKeys needs a key, which means something went terribly wrong.") redis_search_key = ":".join([self.namespace, self.key, "*"]) keys = self.conn.keys(redis_search_key) if keys: for key in keys: part = key.split(":")[-1] self.get(part)
[docs] def delete(self): """ Deletes all the keys from redis along with emptying the objects internal `_data` dict, then deleting itself at the end of it all. """ redis_search_key = ":".join([self.namespace, self.key, "*"]) keys = self.conn.keys(redis_search_key) if keys: for key in keys: part = key.split(":")[-1] self._data.pop(part) self.conn.delete(part) del self
[docs] def get(self, part): """ Retrieves a part of the model from redis and stores it. :param part: The part of the model to retrieve. :raises RedisORMException: If the redis type is different from string or list (the only two supported types at this time.) """ redis_key = ':'.join([self.namespace, self.key, part]) objectType = self.conn.type(redis_key) if objectType == "string": self._data[part] = self.conn.get(redis_key) elif objectType == "list": self._data[part] = RedisList(redis_key, self.conn) else: raise RedisORMException("Other types besides string and list are unsupported at this time.")
[docs] def get_default(self, part, default=None): """ Works just like a `dict`'s `get()` method, returning the default if no matching key was found. :param part: The key which to look for :param default: The default to return if no match was found """ return self._data.get(part, default)
def __repr__(self): return str(self._data) def __getitem__(self, part): return self._data[part] def __setitem__(self, part, value): key = ':'.join([self.namespace, self.key, part]) if isinstance(value, list): self._data[part] = RedisList(key, self.conn, start=value) else: if value == None: value = "" self._data[part] = value self.conn.set(key, value) def __delitem__(self, part): key = ':'.join([self.namespace, self.key, part]) self._data.pop(part) self.conn.delete(key) def __contains__(self, part): return part in self._data
[docs]class RedisList(object): """ Attempts to emulate a python `list`, while backing the list in redis. This supports most of the common `list` functions, except as noted. Generally speaking, you won't have to create an instance of this class, however if you are working with a `list` then this is the class you'll get back, not a `list` class. .. note:: Most notably, this is currently missing the sort and reverse functions. """ def __init__(self, key, conn, start=[], reset=False): self._list = [] self.conn = conn self.key = key self.sync() # Haxs I say... if start and not reset: self.extend(start) if start and reset: self.reset() self.extend(start) def __repr__(self): return repr(self._list) def __str__(self): return str(self._list)
[docs] def sync(self): self._list = self.conn.lrange(self.key, 0, -1) self.listToInt()
[docs] def listToInt(self): for elem in range(len(self._list)): try: self._list[elem] = int(self._list[elem]) except: pass
[docs] def append(self, other): self._list.append(other) self.conn.rpush(self.key, other) return self._list
[docs] def prepend(self, other): self._list.insert(0, other) self.conn.lpush(self.key, other)
[docs] def extend(self, other): assert type(other) == list self._list.extend(other) for key in other: self.conn.rpush(self.key, key) return self._list
[docs] def insert(self, index, elem): self._list.insert(index, elem) self.conn.linsert(self.key, 'AFTER', index, elem) return self._list
[docs] def remove(self, elem): self._list.remove(elem) self.conn.lrem(self.key, 1, elem) return self._list
[docs] def pop(self): value = self._list.pop() self.conn.rpop(self.key) return value
[docs] def lpop(self): value = self._list.pop(0) self.conn.lpop(self.key) return value
[docs] def index(self, elem): return self._list.index(elem)
[docs] def count(self): return self._list.count()
[docs] def reset(self): self._list = [] self.conn.delete(self.key)
def __len__(self): return len(self._list) def __getitem__(self, index): return self._list[index] def __setitem__(self, index, value): self._list[index] = value self.conn.lset(self.key, index, value) def __iter__(self): for item in self._list: yield item def __contains__(self, item): return item in self._list def __eq__(self, other): return self._list == other
[docs]class RedisModel(object): """ Emulates a python `object` for the data stored in the collection of keys which match this models, in Redis. Raw data from the redis is stored in `_data` which is a :py:class:`.RedisKeys` instance. This allows for the black magic which makes this class store changes in realtime to redis. This object has a `__repr__` method which can be used with print or logging statements. It will give the id and a representation of the internal `_data` :py:class:`.RedisKeys` for debugging purposes. """ _data = None key = None conn = None namespace = None _protected_items = [] #: Object properties which shouldn't be stored in redis. def __init__(self, namespace=None, key=None, conn=None, **kwargs): """ TODO: Me :param namespace: The key prefix which should be used. :param key: The key or id of this object. :param conn: The redis connection to use. This can also be set on the class instance, or on the module level. :param kwargs: Any additional data which should be stored. This is used for creating a new object in redis. :raised RedisORMException: If no connection or key was supplied, or if there was a problem while creating the :py:class:`.RedisKeys` instance for the interal `_data` """ self.namespace = namespace or "" if not key: raise RedisORMException("No key supplied.") self.key = key self.conn = conn or redis if not self.conn: raise RedisORMException("No connection supplied.") self._data = RedisKeys(conn=self.conn, namespace=self.namespace, key=self.key) if kwargs: for item in kwargs: if item not in self._protected_items and item[0] != "_": setattr(self, item, kwargs[item]) # Hook to run any inherited class code, if needed self.finish_init()
[docs] def finish_init(self): """ A hook called at the end of the main `__init__` to allow for custom inherited classes to customize their init process without having to redo all of the existing int. This should accept nothing besides `self` and nothing should be returned. """ pass
[docs] def get(self, attr, default=None): """ Acts like a `dict.get()` where it will return a default if no matching value was found for the given key. :param attr: The key to look for. If this is found then its value is returned, otherwise `default` is returned. :param default: The default to return if no match was found. """ return self._data.get_default(attr, default)
def _get(self, attr): pro_its = object.__getattribute__(self, "_protected_items") if attr[0] == "_" or attr in pro_its: return object.__getattribute__(self, attr) elif attr in dir(self): return object.__getattribute__(self, attr) else: data = object.__getattribute__(self, "_data") return data[attr] def _set(self, attr, val): pro_its = object.__getattribute__(self, "_protected_items") if attr[0] == "_" or attr in pro_its: return object.__setattr__(self, attr, val) elif hasattr(val, "__call__") or attr in dir(self): return object.__setattr__(self, attr, val) else: data = object.__getattribute__(self, "_data") data[attr] = val return val def __getattr__(self, item): return object.__getattribute__(self, "_get")(item) def __getitem__(self, item): return object.__getattribute__(self, "_get")(item) def __setattr__(self, item, value): return object.__getattribute__(self, "_set")(item, value) def __setitem__(self, item, value): return object.__getattribute__(self, "_set")(item, value) def __delitem__(self, item): """ Deletes the given item from the objects _data dict. """ keys = object.__getattribute__(self, "_data") if item in keys: del(keys[item]) def __contains__(self, item): """ Allows for the use of syntax similar to:: if "blah" in model: This only works with the internal _data, and does not include other properties in the objects namepsace, simply due to the mess that would create, and my lazyness. """ keys = object.__getattribute__(self, "_data") if item in keys: return True return False @classmethod
[docs] def new(cls, id=None, **kwargs): """ Creates a new instance, filling out the models data with the keyword arguments passed, so long as those keywords are not in the protected items array. """ return cls(id=id, **kwargs)
[docs] def delete(self): """ Deletes the current instance, if its in the database (or try). """ self._data.delete() del self
def __repr__(self): """ Allows for the representation of the object, for debugging purposes """ return "<RedisModel.%s at %s with data: %s >" % (self.__class__.__name__, id(self), self._data) @property def protected_items(self): """ Provides a cleaner interface to dynamically add items to the models list of protected functions to not store in the database """ return self._protected_items @protected_items.setter def protected_items(self, value): if type(value) is list: self._protected_items.extend(value) else: assert type(value) is str self._protected_items.append(value) return self._protected_items