We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
sessions using redis broken
As per discussion on; https://groups.google.com/forum/?fromgroups#!topic/web2py/z1ee025ghRU

There are actually two issues.
1) adding () after the decode on line 185 of redis_session.py prevents a traceback occurring with messsage - AttributeError: 'builtin_function_or_method' object has no attribute 'decode'

2) web2py redis session code does not appear to retrieve the key successfully as it creates a new session for each page hit, ignoring the previous sessions created.  This makes the session in redis functionality unusable.  In other words, it creates the sessions (which can be seen in redis) but does not use them again and creates a new session per request.

python 3
python-redis library 2.x and 3.x
web2py 2.18.5 and master as at 31 oct 2019

Verified by another user as well (Leonel)



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gluon/contrib/redis_session.py`
Content:
```
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 Developed by niphlod@gmail.com
5 License MIT/BSD/GPL
6 
7 Redis-backed sessions
8 """
9 
10 import logging
11 from threading import Lock
12 from gluon import current
13 from gluon.storage import Storage
14 from gluon.contrib.redis_utils import acquire_lock, release_lock
15 from gluon.contrib.redis_utils import register_release_lock
16 from gluon._compat import to_native
17 
18 logger = logging.getLogger("web2py.session.redis")
19 
20 locker = Lock()
21 
22 
23 def RedisSession(redis_conn, session_expiry=False, with_lock=False, db=None):
24     """
25     Usage example: put in models::
26 
27         from gluon.contrib.redis_utils import RConn
28         rconn = RConn()
29         from gluon.contrib.redis_session import RedisSession
30         sessiondb = RedisSession(redis_conn=rconn, with_lock=True, session_expiry=False)
31         session.connect(request, response, db = sessiondb)
32 
33     Args:
34         redis_conn: a redis-like connection object
35         with_lock: prevent concurrent modifications to the same session
36         session_expiry: delete automatically sessions after n seconds
37                         (still need to run sessions2trash.py every 1M sessions
38                         or so)
39 
40     Simple slip-in storage for session
41     """
42 
43     locker.acquire()
44     try:
45         instance_name = 'redis_instance_' + current.request.application
46         if not hasattr(RedisSession, instance_name):
47             setattr(RedisSession, instance_name,
48                     RedisClient(redis_conn, session_expiry=session_expiry, with_lock=with_lock))
49         return getattr(RedisSession, instance_name)
50     finally:
51         locker.release()
52 
53 
54 class RedisClient(object):
55 
56     def __init__(self, redis_conn, session_expiry=False, with_lock=False):
57         self.r_server = redis_conn
58         self._release_script = register_release_lock(self.r_server)
59         self.tablename = None
60         self.session_expiry = session_expiry
61         self.with_lock = with_lock
62 
63     def get(self, what, default):
64         return self.tablename
65 
66     def Field(self, fieldname, type='string', length=None, default=None,
67               required=False, requires=None):
68         return None
69 
70     def define_table(self, tablename, *fields, **args):
71         if not self.tablename:
72             self.tablename = MockTable(
73                 self, self.r_server, tablename, self.session_expiry,
74                 self.with_lock)
75         return self.tablename
76 
77     def __getitem__(self, key):
78         return self.tablename
79 
80     def __call__(self, where=''):
81         q = self.tablename.query
82         return q
83 
84     def commit(self):
85         # this is only called by session2trash.py
86         pass
87 
88 
89 class MockTable(object):
90 
91     def __init__(self, db, r_server, tablename, session_expiry, with_lock=False):
92         # here self.db is the RedisClient instance
93         self.db = db
94         self.tablename = tablename
95         # set the namespace for sessions of this app
96         self.keyprefix = 'w2p:sess:%s' % tablename.replace('web2py_session_', '')
97         # fast auto-increment id (needed for session handling)
98         self.serial = "%s:serial" % self.keyprefix
99         # index of all the session keys of this app
100         self.id_idx = "%s:id_idx" % self.keyprefix
101         # remember the session_expiry setting
102         self.session_expiry = session_expiry
103         self.with_lock = with_lock
104 
105     def __call__(self, record_id, unique_key=None):
106         # Support DAL shortcut query: table(record_id)
107 
108         # This will call the __getattr__ below
109         # returning a MockQuery
110         q = self.id
111 
112         # Instructs MockQuery, to behave as db(table.id == record_id)
113         q.op = 'eq'
114         q.value = record_id
115         q.unique_key = unique_key
116 
117         row = q.select()
118         return row[0] if row else Storage()
119 
120     def __getattr__(self, key):
121         if key == 'id':
122             # return a fake query. We need to query it just by id for normal operations
123             self.query = MockQuery(
124                 field='id', db=self.db,
125                 prefix=self.keyprefix, session_expiry=self.session_expiry,
126                 with_lock=self.with_lock, unique_key=self.unique_key
127             )
128             return self.query
129         elif key == '_db':
130             # needed because of the calls in sessions2trash.py and globals.py
131             return self.db
132 
133     def insert(self, **kwargs):
134         # usually kwargs would be a Storage with several keys:
135         # 'locked', 'client_ip','created_datetime','modified_datetime'
136         # 'unique_key', 'session_data'
137         # retrieve a new key
138         newid = str(self.db.r_server.incr(self.serial))
139         key = self.keyprefix + ':' + newid
140         if self.with_lock:
141             key_lock = key + ':lock'
142             acquire_lock(self.db.r_server, key_lock, newid)
143         with self.db.r_server.pipeline() as pipe:
144             # add it to the index
145             pipe.sadd(self.id_idx, key)
146             # set a hash key with the Storage
147             pipe.hmset(key, kwargs)
148             if self.session_expiry:
149                 pipe.expire(key, self.session_expiry)
150             pipe.execute()
151         if self.with_lock:
152             release_lock(self.db, key_lock, newid)
153         return newid
154 
155 
156 class MockQuery(object):
157     """a fake Query object that supports querying by id
158        and listing all keys. No other operation is supported
159     """
160     def __init__(self, field=None, db=None, prefix=None, session_expiry=False,
161                  with_lock=False, unique_key=None):
162         self.field = field
163         self.value = None
164         self.db = db
165         self.keyprefix = prefix
166         self.op = None
167         self.session_expiry = session_expiry
168         self.with_lock = with_lock
169         self.unique_key = unique_key
170 
171     def __eq__(self, value, op='eq'):
172         self.value = value
173         self.op = op
174 
175     def __gt__(self, value, op='ge'):
176         self.value = value
177         self.op = op
178 
179     def select(self):
180         if self.op == 'eq' and self.field == 'id' and self.value:
181             # means that someone wants to retrieve the key self.value
182             key = self.keyprefix + ':' + str(self.value)
183             if self.with_lock:
184                 acquire_lock(self.db.r_server, key + ':lock', self.value, 2)
185             rtn = {to_native(k.decode): v for k, v in self.db.r_server.hgetall(key).items()}
186             if rtn:
187                 if self.unique_key:
188                     # make sure the id and unique_key are correct
189                     if rtn['unique_key'] == to_native(self.unique_key):
190                         rtn['update_record'] = self.update  # update record support
191                     else:
192                         rtn = None
193             return [Storage(rtn)] if rtn else []
194         elif self.op == 'ge' and self.field == 'id' and self.value == 0:
195             # means that someone wants the complete list
196             rtn = []
197             id_idx = "%s:id_idx" % self.keyprefix
198             # find all session keys of this app
199             allkeys = self.db.r_server.smembers(id_idx)
200             for sess in allkeys:
201                 val = self.db.r_server.hgetall(sess)
202                 if not val:
203                     if self.session_expiry:
204                         # clean up the idx, because the key expired
205                         self.db.r_server.srem(id_idx, sess)
206                     continue
207                 val = Storage(val)
208                 # add a delete_record method (necessary for sessions2trash.py)
209                 val.delete_record = RecordDeleter(
210                     self.db, sess, self.keyprefix)
211                 rtn.append(val)
212             return rtn
213         else:
214             raise Exception("Operation not supported")
215 
216     def update(self, **kwargs):
217         # means that the session has been found and needs an update
218         if self.op == 'eq' and self.field == 'id' and self.value:
219             key = self.keyprefix + ':' + str(self.value)
220             if not self.db.r_server.exists(key):
221                 return None
222             with self.db.r_server.pipeline() as pipe:
223                 pipe.hmset(key, kwargs)
224                 if self.session_expiry:
225                     pipe.expire(key, self.session_expiry)
226                 rtn = pipe.execute()[0]
227             if self.with_lock:
228                 release_lock(self.db, key + ':lock', self.value)
229             return rtn
230 
231     def delete(self, **kwargs):
232         # means that we want this session to be deleted
233         if self.op == 'eq' and self.field == 'id' and self.value:
234             id_idx = "%s:id_idx" % self.keyprefix
235             key = self.keyprefix + ':' + str(self.value)
236             with self.db.r_server.pipeline() as pipe:
237                 pipe.delete(key)
238                 pipe.srem(id_idx, key)
239                 rtn = pipe.execute()
240             return rtn[1]
241 
242 
243 class RecordDeleter(object):
244     """Dumb record deleter to support sessions2trash.py"""
245 
246     def __init__(self, db, key, keyprefix):
247         self.db, self.key, self.keyprefix = db, key, keyprefix
248 
249     def __call__(self):
250         id_idx = "%s:id_idx" % self.keyprefix
251         # remove from the index
252         self.db.r_server.srem(id_idx, self.key)
253         # remove the key itself
254         self.db.r_server.delete(self.key)
255
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/gluon/contrib/redis_session.py b/gluon/contrib/redis_session.py
--- a/gluon/contrib/redis_session.py
+++ b/gluon/contrib/redis_session.py
@@ -14,6 +14,7 @@
 from gluon.contrib.redis_utils import acquire_lock, release_lock
 from gluon.contrib.redis_utils import register_release_lock
 from gluon._compat import to_native
+from datetime import datetime
 
 logger = logging.getLogger("web2py.session.redis")
 
@@ -65,13 +66,13 @@
 
     def Field(self, fieldname, type='string', length=None, default=None,
               required=False, requires=None):
-        return None
+        return fieldname, type
 
     def define_table(self, tablename, *fields, **args):
         if not self.tablename:
             self.tablename = MockTable(
                 self, self.r_server, tablename, self.session_expiry,
-                self.with_lock)
+                with_lock=self.with_lock, fields=fields)
         return self.tablename
 
     def __getitem__(self, key):
@@ -85,10 +86,26 @@
         # this is only called by session2trash.py
         pass
 
+    def convert_dict_string(self, dict_string):
+        fields = self.tablename.fields
+        typed_dict = dict()
+        converters = {
+            'boolean': lambda x: 1 if x.decode() == '1' else 0,
+            'blob': lambda x: x,
+        }
+        for field, ftype in fields:
+            if field not in dict_string:
+                continue
+            if ftype in converters:
+                typed_dict[field] = converters[ftype](dict_string[field])
+            else:
+                typed_dict[field] = dict_string[field].decode()
+        return typed_dict
+
 
 class MockTable(object):
 
-    def __init__(self, db, r_server, tablename, session_expiry, with_lock=False):
+    def __init__(self, db, r_server, tablename, session_expiry, with_lock=False, fields=None):
         # here self.db is the RedisClient instance
         self.db = db
         self.tablename = tablename
@@ -101,6 +118,7 @@
         # remember the session_expiry setting
         self.session_expiry = session_expiry
         self.with_lock = with_lock
+        self.fields = fields if fields is not None else []
 
     def __call__(self, record_id, unique_key=None):
         # Support DAL shortcut query: table(record_id)
@@ -172,7 +190,11 @@
         self.value = value
         self.op = op
 
-    def __gt__(self, value, op='ge'):
+    def __ge__(self, value, op='ge'):
+        self.value = value
+        self.op = op
+
+    def __gt__(self, value, op='gt'):
         self.value = value
         self.op = op
 
@@ -182,7 +204,7 @@
             key = self.keyprefix + ':' + str(self.value)
             if self.with_lock:
                 acquire_lock(self.db.r_server, key + ':lock', self.value, 2)
-            rtn = {to_native(k.decode): v for k, v in self.db.r_server.hgetall(key).items()}
+            rtn = {to_native(k): v for k, v in self.db.r_server.hgetall(key).items()}
             if rtn:
                 if self.unique_key:
                     # make sure the id and unique_key are correct
@@ -190,8 +212,8 @@
                         rtn['update_record'] = self.update  # update record support
                     else:
                         rtn = None
-            return [Storage(rtn)] if rtn else []
-        elif self.op == 'ge' and self.field == 'id' and self.value == 0:
+            return [Storage(self.db.convert_dict_string(rtn))] if rtn else []
+        elif self.op in ('ge', 'gt') and self.field == 'id' and self.value == 0:
             # means that someone wants the complete list
             rtn = []
             id_idx = "%s:id_idx" % self.keyprefix
@@ -204,7 +226,7 @@
                         # clean up the idx, because the key expired
                         self.db.r_server.srem(id_idx, sess)
                     continue
-                val = Storage(val)
+                val = Storage(self.db.convert_dict_string(val))
                 # add a delete_record method (necessary for sessions2trash.py)
                 val.delete_record = RecordDeleter(
                     self.db, sess, self.keyprefix)

{"golden_diff": "diff --git a/gluon/contrib/redis_session.py b/gluon/contrib/redis_session.py\n--- a/gluon/contrib/redis_session.py\n+++ b/gluon/contrib/redis_session.py\n@@ -14,6 +14,7 @@\n from gluon.contrib.redis_utils import acquire_lock, release_lock\n from gluon.contrib.redis_utils import register_release_lock\n from gluon._compat import to_native\n+from datetime import datetime\n \n logger = logging.getLogger(\"web2py.session.redis\")\n \n@@ -65,13 +66,13 @@\n \n     def Field(self, fieldname, type='string', length=None, default=None,\n               required=False, requires=None):\n-        return None\n+        return fieldname, type\n \n     def define_table(self, tablename, *fields, **args):\n         if not self.tablename:\n             self.tablename = MockTable(\n                 self, self.r_server, tablename, self.session_expiry,\n-                self.with_lock)\n+                with_lock=self.with_lock, fields=fields)\n         return self.tablename\n \n     def __getitem__(self, key):\n@@ -85,10 +86,26 @@\n         # this is only called by session2trash.py\n         pass\n \n+    def convert_dict_string(self, dict_string):\n+        fields = self.tablename.fields\n+        typed_dict = dict()\n+        converters = {\n+            'boolean': lambda x: 1 if x.decode() == '1' else 0,\n+            'blob': lambda x: x,\n+        }\n+        for field, ftype in fields:\n+            if field not in dict_string:\n+                continue\n+            if ftype in converters:\n+                typed_dict[field] = converters[ftype](dict_string[field])\n+            else:\n+                typed_dict[field] = dict_string[field].decode()\n+        return typed_dict\n+\n \n class MockTable(object):\n \n-    def __init__(self, db, r_server, tablename, session_expiry, with_lock=False):\n+    def __init__(self, db, r_server, tablename, session_expiry, with_lock=False, fields=None):\n         # here self.db is the RedisClient instance\n         self.db = db\n         self.tablename = tablename\n@@ -101,6 +118,7 @@\n         # remember the session_expiry setting\n         self.session_expiry = session_expiry\n         self.with_lock = with_lock\n+        self.fields = fields if fields is not None else []\n \n     def __call__(self, record_id, unique_key=None):\n         # Support DAL shortcut query: table(record_id)\n@@ -172,7 +190,11 @@\n         self.value = value\n         self.op = op\n \n-    def __gt__(self, value, op='ge'):\n+    def __ge__(self, value, op='ge'):\n+        self.value = value\n+        self.op = op\n+\n+    def __gt__(self, value, op='gt'):\n         self.value = value\n         self.op = op\n \n@@ -182,7 +204,7 @@\n             key = self.keyprefix + ':' + str(self.value)\n             if self.with_lock:\n                 acquire_lock(self.db.r_server, key + ':lock', self.value, 2)\n-            rtn = {to_native(k.decode): v for k, v in self.db.r_server.hgetall(key).items()}\n+            rtn = {to_native(k): v for k, v in self.db.r_server.hgetall(key).items()}\n             if rtn:\n                 if self.unique_key:\n                     # make sure the id and unique_key are correct\n@@ -190,8 +212,8 @@\n                         rtn['update_record'] = self.update  # update record support\n                     else:\n                         rtn = None\n-            return [Storage(rtn)] if rtn else []\n-        elif self.op == 'ge' and self.field == 'id' and self.value == 0:\n+            return [Storage(self.db.convert_dict_string(rtn))] if rtn else []\n+        elif self.op in ('ge', 'gt') and self.field == 'id' and self.value == 0:\n             # means that someone wants the complete list\n             rtn = []\n             id_idx = \"%s:id_idx\" % self.keyprefix\n@@ -204,7 +226,7 @@\n                         # clean up the idx, because the key expired\n                         self.db.r_server.srem(id_idx, sess)\n                     continue\n-                val = Storage(val)\n+                val = Storage(self.db.convert_dict_string(val))\n                 # add a delete_record method (necessary for sessions2trash.py)\n                 val.delete_record = RecordDeleter(\n                     self.db, sess, self.keyprefix)\n", "issue": "sessions using redis broken\nAs per discussion on; https://groups.google.com/forum/?fromgroups#!topic/web2py/z1ee025ghRU\r\n\r\nThere are actually two issues.\r\n1) adding () after the decode on line 185 of redis_session.py prevents a traceback occurring with messsage - AttributeError: 'builtin_function_or_method' object has no attribute 'decode'\r\n\r\n2) web2py redis session code does not appear to retrieve the key successfully as it creates a new session for each page hit, ignoring the previous sessions created.  This makes the session in redis functionality unusable.  In other words, it creates the sessions (which can be seen in redis) but does not use them again and creates a new session per request.\r\n\r\npython 3\r\npython-redis library 2.x and 3.x\r\nweb2py 2.18.5 and master as at 31 oct 2019\r\n\r\nVerified by another user as well (Leonel)\r\n\r\n\n", "before_files": [{"content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\"\"\"\nDeveloped by niphlod@gmail.com\nLicense MIT/BSD/GPL\n\nRedis-backed sessions\n\"\"\"\n\nimport logging\nfrom threading import Lock\nfrom gluon import current\nfrom gluon.storage import Storage\nfrom gluon.contrib.redis_utils import acquire_lock, release_lock\nfrom gluon.contrib.redis_utils import register_release_lock\nfrom gluon._compat import to_native\n\nlogger = logging.getLogger(\"web2py.session.redis\")\n\nlocker = Lock()\n\n\ndef RedisSession(redis_conn, session_expiry=False, with_lock=False, db=None):\n    \"\"\"\n    Usage example: put in models::\n\n        from gluon.contrib.redis_utils import RConn\n        rconn = RConn()\n        from gluon.contrib.redis_session import RedisSession\n        sessiondb = RedisSession(redis_conn=rconn, with_lock=True, session_expiry=False)\n        session.connect(request, response, db = sessiondb)\n\n    Args:\n        redis_conn: a redis-like connection object\n        with_lock: prevent concurrent modifications to the same session\n        session_expiry: delete automatically sessions after n seconds\n                        (still need to run sessions2trash.py every 1M sessions\n                        or so)\n\n    Simple slip-in storage for session\n    \"\"\"\n\n    locker.acquire()\n    try:\n        instance_name = 'redis_instance_' + current.request.application\n        if not hasattr(RedisSession, instance_name):\n            setattr(RedisSession, instance_name,\n                    RedisClient(redis_conn, session_expiry=session_expiry, with_lock=with_lock))\n        return getattr(RedisSession, instance_name)\n    finally:\n        locker.release()\n\n\nclass RedisClient(object):\n\n    def __init__(self, redis_conn, session_expiry=False, with_lock=False):\n        self.r_server = redis_conn\n        self._release_script = register_release_lock(self.r_server)\n        self.tablename = None\n        self.session_expiry = session_expiry\n        self.with_lock = with_lock\n\n    def get(self, what, default):\n        return self.tablename\n\n    def Field(self, fieldname, type='string', length=None, default=None,\n              required=False, requires=None):\n        return None\n\n    def define_table(self, tablename, *fields, **args):\n        if not self.tablename:\n            self.tablename = MockTable(\n                self, self.r_server, tablename, self.session_expiry,\n                self.with_lock)\n        return self.tablename\n\n    def __getitem__(self, key):\n        return self.tablename\n\n    def __call__(self, where=''):\n        q = self.tablename.query\n        return q\n\n    def commit(self):\n        # this is only called by session2trash.py\n        pass\n\n\nclass MockTable(object):\n\n    def __init__(self, db, r_server, tablename, session_expiry, with_lock=False):\n        # here self.db is the RedisClient instance\n        self.db = db\n        self.tablename = tablename\n        # set the namespace for sessions of this app\n        self.keyprefix = 'w2p:sess:%s' % tablename.replace('web2py_session_', '')\n        # fast auto-increment id (needed for session handling)\n        self.serial = \"%s:serial\" % self.keyprefix\n        # index of all the session keys of this app\n        self.id_idx = \"%s:id_idx\" % self.keyprefix\n        # remember the session_expiry setting\n        self.session_expiry = session_expiry\n        self.with_lock = with_lock\n\n    def __call__(self, record_id, unique_key=None):\n        # Support DAL shortcut query: table(record_id)\n\n        # This will call the __getattr__ below\n        # returning a MockQuery\n        q = self.id\n\n        # Instructs MockQuery, to behave as db(table.id == record_id)\n        q.op = 'eq'\n        q.value = record_id\n        q.unique_key = unique_key\n\n        row = q.select()\n        return row[0] if row else Storage()\n\n    def __getattr__(self, key):\n        if key == 'id':\n            # return a fake query. We need to query it just by id for normal operations\n            self.query = MockQuery(\n                field='id', db=self.db,\n                prefix=self.keyprefix, session_expiry=self.session_expiry,\n                with_lock=self.with_lock, unique_key=self.unique_key\n            )\n            return self.query\n        elif key == '_db':\n            # needed because of the calls in sessions2trash.py and globals.py\n            return self.db\n\n    def insert(self, **kwargs):\n        # usually kwargs would be a Storage with several keys:\n        # 'locked', 'client_ip','created_datetime','modified_datetime'\n        # 'unique_key', 'session_data'\n        # retrieve a new key\n        newid = str(self.db.r_server.incr(self.serial))\n        key = self.keyprefix + ':' + newid\n        if self.with_lock:\n            key_lock = key + ':lock'\n            acquire_lock(self.db.r_server, key_lock, newid)\n        with self.db.r_server.pipeline() as pipe:\n            # add it to the index\n            pipe.sadd(self.id_idx, key)\n            # set a hash key with the Storage\n            pipe.hmset(key, kwargs)\n            if self.session_expiry:\n                pipe.expire(key, self.session_expiry)\n            pipe.execute()\n        if self.with_lock:\n            release_lock(self.db, key_lock, newid)\n        return newid\n\n\nclass MockQuery(object):\n    \"\"\"a fake Query object that supports querying by id\n       and listing all keys. No other operation is supported\n    \"\"\"\n    def __init__(self, field=None, db=None, prefix=None, session_expiry=False,\n                 with_lock=False, unique_key=None):\n        self.field = field\n        self.value = None\n        self.db = db\n        self.keyprefix = prefix\n        self.op = None\n        self.session_expiry = session_expiry\n        self.with_lock = with_lock\n        self.unique_key = unique_key\n\n    def __eq__(self, value, op='eq'):\n        self.value = value\n        self.op = op\n\n    def __gt__(self, value, op='ge'):\n        self.value = value\n        self.op = op\n\n    def select(self):\n        if self.op == 'eq' and self.field == 'id' and self.value:\n            # means that someone wants to retrieve the key self.value\n            key = self.keyprefix + ':' + str(self.value)\n            if self.with_lock:\n                acquire_lock(self.db.r_server, key + ':lock', self.value, 2)\n            rtn = {to_native(k.decode): v for k, v in self.db.r_server.hgetall(key).items()}\n            if rtn:\n                if self.unique_key:\n                    # make sure the id and unique_key are correct\n                    if rtn['unique_key'] == to_native(self.unique_key):\n                        rtn['update_record'] = self.update  # update record support\n                    else:\n                        rtn = None\n            return [Storage(rtn)] if rtn else []\n        elif self.op == 'ge' and self.field == 'id' and self.value == 0:\n            # means that someone wants the complete list\n            rtn = []\n            id_idx = \"%s:id_idx\" % self.keyprefix\n            # find all session keys of this app\n            allkeys = self.db.r_server.smembers(id_idx)\n            for sess in allkeys:\n                val = self.db.r_server.hgetall(sess)\n                if not val:\n                    if self.session_expiry:\n                        # clean up the idx, because the key expired\n                        self.db.r_server.srem(id_idx, sess)\n                    continue\n                val = Storage(val)\n                # add a delete_record method (necessary for sessions2trash.py)\n                val.delete_record = RecordDeleter(\n                    self.db, sess, self.keyprefix)\n                rtn.append(val)\n            return rtn\n        else:\n            raise Exception(\"Operation not supported\")\n\n    def update(self, **kwargs):\n        # means that the session has been found and needs an update\n        if self.op == 'eq' and self.field == 'id' and self.value:\n            key = self.keyprefix + ':' + str(self.value)\n            if not self.db.r_server.exists(key):\n                return None\n            with self.db.r_server.pipeline() as pipe:\n                pipe.hmset(key, kwargs)\n                if self.session_expiry:\n                    pipe.expire(key, self.session_expiry)\n                rtn = pipe.execute()[0]\n            if self.with_lock:\n                release_lock(self.db, key + ':lock', self.value)\n            return rtn\n\n    def delete(self, **kwargs):\n        # means that we want this session to be deleted\n        if self.op == 'eq' and self.field == 'id' and self.value:\n            id_idx = \"%s:id_idx\" % self.keyprefix\n            key = self.keyprefix + ':' + str(self.value)\n            with self.db.r_server.pipeline() as pipe:\n                pipe.delete(key)\n                pipe.srem(id_idx, key)\n                rtn = pipe.execute()\n            return rtn[1]\n\n\nclass RecordDeleter(object):\n    \"\"\"Dumb record deleter to support sessions2trash.py\"\"\"\n\n    def __init__(self, db, key, keyprefix):\n        self.db, self.key, self.keyprefix = db, key, keyprefix\n\n    def __call__(self):\n        id_idx = \"%s:id_idx\" % self.keyprefix\n        # remove from the index\n        self.db.r_server.srem(id_idx, self.key)\n        # remove the key itself\n        self.db.r_server.delete(self.key)\n", "path": "gluon/contrib/redis_session.py"}], "after_files": [{"content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\"\"\"\nDeveloped by niphlod@gmail.com\nLicense MIT/BSD/GPL\n\nRedis-backed sessions\n\"\"\"\n\nimport logging\nfrom threading import Lock\nfrom gluon import current\nfrom gluon.storage import Storage\nfrom gluon.contrib.redis_utils import acquire_lock, release_lock\nfrom gluon.contrib.redis_utils import register_release_lock\nfrom gluon._compat import to_native\nfrom datetime import datetime\n\nlogger = logging.getLogger(\"web2py.session.redis\")\n\nlocker = Lock()\n\n\ndef RedisSession(redis_conn, session_expiry=False, with_lock=False, db=None):\n    \"\"\"\n    Usage example: put in models::\n\n        from gluon.contrib.redis_utils import RConn\n        rconn = RConn()\n        from gluon.contrib.redis_session import RedisSession\n        sessiondb = RedisSession(redis_conn=rconn, with_lock=True, session_expiry=False)\n        session.connect(request, response, db = sessiondb)\n\n    Args:\n        redis_conn: a redis-like connection object\n        with_lock: prevent concurrent modifications to the same session\n        session_expiry: delete automatically sessions after n seconds\n                        (still need to run sessions2trash.py every 1M sessions\n                        or so)\n\n    Simple slip-in storage for session\n    \"\"\"\n\n    locker.acquire()\n    try:\n        instance_name = 'redis_instance_' + current.request.application\n        if not hasattr(RedisSession, instance_name):\n            setattr(RedisSession, instance_name,\n                    RedisClient(redis_conn, session_expiry=session_expiry, with_lock=with_lock))\n        return getattr(RedisSession, instance_name)\n    finally:\n        locker.release()\n\n\nclass RedisClient(object):\n\n    def __init__(self, redis_conn, session_expiry=False, with_lock=False):\n        self.r_server = redis_conn\n        self._release_script = register_release_lock(self.r_server)\n        self.tablename = None\n        self.session_expiry = session_expiry\n        self.with_lock = with_lock\n\n    def get(self, what, default):\n        return self.tablename\n\n    def Field(self, fieldname, type='string', length=None, default=None,\n              required=False, requires=None):\n        return fieldname, type\n\n    def define_table(self, tablename, *fields, **args):\n        if not self.tablename:\n            self.tablename = MockTable(\n                self, self.r_server, tablename, self.session_expiry,\n                with_lock=self.with_lock, fields=fields)\n        return self.tablename\n\n    def __getitem__(self, key):\n        return self.tablename\n\n    def __call__(self, where=''):\n        q = self.tablename.query\n        return q\n\n    def commit(self):\n        # this is only called by session2trash.py\n        pass\n\n    def convert_dict_string(self, dict_string):\n        fields = self.tablename.fields\n        typed_dict = dict()\n        converters = {\n            'boolean': lambda x: 1 if x.decode() == '1' else 0,\n            'blob': lambda x: x,\n        }\n        for field, ftype in fields:\n            if field not in dict_string:\n                continue\n            if ftype in converters:\n                typed_dict[field] = converters[ftype](dict_string[field])\n            else:\n                typed_dict[field] = dict_string[field].decode()\n        return typed_dict\n\n\nclass MockTable(object):\n\n    def __init__(self, db, r_server, tablename, session_expiry, with_lock=False, fields=None):\n        # here self.db is the RedisClient instance\n        self.db = db\n        self.tablename = tablename\n        # set the namespace for sessions of this app\n        self.keyprefix = 'w2p:sess:%s' % tablename.replace('web2py_session_', '')\n        # fast auto-increment id (needed for session handling)\n        self.serial = \"%s:serial\" % self.keyprefix\n        # index of all the session keys of this app\n        self.id_idx = \"%s:id_idx\" % self.keyprefix\n        # remember the session_expiry setting\n        self.session_expiry = session_expiry\n        self.with_lock = with_lock\n        self.fields = fields if fields is not None else []\n\n    def __call__(self, record_id, unique_key=None):\n        # Support DAL shortcut query: table(record_id)\n\n        # This will call the __getattr__ below\n        # returning a MockQuery\n        q = self.id\n\n        # Instructs MockQuery, to behave as db(table.id == record_id)\n        q.op = 'eq'\n        q.value = record_id\n        q.unique_key = unique_key\n\n        row = q.select()\n        return row[0] if row else Storage()\n\n    def __getattr__(self, key):\n        if key == 'id':\n            # return a fake query. We need to query it just by id for normal operations\n            self.query = MockQuery(\n                field='id', db=self.db,\n                prefix=self.keyprefix, session_expiry=self.session_expiry,\n                with_lock=self.with_lock, unique_key=self.unique_key\n            )\n            return self.query\n        elif key == '_db':\n            # needed because of the calls in sessions2trash.py and globals.py\n            return self.db\n\n    def insert(self, **kwargs):\n        # usually kwargs would be a Storage with several keys:\n        # 'locked', 'client_ip','created_datetime','modified_datetime'\n        # 'unique_key', 'session_data'\n        # retrieve a new key\n        newid = str(self.db.r_server.incr(self.serial))\n        key = self.keyprefix + ':' + newid\n        if self.with_lock:\n            key_lock = key + ':lock'\n            acquire_lock(self.db.r_server, key_lock, newid)\n        with self.db.r_server.pipeline() as pipe:\n            # add it to the index\n            pipe.sadd(self.id_idx, key)\n            # set a hash key with the Storage\n            pipe.hmset(key, kwargs)\n            if self.session_expiry:\n                pipe.expire(key, self.session_expiry)\n            pipe.execute()\n        if self.with_lock:\n            release_lock(self.db, key_lock, newid)\n        return newid\n\n\nclass MockQuery(object):\n    \"\"\"a fake Query object that supports querying by id\n       and listing all keys. No other operation is supported\n    \"\"\"\n    def __init__(self, field=None, db=None, prefix=None, session_expiry=False,\n                 with_lock=False, unique_key=None):\n        self.field = field\n        self.value = None\n        self.db = db\n        self.keyprefix = prefix\n        self.op = None\n        self.session_expiry = session_expiry\n        self.with_lock = with_lock\n        self.unique_key = unique_key\n\n    def __eq__(self, value, op='eq'):\n        self.value = value\n        self.op = op\n\n    def __ge__(self, value, op='ge'):\n        self.value = value\n        self.op = op\n\n    def __gt__(self, value, op='gt'):\n        self.value = value\n        self.op = op\n\n    def select(self):\n        if self.op == 'eq' and self.field == 'id' and self.value:\n            # means that someone wants to retrieve the key self.value\n            key = self.keyprefix + ':' + str(self.value)\n            if self.with_lock:\n                acquire_lock(self.db.r_server, key + ':lock', self.value, 2)\n            rtn = {to_native(k): v for k, v in self.db.r_server.hgetall(key).items()}\n            if rtn:\n                if self.unique_key:\n                    # make sure the id and unique_key are correct\n                    if rtn['unique_key'] == to_native(self.unique_key):\n                        rtn['update_record'] = self.update  # update record support\n                    else:\n                        rtn = None\n            return [Storage(self.db.convert_dict_string(rtn))] if rtn else []\n        elif self.op in ('ge', 'gt') and self.field == 'id' and self.value == 0:\n            # means that someone wants the complete list\n            rtn = []\n            id_idx = \"%s:id_idx\" % self.keyprefix\n            # find all session keys of this app\n            allkeys = self.db.r_server.smembers(id_idx)\n            for sess in allkeys:\n                val = self.db.r_server.hgetall(sess)\n                if not val:\n                    if self.session_expiry:\n                        # clean up the idx, because the key expired\n                        self.db.r_server.srem(id_idx, sess)\n                    continue\n                val = Storage(self.db.convert_dict_string(val))\n                # add a delete_record method (necessary for sessions2trash.py)\n                val.delete_record = RecordDeleter(\n                    self.db, sess, self.keyprefix)\n                rtn.append(val)\n            return rtn\n        else:\n            raise Exception(\"Operation not supported\")\n\n    def update(self, **kwargs):\n        # means that the session has been found and needs an update\n        if self.op == 'eq' and self.field == 'id' and self.value:\n            key = self.keyprefix + ':' + str(self.value)\n            if not self.db.r_server.exists(key):\n                return None\n            with self.db.r_server.pipeline() as pipe:\n                pipe.hmset(key, kwargs)\n                if self.session_expiry:\n                    pipe.expire(key, self.session_expiry)\n                rtn = pipe.execute()[0]\n            if self.with_lock:\n                release_lock(self.db, key + ':lock', self.value)\n            return rtn\n\n    def delete(self, **kwargs):\n        # means that we want this session to be deleted\n        if self.op == 'eq' and self.field == 'id' and self.value:\n            id_idx = \"%s:id_idx\" % self.keyprefix\n            key = self.keyprefix + ':' + str(self.value)\n            with self.db.r_server.pipeline() as pipe:\n                pipe.delete(key)\n                pipe.srem(id_idx, key)\n                rtn = pipe.execute()\n            return rtn[1]\n\n\nclass RecordDeleter(object):\n    \"\"\"Dumb record deleter to support sessions2trash.py\"\"\"\n\n    def __init__(self, db, key, keyprefix):\n        self.db, self.key, self.keyprefix = db, key, keyprefix\n\n    def __call__(self):\n        id_idx = \"%s:id_idx\" % self.keyprefix\n        # remove from the index\n        self.db.r_server.srem(id_idx, self.key)\n        # remove the key itself\n        self.db.r_server.delete(self.key)\n", "path": "gluon/contrib/redis_session.py"}]}