11import os
22import sqlite3
33from contextlib import suppress
4- from typing import Generator , Tuple , Union
4+ from typing import Generator , Tuple , Union , ByteString
55
66
77class CacheStorageBase :
88
9- def __init__ (self , * , maxsize , ttl ):
9+ def __init__ (self , * , maxsize : int , ttl : Union [ int , float ], policy : str ):
1010 self .maxsize = maxsize
1111 self .ttl = ttl
12+ self .policy = policy
1213
13- def __setitem__ (self , key , value ) -> None :
14+ def __setitem__ (self , key : ByteString , value : ByteString ) -> None :
1415 raise NotImplementedError # pragma: no cover
1516
1617 def __getitem__ (self , key ) -> bytes :
@@ -19,7 +20,7 @@ def __getitem__(self, key) -> bytes:
1920 def __delitem__ (self , key ) -> None :
2021 raise NotImplementedError # pragma: no cover
2122
22- def get (self , key , default = None ) -> Union [bytes , None ]:
23+ def get (self , key : ByteString , default = None ) -> Union [bytes , None ]:
2324 raise NotImplementedError # pragma: no cover
2425
2526 def clear (self ) -> None :
@@ -34,9 +35,32 @@ def items(self) -> Generator[Tuple[bytes, bytes], None, None]:
3435
3536class SQLiteStorage (CacheStorageBase ):
3637 SQLITE_TIMESTAMP = "(julianday('now') - 2440587.5)*86400.0"
37-
38- def __init__ (self , * , filepath , ttl , maxsize ):
39- super (SQLiteStorage , self ).__init__ (ttl = ttl , maxsize = maxsize )
38+ POLICIES = {
39+ 'FIFO' : {
40+ 'additional_columns' : [],
41+ 'after_get_ok' : None ,
42+ 'additional_indexes' : [],
43+ 'delete_order_by' : 'ts' ,
44+ },
45+ 'LRU' : {
46+ f'additional_columns' : [f"used INT NOT NULL DEFAULT 0" ],
47+ f'additional_indexes' : ['used, ts' ],
48+ f'after_get_ok' : f"UPDATE cache SET used = (SELECT max(used) FROM cache) + 1" ,
49+ f'delete_order_by' : 'used, ts' ,
50+ },
51+ 'LFU' : {
52+ 'additional_columns' : ['used INT NOT NULL DEFAULT 0' ],
53+ 'additional_indexes' : ['used, ts' ],
54+ 'after_get_ok' : "UPDATE cache SET used = used + 1" ,
55+ f'delete_order_by' : 'used, ts' ,
56+ },
57+ }
58+
59+ def __init__ (self , * , filepath , ttl , maxsize , policy = 'FIFO' ):
60+ if policy not in self .POLICIES :
61+ raise ValueError (f'Invalid policy: { policy } ' )
62+ super (SQLiteStorage , self ).__init__ (
63+ ttl = ttl , maxsize = maxsize , policy = policy )
4064 self .filepath = filepath
4165 self .db = sqlite3 .connect (filepath , isolation_level = 'DEFERRED' )
4266 self .init_db ()
@@ -77,8 +101,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
77101 def __setitem__ (self , key , value ):
78102 with self .db as db :
79103 db .execute (
80- "INSERT OR REPLACE INTO cache VALUES "
81- f"(?, { self .SQLITE_TIMESTAMP } , ?)" ,
104+ "INSERT OR REPLACE INTO cache (key, value) VALUES (?, ?)" ,
82105 (key , value )
83106 )
84107
@@ -95,44 +118,61 @@ def __delitem__(self, key):
95118 raise KeyError ('Not found' )
96119
97120 def get (self , key , default = None ):
98- rows = self .db .execute (
99- self .sql_select ,
100- (key ,),
101- ).fetchall ()
102- return rows [0 ][0 ] if rows else default
121+ with self .db :
122+ rows = self .db .execute (
123+ self .sql_select ,
124+ (key ,),
125+ ).fetchall ()
126+ after_get_ok = self .POLICIES [self .policy ]['after_get_ok' ]
127+ if rows :
128+ if after_get_ok :
129+ self .db .execute (
130+ f'{ after_get_ok } WHERE key = ?' ,
131+ (key ,),
132+ )
133+ return rows [0 ][0 ]
134+ else :
135+ return default
103136
104137 def init_db (self ):
138+ policy_stuff = self .POLICIES [self .policy ]
139+
105140 after_insert_actions = []
106141 if self .ttl > 0 :
107- after_insert_actions .append (
108- ' DELETE FROM cache WHERE '
109- f' ({ self .SQLITE_TIMESTAMP } - ts) > { self .ttl } ;'
110- )
142+ after_insert_actions .append (f'''
143+ DELETE FROM cache WHERE
144+ ({ self .SQLITE_TIMESTAMP } - ts) > { self .ttl } ;
145+ ''' )
111146 if self .maxsize > 0 :
112- after_insert_actions .append (
113- ' DELETE FROM cache WHERE key in ('
114- 'SELECT key FROM cache '
115- 'ORDER BY ts LIMIT '
116- f'max(0, (SELECT COUNT(key) FROM cache) - { self .maxsize } ));'
117- )
147+ after_insert_actions .append (f'''
148+ DELETE FROM cache WHERE key in (
149+ SELECT key FROM cache
150+ ORDER BY { policy_stuff ['delete_order_by' ]}
151+ LIMIT max(0, (SELECT COUNT(key) FROM cache) - { self .maxsize } )
152+ );
153+ ''' )
118154
119155 with self .db as db :
120- db .execute (
121- 'CREATE TABLE IF NOT EXISTS cache ('
122- ' key BINARY PRIMARY KEY,'
123- ' ts REAL,'
124- ' value BLOB'
125- ') WITHOUT ROWID'
126- )
156+ db .execute (f'''
157+ CREATE TABLE IF NOT EXISTS cache (
158+ key BINARY PRIMARY KEY,
159+ ts REAL NOT NULL DEFAULT ({ self .SQLITE_TIMESTAMP } ),
160+ { '' .join (f"{ c } , " for c in policy_stuff ['additional_columns' ])}
161+ value BLOB NOT NULL
162+ ) WITHOUT ROWID
163+ ''' )
127164 db .execute ('CREATE INDEX IF NOT EXISTS i_cache_ts ON cache (ts)' )
165+
166+ for i , columns in enumerate (policy_stuff ['additional_indexes' ]):
167+ db .execute (f'CREATE INDEX IF NOT EXISTS i_cache_{ i } ON cache ({ columns } )' )
168+
128169 if after_insert_actions :
129- trigger_ddl = (
130- 'CREATE TRIGGER IF NOT EXISTS t_cache_cleanup\n '
131- 'AFTER INSERT ON cache FOR EACH ROW BEGIN\n '
132- '%s\n '
133- 'END'
134- ) % '\n ' .join (after_insert_actions )
135- db .execute (trigger_ddl )
170+ db .execute (f'''
171+ CREATE TRIGGER IF NOT EXISTS t_cache_cleanup
172+ AFTER INSERT ON cache FOR EACH ROW BEGIN
173+ %s
174+ END
175+ ''' % '\n ' .join (after_insert_actions ))
136176
137177 def clear (self ):
138178 with self .db as db :
0 commit comments