11import abc
2+ import collections
23from dataclasses import dataclass
34import inspect
45import time
56from typing import Any , Optional , TypeVar
67
7- from . import scheduler
8+ from apscheduler .schedulers .background import BackgroundScheduler
9+ from apscheduler .job import Job
810
911
1012T = TypeVar ("T" )
1113EMPTY = inspect ._empty
1214
13- @dataclass
14- class StorageValue :
15- value : Any
16- expires : Optional [float ]
17- timestamp : float
18-
19-
20- class Storage (metaclass = abc .ABCMeta ):
21- def __init__ (self ) -> None :
22- self .cache : dict [str , StorageValue ] = {}
23- self ._clean_task : int = - 1
24- @abc .abstractmethod
25- def set (self , key : str , value : object , expires : Optional [float ] = None ) -> None :
26- raise NotImplementedError
27- @abc .abstractmethod
28- def get (self , key : str , _def : Any = EMPTY ) -> Any :
29- raise NotImplementedError
30-
31- @abc .abstractmethod
32- def delete (self , key : str ) -> None :
33- raise NotImplementedError
34-
35- def __setitem__ (self , key : str , value : object ) -> Any :
36- return self .set (key , value )
37-
38- def __getitem__ (self , key : str ) -> Any :
39- value = self .get (key )
40- if value == EMPTY :
41- raise IndexError
42- return value
43- def exists (self , key : str ) -> bool :
44- obj = self .cache .get (key , None )
45- return obj is not None and (obj .expires is not None and obj .expires + obj .timestamp >= time .time () or not obj .expires )
46- def __contains__ (self , key : str ) -> bool :
47- return self .exists (key )
48-
49- def clean (self ):
50- for k , v in filter (lambda x : (x [1 ].expires is not None and x [1 ].expires + x [1 ].timestamp >= time .time ()), list (self .cache .items ())):
51- self .cache .pop (k )
52- print (k )
53- self ._start_clean ()
54-
15+ class CacheValue [T ]:
16+ def __init__ (self , value : T , expires : Optional [float ] = None ) -> None :
17+ self .value : T = value
18+ self .expires : Optional [float ] = expires
19+ self .timestamp : float = time .monotonic_ns ()
20+ self .job : Optional [Job ] = None
21+
22+
23+ # Time out cache
24+ class TimeoutCache [T ]:
25+ def __init__ (self ):
26+ self .cache : collections .OrderedDict [str , CacheValue ] = collections .OrderedDict ()
27+ self .background = BackgroundScheduler ()
28+
29+ def set (self , key : str , value : T , timeout : Optional [float ] = None ) -> None :
30+ self ._delete_job (key )
31+ self .cache [key ] = CacheValue (
32+ value ,
33+ timeout
34+ )
35+ if timeout is not None :
36+ self .cache [key ].job = self .background .add_job (self .delete , 'interval' , seconds = timeout , args = [key ])
5537
56- def _start_clean (self ):
57- scheduler .cancel (self ._clean_task )
58- ready = filter (lambda x : (x .expires is not None and x .expires + x .timestamp >= time .time ()), list (self .cache .values ()))
59- if not ready :
38+ def _delete_job (self , key : str ):
39+ current = self .cache .get (key , None )
40+ if current is None or current .job is None :
6041 return
61- next_time = max ( ready , key = lambda x : ( x . expires + x . timestamp ) - time . time ()) # type: ignore
62- self . _clean_task = scheduler . run_later ( self . clean , delay = ( next_time . expires + next_time . timestamp ) - time . time ()) # type: ignore
42+ current . job . remove ()
43+ current . job = None
6344
64- def get_keys (self ) -> list [str ]:
65- return list (self .cache .keys ())
66-
67- def get_startswith_all (self , key : str ) -> dict [str , Any ]:
68- return {k : v for k , v in self .cache .items () if k .startswith (key )}
69-
70- def get_endswith_all (self , key : str ) -> dict [str , Any ]:
71- return {k : v for k , v in self .cache .items () if k .endswith (key )}
72-
73- def get_contains_all (self , key : str ) -> dict [str , Any ]:
74- return {k : v for k , v in self .cache .items () if key in k }
75-
76- class MemoryStorage (Storage ):
77- def __init__ (self ) -> None :
78- super ().__init__ ()
79-
80- def set (self , key : str , value : object , expires : float | None = None ) -> None :
81- data = value
82- obj = StorageValue (
83- data ,
84- expires ,
85- time .time ()
86- )
87- self .cache [key ] = obj
88-
8945 def get (self , key : str , _def : Any = EMPTY ) -> Any :
90- if not self .exists (key ):
46+ current = self .cache .get (key , None )
47+ if current is None :
48+ return _def
49+ if current .expires is not None and current .expires + current .timestamp < time .monotonic_ns ():
50+ self .delete (key )
9151 return _def
92- return self . cache [ key ]. value or _def
52+ return current . value
9353
9454 def delete (self , key : str ) -> None :
95- self .cache .pop (key , None )
55+ self ._delete_job (key )
56+ self .cache .pop (key , None )
57+
58+ def __contains__ (self , key : str ) -> bool :
59+ if key in self .cache :
60+ current = self .cache [key ]
61+ if current .expires is not None and current .expires + current .timestamp < time .monotonic_ns ():
62+ self .delete (key )
63+ return key in self .cache
0 commit comments