Skip to content

Commit fe10f9a

Browse files
committed
Add tests to BasicDatabase and merge implementations
test/session_common.py: add new tests: * test set with bad __timestamps * test get on missing item with no default * test clear() method * test new lifetime() method everything below here was needed to get the tests to work across all db's. roundup/backends/sessions_dbm.py: make set() validate __timestamp as float. If invalid and item is new, set it to time.time(). If invalid and item exists keep original timestamp. add lifetime(key_lifetime) method. Given key_lifetime in seconds, generate a __timestamp that will be deleted after that many seconds. roundup/backends/sessions_rdbms.py: make set() behave the same as session_dbm. add lifetime method as above. roundup/backends/sessions_sqlite.py: import session_rdbms::BasicDatabase and override __init__. rather than cloning all the code. Kept a few logging and sql methods. roundup/test/memorydb.py: make get() on a missing key return KeyError make set() conform to dbm/rdbms implementations.
1 parent 8f69e39 commit fe10f9a

File tree

5 files changed

+129
-133
lines changed

5 files changed

+129
-133
lines changed

roundup/backends/sessions_dbm.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,26 @@ def getall(self, infoid):
8888

8989
def set(self, infoid, **newvalues):
9090
db = self.opendb('c')
91+
timestamp=None
9192
try:
9293
if infoid in db:
9394
values = marshal.loads(db[infoid])
95+
try:
96+
timestamp = values['__timestamp']
97+
except KeyError:
98+
pass # stay at None
9499
else:
95-
values = {'__timestamp': time.time()}
100+
values = {}
101+
102+
if '__timestamp' in newvalues:
103+
try:
104+
float(newvalues['__timestamp'])
105+
except ValueError:
106+
# keep original timestamp if present
107+
newvalues['__timestamp'] = timestamp or time.time()
108+
else:
109+
newvalues['__timestamp'] = time.time()
110+
96111
values.update(newvalues)
97112
db[infoid] = marshal.dumps(values)
98113
finally:
@@ -157,6 +172,14 @@ def opendb(self, mode):
157172
def commit(self):
158173
pass
159174

175+
def lifetime(self, key_lifetime=0):
176+
"""Return the proper timestamp for a key with key_lifetime specified
177+
in seconds. Default lifetime is 0.
178+
"""
179+
now = time.time()
180+
week = 60*60*24*7
181+
return now - week + key_lifetime
182+
160183
def close(self):
161184
pass
162185

roundup/backends/sessions_rdbms.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,37 +60,34 @@ def set(self, infoid, **newvalues):
6060
c = self.cursor
6161
n = self.name
6262
a = self.db.arg
63-
c.execute('select %s_value, %s_time from %ss where %s_key=%s'% \
64-
(n, n, n, n, a),
63+
c.execute('select %s_value from %ss where %s_key=%s'% \
64+
(n, n, n, a),
6565
(infoid,))
6666
res = c.fetchone()
67+
68+
timestamp=time.time()
6769
if res:
6870
values = eval(res[0])
69-
timestamp = res[1]
7071
else:
7172
values = {}
73+
74+
if '__timestamp' in newvalues:
75+
try:
76+
# __timestamp must be representable as a float. Check it.
77+
timestamp = float(newvalues['__timestamp'])
78+
except ValueError:
79+
if res:
80+
# keep the original timestamp
81+
del(newvalues['__timestamp'])
82+
else:
83+
# here timestamp is the new timestamp
84+
newvalues['__timestamp'] = timestamp
7285
values.update(newvalues)
7386
if res:
74-
if '__timestamp' in newvalues:
75-
try:
76-
# __timestamp must be representable as a float. Check it.
77-
timestamp = float(newvalues['__timestamp'])
78-
except ValueError:
79-
pass
80-
8187
sql = ('update %ss set %s_value=%s, %s_time=%s '
8288
'where %s_key=%s'%(n, n, a, n, a, n, a))
8389
args = (repr(values), timestamp, infoid)
8490
else:
85-
if '__timestamp' in newvalues:
86-
try:
87-
# __timestamp must be represntable as a float. Check it.
88-
timestamp = float(newvalues['__timestamp'])
89-
except ValueError:
90-
timestamp = time.time()
91-
else:
92-
timestamp = time.time()
93-
9491
sql = 'insert into %ss (%s_key, %s_time, %s_value) '\
9592
'values (%s, %s, %s)'%(n, n, n, n, a, a, a)
9693
args = (infoid, timestamp, repr(values))
@@ -128,6 +125,14 @@ def commit(self):
128125
self.conn.commit()
129126
self.cursor = self.conn.cursor()
130127

128+
def lifetime(self, item_lifetime=0):
129+
"""Return the proper timestamp for a key with key_lifetime specified
130+
in seconds. Default lifetime is 0.
131+
"""
132+
now = time.time()
133+
week = 60*60*24*7
134+
return now - week + item_lifetime
135+
131136
def close(self):
132137
self.conn.close()
133138

roundup/backends/sessions_sqlite.py

Lines changed: 2 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
import os, time, logging
1515

1616
from roundup.anypy.html import html_escape as escape
17+
import roundup.backends.sessions_rdbms as rdbms_session
1718

18-
class BasicDatabase:
19+
class BasicDatabase(rdbms_session.BasicDatabase):
1920
''' Provide a nice encapsulation of an RDBMS table.
2021
2122
Keys are id strings, values are automatically marshalled data.
@@ -68,118 +69,6 @@ def sql(self, sql, args=None, cursor=None):
6869
else:
6970
cursor.execute(sql)
7071

71-
def clear(self):
72-
self.cursor.execute('delete from %ss'%self.name)
73-
74-
def exists(self, infoid):
75-
n = self.name
76-
self.cursor.execute('select count(*) from %ss where %s_key=%s'%(n,
77-
n, self.db.arg), (infoid,))
78-
return int(self.cursor.fetchone()[0])
79-
80-
_marker = []
81-
def get(self, infoid, value, default=_marker):
82-
n = self.name
83-
self.cursor.execute('select %s_value from %ss where %s_key=%s'%(n,
84-
n, n, self.db.arg), (infoid,))
85-
res = self.cursor.fetchone()
86-
if not res:
87-
if default != self._marker:
88-
return default
89-
raise KeyError('No such %s "%s"'%(self.name, escape(infoid)))
90-
values = eval(res[0])
91-
return values.get(value, None)
92-
93-
def getall(self, infoid):
94-
n = self.name
95-
self.cursor.execute('select %s_value from %ss where %s_key=%s'%(n,
96-
n, n, self.db.arg), (infoid,))
97-
res = self.cursor.fetchone()
98-
if not res:
99-
raise KeyError('No such %s "%s"'%(self.name, escape (infoid)))
100-
return eval(res[0])
101-
102-
def set(self, infoid, **newvalues):
103-
""" Store all newvalues under key infoid with a timestamp in database.
104-
105-
If newvalues['__timestamp'] exists and is representable as a floating point number
106-
(i.e. could be generated by time.time()), that value is used for the <name>_time
107-
column in the database.
108-
"""
109-
c = self.cursor
110-
n = self.name
111-
a = self.db.arg
112-
c.execute('select %s_value from %ss where %s_key=%s'%(n, n, n, a),
113-
(infoid,))
114-
res = c.fetchone()
115-
if res:
116-
values = eval(res[0])
117-
else:
118-
values = {}
119-
values.update(newvalues)
120-
121-
if res:
122-
sql = 'update %ss set %s_value=%s where %s_key=%s'%(n, n,
123-
a, n, a)
124-
args = (repr(values), infoid)
125-
else:
126-
if '__timestamp' in newvalues:
127-
try:
128-
# __timestamp must be represntable as a float. Check it.
129-
timestamp = float(newvalues['__timestamp'])
130-
except ValueError:
131-
timestamp = time.time()
132-
else:
133-
timestamp = time.time()
134-
135-
sql = 'insert into %ss (%s_key, %s_time, %s_value) '\
136-
'values (%s, %s, %s)'%(n, n, n, n, a, a, a)
137-
args = (infoid, timestamp, repr(values))
138-
c.execute(sql, args)
139-
140-
def list(self):
141-
c = self.cursor
142-
n = self.name
143-
c.execute('select %s_key from %ss'%(n, n))
144-
return [res[0] for res in c.fetchall()]
145-
146-
def destroy(self, infoid):
147-
self.cursor.execute('delete from %ss where %s_key=%s'%(self.name,
148-
self.name, self.db.arg), (infoid,))
149-
150-
def updateTimestamp(self, infoid):
151-
""" don't update every hit - once a minute should be OK """
152-
now = time.time()
153-
self.cursor.execute('''update %ss set %s_time=%s where %s_key=%s
154-
and %s_time < %s'''%(self.name, self.name, self.db.arg,
155-
self.name, self.db.arg, self.name, self.db.arg),
156-
(now, infoid, now-60))
157-
158-
def clean(self):
159-
''' Remove session records that haven't been used for a week. '''
160-
now = time.time()
161-
week = 60*60*24*7
162-
old = now - week
163-
self.cursor.execute('delete from %ss where %s_time < %s'%(self.name,
164-
self.name, self.db.arg), (old, ))
165-
166-
def lifetime(self, key_lifetime=None):
167-
"""Return the proper timestamp for a key with key_lifetime specified
168-
in seconds.
169-
"""
170-
now = time.time()
171-
week = 60*60*24*7
172-
return now - week + lifetime
173-
174-
def commit(self):
175-
logger = logging.getLogger('roundup.hyperdb.backend')
176-
logger.info('commit %s' % self.name)
177-
self.conn.commit()
178-
self.cursor = self.conn.cursor()
179-
180-
def close(self):
181-
self.conn.close()
182-
18372
class Sessions(BasicDatabase):
18473
name = 'session'
18574

roundup/test/memorydb.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import shutil
55
import os
6+
import time
67

78
from roundup import date
89
from roundup import hyperdb
@@ -163,12 +164,22 @@ def __contains__(self, key):
163164
def exists(self, infoid):
164165
return infoid in self
165166
def get(self, infoid, value, default=None):
167+
if infoid not in self:
168+
raise KeyError
166169
return self[infoid].get(value, default)
167170
def getall(self, infoid):
168171
if infoid not in self:
169172
raise KeyError(infoid)
170173
return self[infoid]
171174
def set(self, infoid, **newvalues):
175+
if '__timestamp' in newvalues:
176+
try:
177+
float(newvalues['__timestamp'])
178+
except ValueError:
179+
if infoid in self:
180+
del(newvalues['__timestamp'])
181+
else:
182+
newvalues['__timestamp'] = time.time()
172183
self[infoid].update(newvalues)
173184
def list(self):
174185
return list(self.keys())

test/session_common.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,28 @@
22

33
from .db_test_base import config
44

5+
"""
6+
here are three different impementations for these. I am trying to fix
7+
them so they all act the same.
58
9+
set with invalid timestamp:
10+
11+
session_dbm/memorydb - sets to invalid timestamp if new or existing item.
12+
session_rdbms - sets to time.time if new item, keeps original
13+
if item exists. (note that the timestamp is
14+
a separate column, the timestamp embedded in the
15+
value object in the db has the bad __timestamp.
16+
reconciled: set to time.time for new item, keeps original time
17+
of existing item.
18+
19+
Also updateTimestamp does not update the marshalled values idea of
20+
__timestamp. So get(item, '__timestamp') will not work as expected
21+
for rdbms backends, need a sql query to get the timestamp column.
22+
23+
FIXME need to add getTimestamp method to sessions_rdbms.py and
24+
sessions_dbm.py.
25+
26+
"""
627
class SessionTest(object):
728
def setUp(self):
829
# remove previous test, ignore errors
@@ -33,6 +54,11 @@ def testList(self):
3354
self.assertEqual(self.sessions.list().sort(),
3455
[self.s2b('random_key'), self.s2b('random_key2')].sort())
3556

57+
def testGetMissingKey(self):
58+
self.sessions.set('random_key', text='hello, world!', otherval='bar')
59+
with self.assertRaises(KeyError) as e:
60+
self.sessions.get('badc_key', 'text')
61+
3662
def testGetAll(self):
3763
self.sessions.set('random_key', text='hello, world!', otherval='bar')
3864
self.assertEqual(self.sessions.getall('random_key'),
@@ -45,6 +71,16 @@ def testDestroy(self):
4571
self.sessions.destroy('random_key')
4672
self.assertRaises(KeyError, self.sessions.getall, 'random_key')
4773

74+
def testClear(self):
75+
self.sessions.set('random_key', text='hello, world!')
76+
self.sessions.set('random_key2', text='hello, world!')
77+
self.sessions.set('random_key3', text='hello, world!')
78+
self.assertEqual(self.sessions.getall('random_key3'),
79+
{'text': 'hello, world!'})
80+
self.assertEqual(len(self.sessions.list()), 3)
81+
self.sessions.clear()
82+
self.assertEqual(len(self.sessions.list()), 0)
83+
4884
def testSetSession(self):
4985
self.sessions.set('random_key', text='hello, world!', otherval='bar')
5086
self.assertEqual(self.sessions.get('random_key', 'text'),
@@ -59,6 +95,32 @@ def testUpdateSession(self):
5995
self.sessions.set('random_key', text='nope')
6096
self.assertEqual(self.sessions.get('random_key', 'text'), 'nope')
6197

98+
def testBadTimestamp(self):
99+
self.sessions.set('random_key',
100+
text='hello, world!',
101+
__timestamp='not a timestamp')
102+
ts = self.sessions.get('random_key', '__timestamp')
103+
self.assertNotEqual(ts, 'not a timestamp')
104+
# use {1,7} because db's don't pad the fraction to 7 digits.
105+
ts_re=r'^[0-9]{10,16}\.[0-9]{1,7}$'
106+
try:
107+
self.assertRegex(str(ts), ts_re)
108+
except AttributeError: # 2.7 version
109+
import warnings
110+
with warnings.catch_warnings():
111+
warnings.filterwarnings("ignore",category=DeprecationWarning)
112+
self.assertRegexpMatches(str(ts), ts_re)
113+
114+
# now update with a bad timestamp, original timestamp should
115+
# be kept.
116+
self.sessions.set('random_key',
117+
text='hello, world2!',
118+
__timestamp='not a timestamp')
119+
item = self.sessions.get('random_key', "text")
120+
item_ts = self.sessions.get('random_key', "__timestamp")
121+
self.assertEqual(item, 'hello, world2!')
122+
self.assertAlmostEqual(ts, item_ts, 2)
123+
62124
# overridden in dbm and memory backends
63125
def testUpdateTimestamp(self):
64126
def get_ts_via_sql(self):
@@ -88,3 +150,9 @@ def get_ts_via_sql(self):
88150

89151
# use 61 to allow a fudge factor
90152
self.assertGreater(get_ts_via_sql(self)[0] - timestamp, 61)
153+
154+
def testLifetime(self):
155+
ts = self.sessions.lifetime(300)
156+
week_ago = time.time() - 60*60*24*7
157+
self.assertGreater(week_ago + 302, ts)
158+
self.assertLess(week_ago + 298, ts)

0 commit comments

Comments
 (0)