@@ -76,8 +76,15 @@ def __init__(self, db_path: str | Path | None = None) -> None:
7676 # Public API
7777 # ------------------------------------------------------------------
7878
79- def get (self , namespace : str , key : str ) -> Optional [str ]:
80- """Return the cached value or *None* if missing / expired."""
79+ def get (
80+ self , namespace : str , key : str , * , track_lru : bool = True
81+ ) -> Optional [str ]:
82+ """Return the cached value or *None* if missing / expired.
83+
84+ *track_lru* (default ``True``) updates *accessed_at* on a hit so that
85+ LRU eviction in :meth:`set` works correctly. Pass ``False`` when the
86+ namespace has no capacity limit and you want to avoid the extra write.
87+ """
8188 now = time .time ()
8289 with self ._lock :
8390 row = self ._conn .execute (
@@ -95,12 +102,13 @@ def get(self, namespace: str, key: str) -> Optional[str]:
95102 )
96103 self ._conn .commit ()
97104 return None
98- # Touch for LRU tracking
99- self ._conn .execute (
100- "UPDATE cache SET accessed_at = ? WHERE namespace = ? AND key = ?" ,
101- (now , namespace , key ),
102- )
103- self ._conn .commit ()
105+ if track_lru :
106+ # Touch accessed_at so LRU eviction in set() stays accurate.
107+ self ._conn .execute (
108+ "UPDATE cache SET accessed_at = ? WHERE namespace = ? AND key = ?" ,
109+ (now , namespace , key ),
110+ )
111+ self ._conn .commit ()
104112 return value
105113
106114 def set (
@@ -179,6 +187,36 @@ def clear(self, namespace: Optional[str] = None) -> None:
179187 self ._conn .execute ("DELETE FROM cache" )
180188 self ._conn .commit ()
181189
190+ def sweep_expired (self ) -> int :
191+ """Delete all expired rows across every namespace.
192+
193+ Returns the number of rows removed. Designed to be called from a
194+ background thread or an atexit hook without touching private state.
195+ """
196+ now = time .time ()
197+ with self ._lock :
198+ deleted = self ._conn .execute (
199+ "DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at <= ?" ,
200+ (now ,),
201+ ).rowcount
202+ self ._conn .commit ()
203+ return deleted
204+
205+ def vacuum_and_close (self ) -> None :
206+ """Sweep expired rows, VACUUM the database, then close the connection.
207+
208+ VACUUM must run outside any open transaction; this method handles the
209+ ``isolation_level`` toggling safely with a ``finally`` guard.
210+ """
211+ self .sweep_expired ()
212+ with self ._lock :
213+ try :
214+ self ._conn .isolation_level = None
215+ self ._conn .execute ("VACUUM" )
216+ finally :
217+ self ._conn .isolation_level = ""
218+ self .close ()
219+
182220 def close (self ) -> None :
183221 """Close the underlying database connection."""
184222 try :
@@ -206,6 +244,16 @@ def get_cache_db() -> CacheDB:
206244 return _db
207245
208246
247+ def get_cache_db_or_none () -> Optional [CacheDB ]:
248+ """Return the singleton if it has already been initialised, else ``None``.
249+
250+ Unlike :func:`get_cache_db` this never creates the database — useful for
251+ background housekeeping that should be a no-op before the first real cache
252+ access.
253+ """
254+ return _db
255+
256+
209257def reset_cache_db (db : Optional [CacheDB ] = None ) -> None :
210258 """Replace the singleton — mainly useful for tests.
211259
0 commit comments