redis-natives-py
A thin abstraction layer on top of redis-py that exposes Redis entities as native Python datatypes. Simple, plain but powerful. No ORMing or model-messing -- this isn't the real purpose of high performance key-value-stores like Redis.
Available datatypes
- Primitive* (string)
- Set*
- ZSet
- Dict*
- List*
- Sequence
Every datatype instance is directly bound its accordant Redis entity. No caching. Changes are reflected immediately to the database and thereby thread-safe and guaranteed to be consistent. Furthermore all datatypes marked with * implement (almost) exactly the same interface as their builtin relatives. That allows for a whole range of new use-cases which don't have to be directly connected to the persistence/database layer.
Features
- Bound instances; no caching; changes are immediately reflected to the dstore
- Support for key namespaces along with other utilities (see RedisNativeFactory and annotations)
- Most datatypes implement same interface as builtin pendants -- uncomplicated integration in existing systems
FAQ
What about performance in general?
I wrote redis-natives-py with performance in mind. I tried to avoid expensive operations where I could what resulted in an optimized and refactored piece of code that tries to exploit Redis capabilities as best and efficient as possible while keeping its memory footprint as small as possible. Reliable profiling result and further code improvements will follow.
When you have questions or problems with redis-natives-py please contact me via email or file a bug/ticket in the issue tracker.
Examples - Datatypes
Though redis-natives-py bases on redis-py it is
assumed that you already have it installed and made it working properly. I will omit the following
two lines in every example so don't wonder where rClient
and rnatives
are coming from.
from redis import Redis
import redis_natives_py as rnatives
# Our Redis client instance
rClient = Redis()
Functionality shared by all datatypes
All datatypes share the following methods/properties that allow you to perform Redis-specific tasks directly on the entity/instance you want:
- Getter/setter property called
key
. Changes will be reflected to the store immediately. type()
return the Redis-internal datatype name of the associated valuemove(redisClient/id)
moves the key into the database currently selected in the givenRedis
instance or described by integerid
rename(newKey, overwrite=True)
renames the current entity-key tonewKey
overwriting an exisiting key with the same name ifoverwrite
isTrue
- Property
expiration
yields the remaining time in seconds until the entity will be destroyed when it was marked as volatile before let_expire(nSecs)
marks the entity as volatile and determines that it will be automatically destroyed innSecs
let_expire_at(timestamp)
same as line above, but this time it will be destroyed at given timetimestamp
Primitive
You can work with Primitive
exactly as you'd like to with builtin Strings. Primitives expose the
same interface as String plus something more.
from redis_natives import Primitive
myTweet = Primitive(rClient, "message:123", "I love PlusFM!")
myTweet += " P.s.: Bassdrive too!"
print "What did I say? " + myTweet.upper()
# >> I LOVE PLUSFM! P.S.: BASSDRIVE TOO!
When working with Primitive integers there are Primitive.incr(by=1)
and Primitive.decr(by=1)
for incr-/decrementing values.
from rn.datatypes import Primitive
myCounter = Primitive(rClient, "counter:messages", 0)
myCounter.incr()
myCounter.incr(5)
myCounter.decr(2)
# >> 4
List
from redis_natives import List
list = List(redis, 'some_list', type=int)
list.append(1)
list.append(2)
list.append(3)
list.append(4)
list[0:2] # [1, 2]
list[-2:] # [3, 4]
Set
You can work with Set
exactly as you'd like to with builtin Sets. Set operations like difference
and
intersection
are of course performed completely on the datastore-side. You even can pass an arbitrary number of Python sets
and operate with them.
# No need to give an example on native Python Sets
Special methods
Set
has an additional method called grab()
that simply returns a random element from the Set
.
Restrictions
At the moment Set
doesn't support the methods issubset(*others)
and issuperset(other)
. But I will add them
soon.
ZSet
A special datatype is the ZSet
-- an ordered set. The main characteristic is the concept of a
score associated to every set element.
from rn.datatypes import ZSet
zset = ZSet(rClient, "rank:messages")
zset.add("message:123", 0)
zset.incr_score("message:123")
zset.rank_of("message:123")
# > 1
And when you want to query the 10 most popular messages:
from random import randint
from rn.datatypes import ZSet
zset = ZSet(rClient, "rank:messages")
for i in range(20):
zset.add("message:%s" % i, 0)
for i in range(20):
zset.incr_score("message:%s" % randint(0, 19))
# Will return the Top-10
zset.range_by_rank(0, 10, ZOrder.DESC)
Getting all values in a zset:
zset.data
Returning slices:
zset[0:3] # get the first three items by rank
Replacing an item:
# replace the value of the second ranked item, keeping its current score
zset[1] = 'some value'
Dict
You can work with Dict
exactly as you'd like to with builtin Dicts.
# No need to give an example on native Python Dicts
Special methods
Dict
has an additional method called incr(key, by=1)
that increments the value associated to key
by a given int
.
Sequence
The Sequence
datatype implements all functions of Redis list
datatypes.
Compared to List
, a Sequence
doesn't try to meme a native
list
datatype but instead exposes all native functionalities of Redis
for working with list datatypes.
A typical use-case where this functionality is needed are f.e. FIFO/LIFO processings. (stacks/queues)
lookupQueue = Sequence(rClient, "ipLookups")
lookupQueue.push_head("123.123.123.123")
lookupQueue.push_head("124.124.124.124")
lookupQueue.pop_tail()
# > 123.123.123.123
Examples - Annotations & RedisNativeFactory
When you work with with redis_natives
it might become odd to everytime pass in an instance of redis.Redis
or
to keep track created keys. Even more when you work with pseudo-namespaces (f.e. "global:counter:message") and construct
the key names in advance. That's why I introduced annotations
that can be applied to a custom RedisNativeFactory
subclass.
RedisNativeFactory
Creates instances of Redis natives with a preset Redis
client and one or more optional annotation hooks. Normally
you won't use RedisNativeFactory
directly, instead create a custom subclass for every entity type requirement you
have. Create as many subclasses as you want/need whereby you can annotate each subclass individually.
Note 1: You should override the inherited before_create
and after_create
lists with new ones, otherwise the
annotations you add will be applied to all RedisNativeFactory
subclasses.
Note 2: RedisNativeFactory
is implemented as Singleton. Instead of requesting the shared instance over and over again, keep a reference to
the returned instance (basically a constructor function) under an appropriately named variable.
@namespaced(ns, sep=":")
To implicitly embed created keys in one or more namespaces, you use the annotation called namespaced(ns, sep=":")
.
They're applied using the decorator syntax to you custom RedisNativeFactory
subclass. Namespaces are constructed from
top to bottowm whereat you can combine as many namespaces as you like.
from rn.natives import RedisNativeFactory
from rn.datatypes import Primitive
from rn.annotations import namespaced
@namespaced("a")
@namespaced("b")
@namespaced("c")
class FooFactory(RedisNativeFactory):
client = rClient
before_create = []
after_create = []
fk = FooFactory().Primitive("fooKey", "barValue")
print fk
# > barValue
print fk.key
# > a:b:c:fooKey
@temporary(after=None, at=None)
To implicity mark all keys created by a specific RedisNativeFactory
as volatile, you use the annotation called @temporary(after=None, at=None)
.
You can either specify if a entity should be automatically destroyed after
a given number of seconds or at
a given timestamp.
Note Redis' special handling of volatile keys
from time import sleep
from rn.natives import RedisNativeFactory
from rn.datatypes import Primitive
from rn.annotations import temporary
@temporary(after=10)
class FooFactory(RedisNativeFactory):
client = rClient
before_create = []
after_create = []
fk = FooFactory().Primitive("fooKey", "Gone in 10 seconds")
print fk.expiration
# > 10
sleep(10)
print rClient.exists(fk.key)
# > False
print fk
# > TypeError: __repr__ returned non-string (type NoneType)
@indexed(idxSet)
When you keep reversed/additional indexes of certain entities the annotation called indexed(idxSet)
will be handy for you.
For every entity created by the annotated RedisNativeFactory
it will automatically add the entity's key to the given Redis Set
idxSet
.
from rn.natives import RedisNativeFactory
from rn.datatypes import Primitive, Set
from rn.annotations import indexed
myIndex = Set(rClient, "global:index:createdToday")
@indexed(myIndex)
class FooFactory(RedisNativeFactory):
client = rClient
before_create = []
after_create = []
FooFactory().Primitive("fooKey", "I'm listed in global:index:createdToday too!")
print myIndex
# > set(["fooKey"])
@incremental(rPrim)
When you annotate a RedisNativeFactory
with @incremental(rPrim)
the given Redis Primitive
rPrim
will be incremented
by value 1 for every entity created.
from rn.natives import RedisNativeFactory
from rn.datatypes import Primitive
from rn.annotations import incremental
myCounter = Primitive(rClient, "global:counter:messages")
@incremental(myCounter)
class FooFactory(RedisNativeFactory):
client = rClient
before_create = []
after_create = []
ff = FooFactory().Primitive
for i in range(100):
ff("id-%s" % i, "msgbody-%s" % i)
print myCounter
# > 100
@autonamed(obj)
Instead of passing a key-name for every entity to the Datatype constructor, you pass an arbitrary object that is
representable as str
and everytime an entity creation is triggered, obj
is asked to return a str
representation of itself that will be used as key name.
from rn.natives import RedisNativeFactory
from rn.datatypes import Primitive
from rn.annotations import incremental, autonamed
myCounter = Primitive(rClient, "global:counter:messages", 0)
@incremental(myCounter)
@autonamed(myCounter)
class FooFactory(RedisNativeFactory):
client = rClient
before_create = []
after_create = []
ff = FooFactory().Primitive
for i in range(100):
latestKey = ff("id-", "msgbody-%s" % i)
print myCounter
# > 100
print latestKey.key + ": " + latestKey
# > id-100: msgbody-99
Note that latestKey.key
has 100 as suffix just because the annotation incremental
was applied before autonamed
. Switch
their order, and latestKey.key
will have the same suffix as the message body.
Demo: URL shorter service (Will follow soon)
Interesting demo project that shows how to use redis-natives-py together with bottle.py in order to write a full-fledged URL shortener service that even offers hit tracking and statistics.