We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unable to login when using redis for storing sessions
Basically a new session is created on each page load so the login is never established.

Issue discussed at: https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/web2py/6Ig5YVgvIsI/HpueAUELBgAJ

Confirmed with web2py versions from 2.18.5 up to 2.20.4.  Python versions 3.6 and 3.8




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

{"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@@ -13,7 +13,7 @@\n from gluon.storage import Storage\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 gluon._compat import to_native, to_bytes\n from datetime import datetime\n \n logger = logging.getLogger(\"web2py.session.redis\")\n@@ -131,7 +131,6 @@\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@@ -208,7 +207,7 @@\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+                    if rtn['unique_key'] == to_bytes(self.unique_key):\n                         rtn['update_record'] = self.update  # update record support\n                     else:\n                         rtn = None\n", "issue": "Unable to login when using redis for storing sessions\nBasically a new session is created on each page load so the login is never established.\r\n\r\nIssue discussed at: https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/web2py/6Ig5YVgvIsI/HpueAUELBgAJ\r\n\r\nConfirmed with web2py versions from 2.18.5 up to 2.20.4.  Python versions 3.6 and 3.8\r\n\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\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"}], "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, to_bytes\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        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_bytes(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"}]}