8000 fix cache · noworneverev/leetcode-api@4a2f94b · GitHub
[go: up one dir, main page]

Skip to content

Commit 4a2f94b

Browse files
committed
fix cache
1 parent 2009389 commit 4a2f94b

File tree

1 file changed

+116
-106
lines changed

1 file changed

+116
-106
lines changed

api.py

Lines changed: 116 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,149 @@
11
import asyncio
2+
import time
23
from contextlib import asynccontextmanager
34
from fastapi import FastAPI, HTTPException
45
import httpx
6+
from typing import Dict
57
import uvicorn
68

7-
url = "https://leetcode.com/graphql"
9+
app = FastAPI()
10+
leetcode_url = "https://leetcode.com/graphql"
11+
client = httpx.AsyncClient()
812

9-
all_questions = []
10-
question_details = {}
11-
id_to_slug = {}
12-
slug_to_id = {}
13-
frontendid_to_slug = {}
14-
slug_to_frontendid = {}
13+
class QuestionCache:
14+
def __init__(self):
15+
self.questions: Dict[str, dict] = {}
16+
self.slug_to_id: Dict[str, str] = {}
17+
self.frontend_id_to_slug: Dict[str, str] = {}
18+
self.question_details: Dict[str, dict] = {}
19+
self.last_updated: float = 0
20+
self.update_interval: int = 3600
21+
self.lock = asyncio.Lock()
1522

16-
async def fetch_all_questions_data():
17-
async with httpx.AsyncClient() as client:
18-
payload = {
19-
"query": """query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
20-
problemsetQuestionList: questionList(
21-
categorySlug: $categorySlug
22-
limit: $limit
23-
skip: $skip
24-
filters: $filters
25-
) {
26-
total: totalNum
27-
questions: data {
28-
questionId
29-
questionFrontendId
30-
title
31-
titleSlug
32-
}
23+
async def initialize(self):
24+
async with self.lock:
25+
if not self.questions or (time.time() - self.last_updated) > self.update_interval:
26+
await self._fetch_all_questions()
27+
self.last_updated = time.time()
28+
29+
async def _fetch_all_questions(self):
30+
query = """query problemsetQuestionList {
31+
problemsetQuestionList: questionList(
32+
categorySlug: ""
33+
limit: 10000
34+
skip: 0
35+
filters: {}
36+
) {
37+
questions: data {
38+
questionId
39+
questionFrontendId
40+
title
41+
titleSlug
3342
}
34-
}""",
35-
"variables": {"categorySlug": "", "limit": 10000, "skip": 0, "filters": {}}
36-
}
43+
}
44+
}"""
45+
3746
try:
38-
response = await client.post(url, json=payload)
47+
response = await client.post(leetcode_url, json={"query": query})
3948
if response.status_code == 200:
4049
data = response.json()
41-
global all_questions, id_to_slug, slug_to_id, frontendid_to_slug, slug_to_frontendid
42-
all_questions = data["data"]["problemsetQuestionList"]["questions"]
43-
id_to_slug.clear()
44-
slug_to_id.clear()
45-
frontendid_to_slug.clear()
46-
slug_to_frontendid.clear()
47-
for q in all_questions:
48-
id_to_slug[q["questionId"]] = q["titleSlug"]
49-
slug_to_id[q["titleSlug"]] = q["questionId"]
50-
frontendid_to_slug[q["questionFrontendId"]] = q["titleSlug"]
51-
slug_to_frontendid[q["titleSlug"]] = q["questionFrontendId"]
50+
questions = data["data"]["problemsetQuestionList"]["questions"]
51+
52+
self.questions.clear()
53+
self.slug_to_id.clear()
54+
self.frontend_id_to_slug.clear()
55+
56+
for q in questions:
57+
self.questions[q["questionId"]] = q
58+
self.slug_to_id[q["titleSlug"]] = q["questionId"]
59+
self.frontend_id_to_slug[q["questionFrontendId"]] = q["titleSlug"]
5260
except Exception as e:
53-
print(f"Error fetching question list: {e}")
61+
print(f"Error updating questions: {e}")
5462

55-
async def fetch_question_data(title_slug: str):
56-
async with httpx.AsyncClient() as client:
57-
for _ in range(5):
58-
payload = {
59-
"query": """query questionData($titleSlug: String!) {
60-
question(titleSlug: $titleSlug) {
61-
questionId
62-
questionFrontendId
63-
title
64-
content
65-
likes
66-
dislikes
67-
stats
68-
similarQuestions
69-
categoryTitle
70-
hints
71-
topicTags { name }
72-
companyTags { name }
73-
difficulty
74-
isPaidOnly
75-
solution { canSeeDetail content }
76-
hasSolution
77-
hasVideoSolution
78-
}
79-
}""",
80-
"variables": {"titleSlug": title_slug}
81-
}
82-
try:
83-
response = await client.post(url, json=payload)
84-
if response.status_code == 200:
85-
data = response.json()
86-
question = data["data"]["question"]
87-
question["url"] = f"https://leetcode.com/problems/{title_slug}/"
88-
return question
89-
except Exception as e:
90-
print(f"Error fetching question data: {e}")
91-
await asyncio.sleep(1)
92-
return None
93-
94-
async def periodic_cache_update():
95-
while True:
96-
await fetch_all_questions_data()
97-
await asyncio.sleep(3600)
63+
cache = QuestionCache()
9864

9965
@asynccontextmanager
10066
async def lifespan(app: FastAPI):
101-
await fetch_all_questions_data()
102-
update_task = asyncio.create_task(periodic_cache_update())
67+
await cache.initialize()
10368
yield
104-
update_task.cancel()
105-
try:
106-
await update_task
107-
except asyncio.CancelledError:
108-
pass
10969

11070
app = FastAPI(lifespan=lifespan)
11171

72+
async def fetch_with_retry(url: str, payload: dict, retries: int = 3):
73+
for _ in range(retries):
74+
try:
75+
response = await client.post(url, json=payload)
76+
if response.status_code == 200:
77+
return response.json()
78+
except Exception as e:
79+
print(f"Request failed: {e}")
80+
await asyncio.sleep(1)
81+
return None
82+
11283
@app.get("/questions")
11384
async def get_all_questions():
85+
await cache.initialize()
11486
return [{
11587
"id": q["questionId"],
11688
"frontend_id": q["questionFrontendId"],
11789
"title": q["title"],
11890
"title_slug": q["titleSlug"],
11991
"url": f"https://leetcode.com/problems/{q['titleSlug']}/"
120-
} for q in all_questions]
92+
} for q in cache.questions.values()]
12193

12294
@app.get("/question/{identifier}")
12395
async def get_question(identifier: str):
124-
if identifier in frontendid_to_slug:
125-
slug = frontendid_to_slug[identifier]
126-
elif identifier in slug_to_id:
96+
await cache.initialize()
97+
98+
if identifier in cache.frontend_id_to_slug:
99+
slug = cache.frontend_id_to_slug[identifier]
100+
elif identifier in cache.slug_to_id:
127101
slug = identifier
128102
else:
129103
raise HTTPException(status_code=404, detail="Question not found")
130-
qid = slug_to_id[slug]
131-
if qid not in question_details:
132-
data = await fetch_question_data(slug)
133-
if not data:
134-
raise HTTPException(status_code=404, detail="Question data unavailable")
135-
question_details[qid] = data
136-
return question_details[qid]
104+
105+
# check cache
106+
question_id = cache.slug_to_id[slug]
107+
if question_id in cache.question_details:
108+
return cache.question_details[question_id]
109+
110+
# not in cache, fetch from leetcode
111+
query = """query questionData($titleSlug: String!) {
112+
question(titleSlug: $titleSlug) {
113+
questionId
114+
questionFrontendId
115+
title
116+
content
117+
likes
118+
dislikes
119+
stats
120+
similarQuestions
121+
categoryTitle
122+
hints
123+
topicTags { name }
124+
companyTags { name }
125+
difficulty
126+
isPaidOnly
127+
solution { canSeeDetail content }
128+
hasSolution
129+
hasVideoSolution
130+
}
131+
}"""
132+
133+
payload = {
134+
"query": query,
135+
"variables": {"titleSlug": slug}
136+
}
137+
138+
data = await fetch_with_retry(leetcode_url, payload)
139+
if not data or "data" not in data or not data["data"]["question"]:
140+
raise HTTPException(status_code=404, detail="Question data not found")
141+
142+
question_data = data["data"]["question"]
143+
question_data["url"] = f"https://leetcode.com/problems/{slug}/"
144+
145+
cache.question_details[question_id] = question_data
146+
return question_data
137147

138148
@app.get("/user/{username}")
139149
async def get_user_profile(username: str):
@@ -173,7 +183,7 @@ async def get_user_profile(username: str):
173183
}
174184

175185
try:
176-
response = await client.post(url, json=payload)
186+
response = await client.post(leetcode_url, json=payload)
177187
if response.status_code == 200:
178188
data = response.json()
179189
if not data.get("data", {}).get("matchedUser"):
@@ -204,7 +214,7 @@ async def get_daily_challenge():
204214
payload = {"query": query}
205215

206216
try:
207-
response = await client.post(url, json=payload)
217+
response = await client.post(leetcode_url, json=payload)
208218
if response.status_code == 200:
209219
data = response.json()
210220
return data["data"]["activeDailyCodingChallengeQuestion"]
@@ -241,7 +251,7 @@ async def get_user_contest_history(username: str):
241251
}
242252

243253
try:
244-
response = await client.post(url, json=payload)
254+
response = await client.post(leetcode_url, json=payload)
245255
if response.status_code == 200:
246256
data = response.json()
247257
if not data.get("data"):
@@ -271,7 +281,7 @@ async def get_recent_submissions(username: str, limit: int = 20):
271281
}
272282

273283
try:
274-
response = await client.post(url, json=payload)
284+
response = await client.post(leetcode_url, json=payload)
275285
if response.status_code == 200:
276286
data = response.json()
277287
if "errors" in data:
@@ -308,7 +318,7 @@ async def get_problems_by_topic(topic: str):
308318
}
309319

310320
try:
311-
response = await client.post(url, json=payload)
321+
response = await client.post(leetcode_url, json=payload)
312322
if response.status_code == 200:
313323
data = response.json()
314324
return data["data"]["problemsetQuestionList"]["questions"]

0 commit comments

Comments
 (0)
0