1- """Caching layer for KeyUp! CLI using diskcache .
1+ """Caching layer for KeyUp! CLI using sqlite3 .
22
33Provides TTL-based caching for ClickUp API responses to reduce API calls.
44"""
55
6+ import pickle
7+ import sqlite3
8+ import time
69from pathlib import Path
710
8- import diskcache
9-
1011# Cache location: ~/.keyup/cache/
1112CACHE_DIR = Path .home () / ".keyup" / "cache"
13+ CACHE_FILE = CACHE_DIR / "keyup_cache.db"
1214
1315# TTL values in seconds
1416TEAMS_TTL = 24 * 60 * 60 # 24 hours
15- SPACES_TTL = 24 * 60 * 60 # 24 hours
16- PROJECTS_TTL = 24 * 60 * 60 # 24 hours
1717LISTS_TTL = 24 * 60 * 60 # 24 hours
1818TASKS_TTL = 5 * 60 # 5 minutes
1919
2020
21- def get_cache () -> diskcache .Cache :
21+ class SQLiteCache :
22+ """SQLite-backed cache with TTL support using pickle serialization."""
23+
24+ def __init__ (self , db_path : str ):
25+ self ._db_path = db_path
26+ with sqlite3 .connect (self ._db_path ) as conn :
27+ conn .execute ("PRAGMA journal_mode=WAL" )
28+ conn .execute ("""
29+ CREATE TABLE IF NOT EXISTS cache (
30+ key TEXT PRIMARY KEY,
31+ value BLOB NOT NULL,
32+ expires_at REAL NOT NULL
33+ )
34+ """ )
35+
36+ def __contains__ (self , key : str ) -> bool :
37+ with sqlite3 .connect (self ._db_path ) as conn :
38+ row = conn .execute ("SELECT expires_at FROM cache WHERE key = ?" , (key ,)).fetchone ()
39+ return row is not None and row [0 ] > time .time ()
40+
41+ def get (self , key : str ):
42+ with sqlite3 .connect (self ._db_path ) as conn :
43+ row = conn .execute ("SELECT value, expires_at FROM cache WHERE key = ?" , (key ,)).fetchone ()
44+ if row is None :
45+ return None
46+ if row [1 ] <= time .time ():
47+ conn .execute ("DELETE FROM cache WHERE key = ?" , (key ,))
48+ return None
49+ return pickle .loads (row [0 ]) # noqa: S301 - local self-written cache
50+
51+ def set (self , key : str , value , expire : int ) -> None :
52+ with sqlite3 .connect (self ._db_path ) as conn :
53+ conn .execute (
54+ "INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)" ,
55+ (key , pickle .dumps (value ), time .time () + expire ),
56+ )
57+
58+ def delete (self , key : str ) -> None :
59+ with sqlite3 .connect (self ._db_path ) as conn :
60+ conn .execute ("DELETE FROM cache WHERE key = ?" , (key ,))
61+
62+ def clear (self ) -> None :
63+ with sqlite3 .connect (self ._db_path ) as conn :
64+ conn .execute ("DELETE FROM cache" )
65+
66+
67+ def get_cache () -> SQLiteCache :
2268 """Get or create disk cache instance."""
2369 CACHE_DIR .mkdir (parents = True , exist_ok = True )
24- return diskcache . Cache (str (CACHE_DIR ))
70+ return SQLiteCache (str (CACHE_FILE ))
2571
2672
2773def get_teams_data (clickup ):
@@ -37,7 +83,7 @@ def get_teams_data(clickup):
3783 cache_key = "teams"
3884
3985 if cache_key in cache :
40- return cache .get (cache_key ) # type: ignore[no-any-return] # type: ignore[no-any-return] # type: ignore[no-any-return]
86+ return cache .get (cache_key ) # type: ignore[no-any-return]
4187
4288 teams = clickup .teams
4389 cache .set (cache_key , teams , expire = TEAMS_TTL )
@@ -57,7 +103,7 @@ def get_lists_data(team):
57103 cache_key = f"lists:{ team .id } "
58104
59105 if cache_key in cache :
60- return cache .get (cache_key ) # type: ignore[no-any-return] # type: ignore[no-any-return] # type: ignore[no-any-return]
106+ return cache .get (cache_key ) # type: ignore[no-any-return]
61107
62108 lists = team .lists
63109 cache .set (cache_key , lists , expire = LISTS_TTL )
@@ -78,7 +124,7 @@ def get_tasks_data(team, list_id: str):
78124 cache_key = f"tasks:{ list_id } "
79125
80126 if cache_key in cache :
81- return cache .get (cache_key ) # type: ignore[no-any-return] # type: ignore[no-any-return]
127+ return cache .get (cache_key ) # type: ignore[no-any-return]
82128
83129 tasks = team .get_all_tasks (subtasks = False , list_ids = [list_id ])
84130 cache .set (cache_key , tasks , expire = TASKS_TTL )
0 commit comments