Skip to content

Commit fac5cf5

Browse files
committed
Add namespace support
1 parent da13784 commit fac5cf5

File tree

5 files changed

+1732
-0
lines changed

5 files changed

+1732
-0
lines changed

README.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,20 @@ that return Python iterators for convenience: `scan_iter`, `hscan_iter`,
658658
B 2
659659
C 3
660660
661+
Namespacing
662+
^^^^^^^^^^^
663+
664+
.. code-block:: pycon
665+
666+
>>> import redis
667+
>>> r = redis.StrictRedis(host='localhost', port=6379, db=0, namespace="ns:")
668+
>>> # Sets key "ns:foo".
669+
>>> r.set('foo', 'bar')
670+
True
671+
>>> # Gets key "ns:foo".
672+
>>> r.get('foo')
673+
'bar'
674+
661675
Author
662676
^^^^^^
663677

redis/client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
TimeoutError,
2424
WatchError,
2525
)
26+
from redis.namespace import NamespaceWrapper
2627

2728
SYM_EMPTY = b('')
2829

@@ -365,6 +366,8 @@ class StrictRedis(object):
365366
}
366367
)
367368

369+
namespace = None
370+
368371
@classmethod
369372
def from_url(cls, url, db=None, **kwargs):
370373
"""
@@ -1989,6 +1992,23 @@ class Redis(StrictRedis):
19891992
}
19901993
)
19911994

1995+
def __init__(self, *args, **kwargs):
1996+
self.namespace = kwargs.pop('namespace', None)
1997+
return super(Redis, self).__init__(*args, **kwargs)
1998+
1999+
def execute_command(self, *args, **options):
2000+
if self.namespace:
2001+
self.ns = NamespaceWrapper(self.namespace, args)
2002+
args = self.ns.format_args()
2003+
return super(Redis, self).execute_command(*args, **options)
2004+
2005+
def parse_response(self, connection, command_name, **options):
2006+
parent = super(Redis, self)
2007+
response = parent.parse_response(connection, command_name, **options)
2008+
if self.namespace:
2009+
return self.ns.format_response(response)
2010+
return response
2011+
19922012
def pipeline(self, transaction=True, shard_hint=None):
19932013
"""
19942014
Return a new pipeline object that can queue multiple commands for

redis/namespace.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
class NamespaceWrapper():
2+
FORMAT_METHODS = {
3+
"every other": lambda arg_start, l, i: arg_start % 2 == i % 2,
4+
"ignore last": lambda arg_start, l, i: l - 1 != i,
5+
}
6+
RESP_METHODS = {
7+
"mapped_tuple": lambda res: [
8+
tuple(x) if isinstance(x, list) else x for x in res
9+
],
10+
"tuple": lambda res: tuple(res),
11+
"recursive": lambda res: NamespaceWrapper.recursive(res)
12+
}
13+
14+
def __init__(self, namespace, args):
15+
self.namespace = namespace
16+
self.args = list(args)
17+
self.command_name = args[0]
18+
self.cmd = CMDS.get(self.command_name, {})
19+
self.l = len(args) if self.cmd.get('multi', False) else 2
20+
self.arg_start = self.cmd.get('arg_start', 1)
21+
self.method = self.FORMAT_METHODS.get(self.cmd.get('method'))
22+
self.skip = self.cmd.get('skip', [])
23+
24+
def format_args(self):
25+
"""Appends namespace to applicaple args before sending to redis."""
26+
if self.cmd.get('format_args', True) and len(self.args) > 1:
27+
for i in range(self.arg_start, self.l):
28+
arg = self.args[i]
29+
if self.should_format(i, arg):
30+
self.args[i] = self.format_arg(arg)
31+
print(self.args)
32+
return self.args
33+
34+
def should_format(self, i, arg):
35+
return all([
36+
not self.arg_reserved(arg),
37+
self.can_format(arg),
38+
self.valid_method(i),
39+
i not in self.skip,
40+
])
41+
42+
def can_format(self, arg):
43+
return isinstance(arg, (str, bytes))
44+
45+
def valid_method(self, i):
46+
if self.method:
47+
return self.method(self.arg_start, self.l, i)
48+
return True
49+
50+
def arg_reserved(self, arg):
51+
if arg in ['-', '+', "*", "#", "sorted_values"]:
52+
return True
53+
if self.cmd_contains("SCAN") and (arg == "MATCH" or arg == '0'):
54+
return True
55+
if self.cmd_contains('STORE') and arg in ['AGGREGATE', 'MAX', 'MIN']:
56+
return True
57+
return False
58+
59+
def cmd_contains(self, x):
60+
return self.command_name.find(x) > -1
61+
62+
def format_arg(self, arg):
63+
if not isinstance(arg, str):
64+
arg = arg.decode()
65+
if self.command_name.find('LEX') > -1:
66+
return self.format_lex_arg(arg)
67+
return (self.namespace + arg).encode()
68+
69+
def format_lex_arg(self, arg):
70+
for v in ['[', '(']:
71+
if arg.startswith(v):
72+
return arg.replace(v, v + self.namespace)
73+
return (self.namespace + arg).encode()
74+
75+
def format_response(self, response):
76+
"""Removes namespace from responses."""
77+
print(response)
78+
if self.cmd.get('format_response', True):
79+
return self.clean_response(self.remove_namespace(response))
80+
return response
81+
82+
def remove_namespace(self, response, keys=[]):
83+
if isinstance(response, dict) and self.command_name == "SLOWLOG GET":
84+
response['command'] = self.remove_namespace(response['command'])
85+
if isinstance(response, (tuple, list)):
86+
response = [self.remove_namespace(x) for x in response]
87+
try:
88+
if isinstance(response, str):
89+
return response.replace(self.namespace, '', 1)
90+
else:
91+
response = response.decode().replace(self.namespace, '', 1)
92+
return response.encode()
93+
except (AttributeError, TypeError, UnicodeDecodeError):
94+
return response
95+
96+
def clean_response(self, response):
97+
tuple_method = self.RESP_METHODS.get(self.cmd.get('response_method'))
98+
if tuple_method and hasattr(response, '__iter__'):
99+
return tuple_method(response)
100+
return response
101+
102+
@staticmethod
103+
def recursive(l):
104+
if isinstance(l, (list, tuple)):
105+
return tuple(map(NamespaceWrapper.recursive, l))
106+
return l
107+
108+
109+
CMDS = {
110+
'BITOP': {
111+
"multi": True,
112+
"arg_start": 2,
113+
},
114+
'BLPOP': {
115+
"multi": True,
116+
"method": "ignore last",
117+
"response_method": "tuple",
118+
},
119+
'BRPOP': {
120+
"multi": True,
121+
"method": "ignore last",
122+
"response_method": "tuple",
123+
},
124+
'BRPOPLPUSH': {
125+
"multi": True,
126+
"method": "ignore last",
127+
},
128+
'CLIENT GETNAME': {
129+
'format_args': False,
130+
},
131+
'CONFIG GET': {
132+
'format_args': False,
133+
},
134+
'CONFIG SET': {
135+
'format_args': False,
136+
},
137+
'DEL': {
138+
'multi': True,
139+
'format_response': False,
140+
},
141+
'FLUSHDB': {
142+
'format_args': False,
143+
'format_response': False,
144+
},
145+
'INFO': {
146+
'format_args': False,
147+
},
148+
'MGET': {
149+
"multi": True,
150+
},
151+
'MSET': {
152+
"multi": True,
153+
"method": "every other",
154+
},
155+
'MSETNX': {
156+
"multi": True,
157+
"method": "every other",
158+
},
159+
'OBJECT': {
160+
'multi': True,
161+
'format_response': False,
162+
'arg_start': 2,
163+
},
164+
'PFCOUNT': {
165+
"multi": True,
166+
},
167+
'PFMERGE': {
168+
"multi": True,
169+
},
170+
'RENAME': {
171+
"multi": True,
172+
"format_response": False,
173+
},
174+
'RENAMENX': {
175+
"multi": True,
176+
"format_response": False,
177+
},
178+
'RPOPLPUSH': {
179+
"multi": True,
180+
},
181+
'SCAN': {
182+
"multi": True,
183+
"skip": [1],
184+
"response_method": "recursive",
185+
},
186+
'SDIFF': {
187+
"multi": True,
188+
},
189+
'SDIFFSTORE': {
190+
"multi": True,
191+
},
192+
'SINTER': {
193+
'multi': True,
194+
},
195+
'SINTERSTORE': {
196+
"multi": True,
197+
},
198+
'SLOWLOG GET': {
199+
"format_args": False,
200+
"resp_keys": ["command"],
201+
},
202+
'SMOVE': {
203+
"multi": True,
204+
"method": "ignore last",
205+
},
206+
'SORT': {
207+
'multi': True,
208+
"response_method": "mapped_tuple",
209+
},
210+
'SUNION': {
211+
'multi': True,
212+
},
213+
'SUNIONSTORE': {
214+
"multi": True,
215+
},
216+
'ZADD': {
217+
"multi": True,
218+
"method": "every other",
219+
},
220+
'ZINCRBY': {
221+
"multi": True,
222+
},
223+
'ZINTERSTORE': {
224+
"multi": True,
225+
"skip": [2],
226+
},
227+
'ZLEXCOUNT': {
228+
"multi": True,
229+
},
230+
'ZRANGE': {
231+
"response_method": "mapped_tuple",
232+
},
233+
'ZRANGEBYLEX': {
234+
"multi": True,
235+
},
236+
'ZRANGEBYSCORE': {
237+
"response_method": "mapped_tuple",
238+
},
239+
'ZRANK': {
240+
"multi": True,
241+
},
242+
'ZREM': {
243+
"multi": True,
244+
},
245+
'ZREMRANGEBYLEX': {
246+
"multi": True,
247+
},
248+
'ZREMRANGEBYRANK': {
249+
"multi": True,
250+
},
251+
'ZREMRANGEBYSCORE': {
252+
"multi": True,
253+
},
254+
'ZREVRANGE': {
255+
"response_method": "mapped_tuple",
256+
},
257+
'ZREVRANGEBYLEX': {
258+
"multi": True,
259+
},
260+
'ZREVRANGEBYSCORE': {
261+
"response_method": "mapped_tuple",
262+
},
263+
'ZREVRANK': {
264+
"multi": True,
265+
},
266+
'ZSCAN': {
267+
"multi": True,
268+
"response_method": "recursive",
269+
},
270+
'ZSCORE': {
271+
"multi": True,
272+
},
273+
'ZUNIONSTORE': {
274+
"multi": True,
275+
"skip": [2],
276+
},
277+
}

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,8 @@ def r(request, **kwargs):
4444
@pytest.fixture()
4545
def sr(request, **kwargs):
4646
return _get_client(redis.StrictRedis, request, **kwargs)
47+
48+
49+
@pytest.fixture()
50+
def nsr(request, **kwargs):
51+
return _get_client(redis.Redis, request, namespace='namespace', **kwargs)

0 commit comments

Comments
 (0)