Skip to content

Commit 3e0b6dd

Browse files
committed
Initial commit.
0 parents  commit 3e0b6dd

File tree

8 files changed

+274
-0
lines changed

8 files changed

+274
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*
2+
!/**/
3+
!.gitignore
4+
!requirements.txt
5+
!**/*.py
6+
!**/*.md
7+
8+
.env
9+
10+
*.pyc

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# ReJSON Python Client
2+
3+
This is a package that allows storing, updating and querying objects as JSON
4+
documents in a [Redis](https://redis.io) database that is extended with the
5+
[ReJSON module](https://github.com/redislabsmodules/rejson). The package extends
6+
[redis-py](https://github.com/andymccurdy/redis-py)'s interface with ReJSON's
7+
API, and performs on-the-fly serialization/deserialization of objects to/from
8+
JSON.
9+
10+
## Installation
11+
12+
```bash
13+
$ pip install rejson-py
14+
```
15+
16+
## Usage example
17+
18+
```python
19+
from rejson import Client, Path
20+
21+
rj = Client(host='localhost', port=6379)
22+
23+
# Set the key `obj` to some object
24+
rj.JSONSet('obj', Path.rootPath(), {
25+
'answer': 42,
26+
'arr': [None, True, 3.14],
27+
'truth': {
28+
'coord': 'out there'
29+
}
30+
})
31+
32+
# Get something
33+
question = 'Is there anybody... {}?'.format(
34+
rj.JSONGet('obj', Path('.truth.coord'))
35+
)
36+
37+
# Delete something (or perhaps nothing)
38+
rj.JSONDel('obj', Path('.arr[0]'))
39+
40+
# Update something
41+
rj.JSONSet('obj', Path('.answer'), 2.17)
42+
```

rejson/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
`rejson-py is a client for ReJSON
3+
"""
4+
5+
from .client import Client
6+
from .path import Path

rejson/client.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from sys import stdout
2+
import json
3+
from redis import StrictRedis, exceptions
4+
from redis._compat import (b, basestring, bytes, imap, iteritems, iterkeys,
5+
itervalues, izip, long, nativestr, unicode,
6+
safe_unicode)
7+
from .path import Path
8+
9+
def str_path(p):
10+
"Returns the string representation of a path if it is of class Path"
11+
if isinstance(p, Path):
12+
return p.strPath
13+
else:
14+
return p
15+
16+
def bulk_of_jsons(b):
17+
"Replace serialized JSON values with objects in a bulk array response (list)"
18+
for index, item in enumerate(b):
19+
if item is not None:
20+
b[index] = json.loads(item)
21+
return b
22+
23+
class Client(StrictRedis):
24+
"""
25+
Implementation of ReJSON commands
26+
27+
This class provides an interface for ReJSON's commands and performs on-the-fly
28+
serialization/deserialization of objects to/from JSON.
29+
"""
30+
31+
MODULE_INFO = {
32+
'name': 'ReJSON',
33+
'ver': 1
34+
}
35+
36+
MODULE_CALLBACKS = {
37+
'JSON.DEL': lambda r: int(r),
38+
'JSON.GET': lambda r: json.loads(r),
39+
'JSON.MGET': bulk_of_jsons,
40+
'JSON.SET': lambda r: r and nativestr(r) == 'OK',
41+
}
42+
43+
def __init__(self, **kwargs):
44+
super(Client, self).__init__(**kwargs)
45+
46+
self.__checkPrerequirements()
47+
48+
# Inject the callbacks for the module's commands
49+
self.response_callbacks.update(self.MODULE_CALLBACKS)
50+
51+
def __checkPrerequirements(self):
52+
"Checks that the module is ready"
53+
try:
54+
reply = self.execute_command('MODULE', 'LIST')
55+
except exceptions.ResponseError as e:
56+
if e.message.startswith('unknown command'):
57+
raise exceptions.RedisError('Modules are not supported '
58+
'on your Redis server - consider '
59+
'upgrading to a newer version.')
60+
finally:
61+
info = self.MODULE_INFO
62+
for r in reply:
63+
module = dict(zip(r[0::2], r[1::2]))
64+
if info['name'] == module['name'] and \
65+
info['ver'] <= module['ver']:
66+
return
67+
raise exceptions.RedisError('ReJSON is not loaded - follow the '
68+
'instructions at http://rejson.io')
69+
70+
def JSONDel(self, name, path=Path.rootPath()):
71+
"""
72+
Deletes the JSON value stored at key ``name`` under ``path``
73+
"""
74+
return self.execute_command('JSON.DEL', name, str_path(path))
75+
76+
def JSONGet(self, name, *args):
77+
"""
78+
Get the object stored as a JSON value at key ``name``
79+
``args`` is zero or more paths, and defaults to root path
80+
"""
81+
pieces = [name]
82+
if len(args) == 0:
83+
pieces.append(Path.rootPath())
84+
else:
85+
for p in args:
86+
pieces.append(str_path(p))
87+
return self.execute_command('JSON.GET', *pieces)
88+
89+
def JSONMGet(self, path, *args):
90+
"""
91+
Gets the objects stored as a JSON values under ``path`` from
92+
keys ``args``
93+
"""
94+
pieces = []
95+
pieces.extend(args)
96+
pieces.append(str_path(path))
97+
return self.execute_command('JSON.MGET', *pieces)
98+
99+
def JSONSet(self, name, path, obj, nx=False, xx=False):
100+
"""
101+
Set the JSON value at key ``name`` under the ``path`` to ``obj``
102+
``nx`` if set to True, set ``value`` only if it does not exist
103+
``xx`` if set to True, set ``value`` only if it exists
104+
"""
105+
pieces = [name, str_path(path), json.dumps(obj)]
106+
107+
# Handle existential modifiers
108+
if nx and xx:
109+
raise Exception('nx and xx are mutually exclusive: use one, the '
110+
'other or neither - but not both')
111+
elif nx:
112+
pieces.append('NX')
113+
elif xx:
114+
pieces.append('XX')
115+
116+
return self.execute_command('JSON.SET', *pieces)
117+
118+
def JSONType(self, name, path=Path.rootPath()):
119+
"""
120+
Gets the type of the JSON value under ``path`` from key ``name``
121+
"""
122+
return self.execute_command('JSON.TYPE', name, str_path(path))

rejson/path.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class Path(object):
2+
strPath = ''
3+
4+
@staticmethod
5+
def rootPath():
6+
return '.'
7+
8+
def __init__(self, path):
9+
self.strPath = path

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
hiredis==0.2.0
2+
redis==2.10.5

setup.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env python
2+
from setuptools import setup, find_packages
3+
4+
setup(
5+
name='rejson',
6+
version='0.1',
7+
8+
description='ReJSON Python Client',
9+
url='http://github.com/RedisLabs/rejson-py',
10+
packages=find_packages(),
11+
install_requires=['redis', 'hiredis', 'rmtest'],
12+
classifiers=[
13+
'Development Status :: 4 - Beta',
14+
'Intended Audience :: Developers',
15+
'License :: OSI Approved :: BSD License',
16+
'Programming Language :: Python :: 2.7',
17+
'Topic :: Database',
18+
'Topic :: Software Development :: Testing'
19+
]
20+
)

test/test.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import redis
2+
from unittest import TestCase
3+
from rejson import Client, Path
4+
5+
class ReJSONTestCase(TestCase):
6+
7+
def testJSONSetGetDelShouldSucceed(self):
8+
"Test basic JSONSet/Get/Del"
9+
rj = Client()
10+
rj.flushdb()
11+
12+
self.assertTrue(rj.JSONSet('foo', Path.rootPath(), 'bar'))
13+
self.assertEqual('bar', rj.JSONGet('foo'))
14+
self.assertEqual(1, rj.JSONDel('foo'))
15+
self.assertFalse(rj.exists('foo'))
16+
17+
def testMGetShouldSucceed(self):
18+
"Test JSONMGet"
19+
rj = Client()
20+
rj.flushdb()
21+
22+
rj.JSONSet('1', Path.rootPath(), 1)
23+
rj.JSONSet('2', Path.rootPath(), 2)
24+
r = rj.JSONMGet(Path.rootPath(), '1', '2')
25+
e = [1, 2]
26+
self.assertListEqual(e, r)
27+
28+
def testTypeShouldSucceed(self):
29+
"Test JSONType"
30+
rj = Client()
31+
rj.flushdb()
32+
33+
rj.JSONSet('1', Path.rootPath(), 1)
34+
self.assertEqual('integer', rj.JSONType('1'))
35+
36+
def testUsageExampleShouldSucceed(self):
37+
"Test the usage example"
38+
39+
# Create a new rejson-py client
40+
rj = Client(host='localhost', port=6379)
41+
42+
# Set the key `obj` to some object
43+
rj.JSONSet('obj', Path.rootPath(), {
44+
'answer': 42,
45+
'arr': [None, True, 3.14],
46+
'truth': {
47+
'coord': 'out there'
48+
}
49+
})
50+
51+
# Get something
52+
question = 'Is there anybody... {}?'.format(
53+
rj.JSONGet('obj', Path('.truth.coord'))
54+
)
55+
56+
# Delete something (or perhaps nothing)
57+
rj.JSONDel('obj', Path('.arr[0]'))
58+
59+
# Update something
60+
rj.JSONSet('obj', Path('.answer'), 2.17)
61+
62+
if __name__ == '__main__':
63+
unittest.main()

0 commit comments

Comments
 (0)