|
6 | 6 | import uvicorn |
7 | 7 | from fastapi import FastAPI, HTTPException |
8 | 8 | from fastapi.responses import HTMLResponse |
| 9 | +import random |
9 | 10 |
|
10 | 11 | app = FastAPI() |
11 | 12 | leetcode_url = "https://leetcode.com/graphql" |
@@ -146,40 +147,41 @@ async def get_problem(id_or_slug: str): |
146 | 147 | cache.question_details[question_id] = question_data |
147 | 148 | return question_data |
148 | 149 |
|
149 | | -@app.get("/problems/{topic}", tags=["Problems"]) |
150 | | -async def get_problems_by_topic(topic: str): |
151 | | - async with httpx.AsyncClient() as client: |
152 | | - query = """query problemsetQuestionList($categorySlug: String, $filters: QuestionListFilterInput) { |
153 | | - problemsetQuestionList: questionList( |
154 | | - categorySlug: $categorySlug |
155 | | - filters: $filters |
156 | | - ) { |
157 | | - questions: data { |
158 | | - questionId |
159 | | - title |
160 | | - titleSlug |
161 | | - difficulty |
162 | | - topicTags { name } |
163 | | - } |
164 | | - } |
165 | | - }""" |
166 | | - |
167 | | - payload = { |
168 | | - "query": query, |
169 | | - "variables": { |
170 | | - "categorySlug": "", |
171 | | - "filters": {"tags": [topic]} |
172 | | - } |
173 | | - } |
174 | | - |
175 | | - try: |
176 | | - response = await client.post(leetcode_url, json=payload) |
177 | | - if response.status_code == 200: |
178 | | - data = response.json() |
179 | | - return data["data"]["problemsetQuestionList"]["questions"] |
180 | | - raise HTTPException(status_code=response.status_code, detail="Error fetching problems by topic") |
181 | | - except Exception as e: |
182 | | - raise HTTPException(status_code=500, detail=str(e)) |
| 150 | +@app.get("/search", tags=["Problems"]) |
| 151 | +async def search_problems(query: str): |
| 152 | + """ |
| 153 | + Search for problems whose titles contain the given query (case-insensitive). |
| 154 | + """ |
| 155 | + await cache.initialize() |
| 156 | + query_lower = query.lower() |
| 157 | + results = [] |
| 158 | + for q in cache.questions.values(): |
| 159 | + if query_lower in q["title"].lower(): |
| 160 | + results.append({ |
| 161 | + "id": q["questionId"], |
| 162 | + "frontend_id": q["questionFrontendId"], |
| 163 | + "title": q["title"], |
| 164 | + "title_slug": q["titleSlug"], |
| 165 | + "url": f"https://leetcode.com/problems/{q['titleSlug']}/" |
| 166 | + }) |
| 167 | + return results |
| 168 | + |
| 169 | +@app.get("/random", tags=["Problems"]) |
| 170 | +async def get_random_problem(): |
| 171 | + """ |
| 172 | + Return a random problem from the cached questions. |
| 173 | + """ |
| 174 | + await cache.initialize() |
| 175 | + if not cache.questions: |
| 176 | + raise HTTPException(status_code=404, detail="No questions available") |
| 177 | + q = random.choice(list(cache.questions.values())) |
| 178 | + return { |
| 179 | + "id": q["questionId"], |
| 180 | + "frontend_id": q["questionFrontendId"], |
| 181 | + "title": q["title"], |
| 182 | + "title_slug": q["titleSlug"], |
| 183 | + "url": f"https://leetcode.com/problems/{q['titleSlug']}/" |
| 184 | + } |
183 | 185 |
|
184 | 186 | @app.get("/user/{username}", tags=["Users"]) |
185 | 187 | async def get_user_profile(username: str): |
@@ -328,132 +330,23 @@ async def get_daily_challenge(): |
328 | 330 | except Exception as e: |
329 | 331 | raise HTTPException(status_code=500, detail=str(e)) |
330 | 332 |
|
331 | | -@app.get("/", response_class=HTMLResponse, include_in_schema=False) |
332 | | -async def home(): |
333 | | - return """ |
334 | | - <!DOCTYPE html> |
335 | | - <html lang="en"> |
336 | | - <head> |
337 | | - <meta charset="UTF-8"> |
338 | | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
339 | | - <title>LeetCode API</title> |
340 | | - <script src="https://cdn.tailwindcss.com"></script> |
341 | | - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> |
342 | | - <style> |
343 | | - /* Dark mode styles */ |
344 | | - body.dark-mode { |
345 | | - background-color: #1a202c !important; |
346 | | - color: #e2e8f0; |
347 | | - } |
348 | | - |
349 | | - body.dark-mode .bg-white { |
350 | | - background-color: #2d3748 !important; |
351 | | - } |
352 | | -
|
353 | | - body.dark-mode .text-gray-800 { |
354 | | - color: #e2e8f0 !important; |
355 | | - } |
356 | | -
|
357 | | - body.dark-mode .text-gray-600 { |
358 | | - color: #cbd5e0 !important; |
359 | | - } |
360 | | -
|
361 | | - body.dark-mode .text-gray-500 { |
362 | | - color: #a0aec0 !important; |
363 | | - } |
| 333 | +@app.get("/health", tags=["Utility"]) |
| 334 | +async def health_check(): |
| 335 | + return {"status": "ok", "timestamp": time.time()} |
364 | 336 |
|
365 | | - body.dark-mode .bg-blue-500 { |
366 | | - background-color: #2563eb !important; |
367 | | - } |
| 337 | +@app.get("/refresh", tags=["Admin"]) |
| 338 | +async def refresh_cache(): |
| 339 | + # Force refresh the question cache regardless of the update interval |
| 340 | + async with cache.lock: |
| 341 | + await cache._fetch_all_questions() |
| 342 | + cache.last_updated = time.time() |
| 343 | + return {"detail": "Cache refreshed"} |
368 | 344 |
|
369 | | - body.dark-mode .bg-green-500 { |
370 | | - background-color: #059669 !important; |
371 | | - } |
372 | | -
|
373 | | - body.dark-mode .hover\:bg-blue-600:hover { |
374 | | - background-color: #1d4ed8 !important; |
375 | | - } |
376 | | -
|
377 | | - body.dark-mode .hover\:bg-green-600:hover { |
378 | | - background-color: #047857 !important; |
379 | | - } |
380 | | - </style> |
381 | | - </head> |
382 | | - <body class="bg-gray-100 min-h-screen flex items-center justify-center"> |
383 | | - <div class="max-w-2xl mx-4 text-center"> |
384 | | - <div class="bg-white rounded-lg shadow-lg p-8 space-y-6"> |
385 | | - <div class="flex justify-end"> |
386 | | - <button id="theme-toggle" class="text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-100 focus:outline-none"> |
387 | | - <i id="theme-icon" class="fas fa-moon"></i> |
388 | | - </button> |
389 | | - </div> |
390 | | - <h1 class="text-4xl font-bold text-gray-800 mb-4"> |
391 | | - LeetCode API Gateway |
392 | | - <i class="fas fa-rocket text-blue-500 ml-2"></i> |
393 | | - </h1> |
394 | | - |
395 | | - <p class="text-gray-600 text-lg"> |
396 | | - Explore LeetCode data through our API endpoints. Get problem details, |
397 | | - user statistics, submissions history, and more! |
398 | | - </p> |
399 | | -
|
400 | | - <div class="flex flex-col sm:flex-row justify-center gap-4"> |
401 | | - <a href="https://leetcode-api-pied.vercel.app/docs" |
402 | | - target="_blank" |
403 | | - class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg |
404 | | - transition-all duration-300 transform hover:scale-105 |
405 | | - flex items-center justify-center gap-2"> |
406 | | - <i class="fas fa-book-open"></i> |
407 | | - API Documentation |
408 | | - </a> |
409 | | - |
410 | | - <a href="https://docs.google.com/spreadsheets/d/1sRWp95wqo3a7lLBbtNd_3KkTyGjx_9sctTOL5JOb6pA/edit?usp=sharing" |
411 | | - target="_blank" |
412 | | - class="bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-lg |
413 | | - transition-all duration-300 transform hover:scale-105 |
414 | | - flex items-center justify-center gap-2"> |
415 | | - <i class="fas fa-table"></i> |
416 | | - Google Sheet (Updated Daily) |
417 | | - </a> |
418 | | - </div> |
419 | | -
|
420 | | - <p class="text-gray-500 text-sm mt-8 flex items-center justify-center gap-1"> |
421 | | - Made with ❤️ by |
422 | | - <a href="https://noworneverev.github.io/" target="_blank" |
423 | | - class="text-blue-500 font-semibold hover:text-blue-600 transition duration-300"> |
424 | | - Yan-Ying Liao |
425 | | - </a> |
426 | | - </p> |
427 | | - </div> |
428 | | - </div> |
429 | | -
|
430 | | - <script> |
431 | | - const themeToggleBtn = document.getElementById('theme-toggle'); |
432 | | - const themeIcon = document.getElementById('theme-icon'); |
433 | | - const body = document.body; |
434 | | -
|
435 | | - // Check local storage for theme preference |
436 | | - const currentTheme = localStorage.getItem('theme'); |
437 | | - if (currentTheme === 'dark') { |
438 | | - body.classList.add('dark-mode'); |
439 | | - themeIcon.classList.replace('fa-moon', 'fa-sun'); |
440 | | - } |
441 | | -
|
442 | | - themeToggleBtn.addEventListener('click', () => { |
443 | | - body.classList.toggle('dark-mode'); |
444 | | - if (body.classList.contains('dark-mode')) { |
445 | | - themeIcon.classList.replace('fa-moon', 'fa-sun'); |
446 | | - localStorage.setItem('theme', 'dark'); |
447 | | - } else { |
448 | | - themeIcon.classList.replace('fa-sun', 'fa-moon'); |
449 | | - localStorage.setItem('theme', 'light'); |
450 | | - } |
451 | | - }); |
452 | | - </script> |
453 | | - </body> |
454 | | - </html> |
455 | | - """ |
456 | 345 |
|
| 346 | +@app.get("/", response_class=HTMLResponse, include_in_schema=False) |
| 347 | +async def home(): |
| 348 | + from src.api.home import HOME_PAGE_HTML |
| 349 | + return HOME_PAGE_HTML |
457 | 350 |
|
458 | 351 | if __name__ == "__main__": |
459 | 352 | uvicorn.run(app, host="0.0.0.0", port=8000) |
0 commit comments