Skip to content

Commit 7248c84

Browse files
committed
Replace diskcache with built-in sqlite3 for TTL-based caching
- Drop diskcache dependency (unmaintained since 2023); remove from pyproject.toml, uv.lock, and pyright hook - Add SQLiteCache class backed by sqlite3 + pickle, with WAL mode and per-call context-manager connections - Use keyup_cache.db filename to avoid collision with existing diskcache cache.db files - Remove unused SPACES_TTL and PROJECTS_TTL constants - Update docs and tests accordingly
1 parent 5446f8b commit 7248c84

5 files changed

Lines changed: 62 additions & 27 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ Use `--no-cache` to bypass cache and fetch fresh data from the API.
204204
- [pyclickup](https://github.com/matagus/pyclickup) - ClickUp API wrapper
205205
- [colorist](https://github.com/TMDBC/colorist) - Terminal colors
206206
- [inquirer](https://github.com/magmax/python-inquirer) - Interactive prompts
207-
- [diskcache](https://github.com/kemche007/diskcache) - Caching layer
207+
- [sqlite3](https://docs.python.org/3/library/sqlite3.html) - Caching layer (Python standard library)
208208
- [python-dotenv](https://github.com/theskumar/python-dotenv) - Environment loading
209209

210210
## Development

keyup/cli/cache.py

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,73 @@
1-
"""Caching layer for KeyUp! CLI using diskcache.
1+
"""Caching layer for KeyUp! CLI using sqlite3.
22
33
Provides TTL-based caching for ClickUp API responses to reduce API calls.
44
"""
55

6+
import pickle
7+
import sqlite3
8+
import time
69
from pathlib import Path
710

8-
import diskcache
9-
1011
# Cache location: ~/.keyup/cache/
1112
CACHE_DIR = Path.home() / ".keyup" / "cache"
13+
CACHE_FILE = CACHE_DIR / "keyup_cache.db"
1214

1315
# TTL values in seconds
1416
TEAMS_TTL = 24 * 60 * 60 # 24 hours
15-
SPACES_TTL = 24 * 60 * 60 # 24 hours
16-
PROJECTS_TTL = 24 * 60 * 60 # 24 hours
1717
LISTS_TTL = 24 * 60 * 60 # 24 hours
1818
TASKS_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

2773
def 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)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ classifiers = [
3131
"Topic :: Software Development",
3232
]
3333
dependencies = [
34-
"python-dotenv", "colorist", "pyclickup", "inquirer", "cyclopts", "diskcache"
34+
"python-dotenv", "colorist", "pyclickup", "inquirer", "cyclopts"
3535
]
3636

3737
[project.optional-dependencies]

tests/test_cache.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ def test_get_cache_creates_directory(self, tmp_path):
4141
shutil.rmtree(str(cache_dir))
4242
shutil.move(str(backup_path), str(cache_dir))
4343

44-
@patch("keyup.cli.cache.diskcache.Cache")
44+
@patch("keyup.cli.cache.SQLiteCache")
4545
@patch("keyup.cli.cache.Path")
46-
def test_get_cache_returns_cache_instance(self, mock_path_class, mock_cache):
47-
"""Test that get_cache returns a Cache instance."""
46+
def test_get_cache_returns_cache_instance(self, mock_path_class, mock_sqlite_cache):
47+
"""Test that get_cache returns a SQLiteCache instance."""
4848
mock_path_class.return_value = Mock()
4949
cache = get_cache()
50-
assert cache is mock_cache.return_value
50+
assert cache is mock_sqlite_cache.return_value
5151

5252

5353
class TestGetTeamsData:

uv.lock

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)