Skip to content

Commit 11cacb6

Browse files
Introduce caching for LeetCode API interactions
Added a CachedLeetCodeAPI class to improve performance by caching API responses. Updated PracticeHandler to utilize cached API methods instead of direct API calls. Removed unused fields from LeetCode API queries to streamline data fetching.
1 parent 11117f0 commit 11cacb6

File tree

5 files changed

+126
-59
lines changed

5 files changed

+126
-59
lines changed

api/CachedLeetCodeAPI.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import json
2+
import hashlib
3+
import tempfile
4+
from typing import Any, Callable, Optional
5+
from pathlib import Path
6+
import time
7+
8+
from api.LeetCodeAPI import LeetCodeAPI
9+
from utils.logger import log, LogLevel
10+
11+
12+
class CachedLeetCodeAPI:
13+
def __init__(self, cache_dir: Optional[str] = None, cache_expiry: int = 3600):
14+
"""
15+
Initialize the CachedLeetCodeAPI with caching functionality.
16+
:param cache_dir: Directory to store cached API results. Defaults to OS temp directory if None.
17+
:param cache_expiry: Expiry time for cache in seconds (default: 1 hour).
18+
"""
19+
# Use the system's temporary directory if no cache directory is provided
20+
self.cache_dir = Path(cache_dir or tempfile.gettempdir()) / "cached_leetcode_api"
21+
self.cache_expiry = cache_expiry # Expiry time in seconds
22+
self.cache_dir.mkdir(parents=True, exist_ok=True) # Create cache directory if it doesn't exist
23+
24+
self.api = LeetCodeAPI() # Delegate actual API calls to existing LeetCodeAPI class
25+
26+
def _get_cache_key(self, unique_id: str) -> str:
27+
"""
28+
Generate a cache key based on a unique identifier (e.g., query or API parameters).
29+
:param unique_id: Unique identifier for the API request (e.g., slug, query, variable JSON).
30+
:return: Cache filename based on the MD5 hash of the unique ID.
31+
"""
32+
return hashlib.md5(unique_id.encode('utf-8')).hexdigest()
33+
34+
def _read_from_cache(self, cache_key: str) -> Any:
35+
"""
36+
Read data from cache if it exists and is valid.
37+
:param cache_key: Key corresponding to the cached data.
38+
:return: Cached data, or None if cache is invalid or missing.
39+
"""
40+
cache_file = self.cache_dir / cache_key
41+
if cache_file.exists():
42+
# Check expiry
43+
if time.time() - cache_file.stat().st_mtime <= self.cache_expiry:
44+
with open(cache_file, "r") as f:
45+
return json.load(f)
46+
47+
return None # Cache miss or expired
48+
49+
def _write_to_cache(self, cache_key: str, data: Any) -> None:
50+
"""
51+
Write data to cache.
52+
:param cache_key: Key corresponding to the cached data.
53+
:param data: Data to cache.
54+
"""
55+
cache_file = self.cache_dir / cache_key
56+
with open(cache_file, "w") as f:
57+
json.dump(data, f)
58+
59+
def _fetch_with_cache(self, fetch_func: Callable, unique_id: str, *args, **kwargs) -> Any:
60+
"""
61+
Fetch data with caching.
62+
:param fetch_func: Function to fetch data if cache is not available.
63+
:param unique_id: Unique identifier for the request (e.g., API parameters).
64+
:param args: Positional arguments to pass to the fetch function.
65+
:param kwargs: Keyword arguments to pass to the fetch function.
66+
:return: Fetched or cached data.
67+
"""
68+
cache_key = self._get_cache_key(unique_id)
69+
70+
# Check cache first
71+
cached_data = self._read_from_cache(cache_key)
72+
if cached_data is not None:
73+
log(f"✅ Cache hit for {unique_id}", LogLevel.DEBUG)
74+
return cached_data
75+
76+
# Cache miss, call the API
77+
log(f"❌ Cache miss for {unique_id}. Fetching from API...", LogLevel.DEBUG)
78+
api_data = fetch_func(*args, **kwargs)
79+
80+
# Save API data to cache
81+
self._write_to_cache(cache_key, api_data)
82+
return api_data
83+
84+
# Cached versions of API methods
85+
def fetch_problems(self, *args, **kwargs):
86+
"""
87+
Cached version of fetch_problems
88+
"""
89+
unique_id = f"fetch_problems-{json.dumps([args, kwargs], sort_keys=True)}" # Unique key based on args
90+
return self._fetch_with_cache(self.api.fetch_problems, unique_id, *args, **kwargs)
91+
92+
def fetch_daily_challenge(self, *args, **kwargs):
93+
"""
94+
Cached version of fetch_daily_challenge
95+
"""
96+
return self._fetch_with_cache(self.api.fetch_daily_challenge, "fetch_daily_challenge")
97+
98+
def fetch_problem(self, problem_slug: str, *args, **kwargs):
99+
"""
100+
Cached version of fetch_problem
101+
"""
102+
unique_id = f"fetch_problem-{problem_slug}"
103+
return self._fetch_with_cache(self.api.fetch_problem, unique_id, problem_slug, *args, **kwargs)
104+
105+
def get_study_plan(self, slug: str, *args, **kwargs):
106+
"""
107+
Cached version of get_study_plan
108+
"""
109+
unique_id = f"get_study_plan-{slug}"
110+
return self._fetch_with_cache(self.api.get_study_plan, unique_id, slug, *args, **kwargs)

api/LeetCodeAPI.py

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def fetch_problems(
5050
acRate
5151
difficulty
5252
content
53-
frontendQuestionId: questionFrontendId
5453
title
5554
titleSlug
5655
codeSnippets {
@@ -127,17 +126,9 @@ def fetch_daily_challenge(self) -> Dict[str, Any]:
127126
question {
128127
titleSlug
129128
title
130-
translatedTitle
131129
content
132130
acRate
133131
difficulty
134-
freqBar
135-
frontendQuestionId: questionFrontendId
136-
isFavor
137-
paidOnly: isPaidOnly
138-
status
139-
hasVideoSolution
140-
hasSolution
141132
codeSnippets {
142133
lang
143134
code
@@ -177,7 +168,6 @@ def fetch_problem(self, problem_slug: str) -> Dict[str, Any]:
177168
query = """
178169
query getQuestionDetails($titleSlug: String!) {
179170
question(titleSlug: $titleSlug) {
180-
questionId
181171
title
182172
titleSlug
183173
content
@@ -227,52 +217,14 @@ def get_study_plan(self, slug: str) -> Dict[str, Any]:
227217
studyPlanV2Detail(planSlug: $slug) {
228218
slug
229219
name
230-
highlight
231-
staticCoverPicture
232-
colorPalette
233-
threeDimensionUrl
234220
description
235-
premiumOnly
236-
needShowTags
237-
awardDescription
238-
defaultLanguage
239-
award {
240-
name
241-
config {
242-
icon
243-
iconGif
244-
iconGifBackground
245-
}
246-
}
247-
relatedStudyPlans {
248-
cover
249-
highlight
250-
name
251-
slug
252-
premiumOnly
253-
}
254221
planSubGroups {
255222
slug
256223
name
257-
premiumOnly
258224
questionNum
259225
questions {
260-
translatedTitle
261-
titleSlug
262226
title
263-
questionFrontendId
264-
paidOnly
265-
id
266-
difficulty
267-
hasOfficialSolution
268-
topicTags {
269-
slug
270-
name
271-
}
272-
solutionInfo {
273-
solutionSlug
274-
solutionTopicId
275-
}
227+
titleSlug
276228
}
277229
}
278230
}

handlers/CacheHandler.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from api.CachedLeetCodeAPI import CachedLeetCodeAPI
2+
3+
cached_api = CachedLeetCodeAPI(cache_expiry=3600)

handlers/PracticeHandler.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from typing import List, Dict, Any, Optional
66

77
from utils.logger import log, LogLevel
8+
89
from handlers.SolutionHandler import SolutionHandler
9-
from api.LeetCodeAPI import LeetCodeAPI
10+
from handlers.CacheHandler import cached_api
1011

1112
difficulty_map = {
1213
"easy": "Easy",
@@ -17,7 +18,7 @@
1718

1819
class PracticeMode:
1920
def __init__(self):
20-
self.leetcode_api = LeetCodeAPI()
21+
pass
2122

2223
def handle(self, args):
2324
raise NotImplementedError("This method should be implemented by subclasses.")
@@ -93,7 +94,7 @@ def get_random_problem(
9394
:param difficulties: Difficulty levels of the problems (e.g., "Easy", "Medium", "Hard").
9495
:return: A random problem dictionary or None if no problems are found.
9596
"""
96-
problems = self.leetcode_api.fetch_problems(
97+
problems = cached_api.fetch_problems(
9798
limit=1000, difficulties=difficulties
9899
)
99100

@@ -109,7 +110,7 @@ def get_study_plan_problems(self, slug: str) -> List[Dict[str, Any]]:
109110
:param slug: The slug of the study plan (e.g., "leetcode-75").
110111
:return: A list of dictionaries, each containing details of a problem.
111112
"""
112-
study_plan = self.leetcode_api.get_study_plan(slug)
113+
study_plan = cached_api.get_study_plan(slug)
113114

114115
if not study_plan:
115116
raise Exception(f"❌ Study plan not found for slug: {slug}")
@@ -137,7 +138,10 @@ def get_random_study_plan_problem(self, slug: str) -> Optional[Dict[str, Any]]:
137138
return None
138139

139140
random_index = random.randint(0, len(problems) - 1)
140-
return problems[random_index]
141+
problem = problems[random_index]
142+
143+
# Get the problem in the right format
144+
return cached_api.fetch_problem(problem["titleSlug"])
141145

142146

143147
class RandomProblemMode(PracticeMode):
@@ -170,7 +174,7 @@ def handle(self, args):
170174

171175
try:
172176
# Use the LeetCodeAPI's fetch_daily_challenge method
173-
daily_challenge = self.leetcode_api.fetch_daily_challenge()
177+
daily_challenge = cached_api.fetch_daily_challenge()
174178
difficulty_label = difficulty_map[
175179
daily_challenge["question"]["difficulty"].lower()
176180
]
@@ -196,7 +200,7 @@ def handle(self, args):
196200

197201
for slug in args["problems"]:
198202
try:
199-
problem = self.leetcode_api.fetch_problem(slug.strip())
203+
problem = cached_api.fetch_problem(slug.strip())
200204

201205
if not problem:
202206
log(f"Problem with slug '{slug}' not found.", LogLevel.ERROR)
@@ -225,9 +229,6 @@ def handle(self, args):
225229
log("No problems found for the selected study plan.", LogLevel.ERROR)
226230
return
227231

228-
# Get the problem in the right format
229-
problem = self.leetcode_api.fetch_problem(problem["titleSlug"])
230-
231232
difficulty_label = difficulty_map[problem["difficulty"].lower()]
232233
url = f"https://leetcode.com/problems/{problem['titleSlug']}"
233234

utils/watch_and_submit.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import time
33

44
from api.LeetCodeAPI import LeetCodeAPI
5+
56
from utils.timer import start_timer
67
from utils.logger import log, LogLevel
78

0 commit comments

Comments
 (0)