Skip to content

Commit 1aa8725

Browse files
committed
generate jupyter notebook from leetcode
1 parent b201526 commit 1aa8725

File tree

9 files changed

+416
-8
lines changed

9 files changed

+416
-8
lines changed

jupyterlab_leetcode/handlers/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from .base_handler import BaseHandler
44
from .cookie_handler import GetCookieHandler
5-
from .leetcode_handler import (LeetCodeProfileHandler, LeetCodeQuestionHandler,
5+
from .leetcode_handler import (CreateNotebookHandler, LeetCodeProfileHandler,
6+
LeetCodeQuestionHandler,
67
LeetCodeStatisticsHandler)
78

89

@@ -14,6 +15,7 @@ def setup_handlers(web_app):
1415
LeetCodeProfileHandler,
1516
LeetCodeStatisticsHandler,
1617
LeetCodeQuestionHandler,
18+
CreateNotebookHandler,
1719
]
1820

1921
web_app.add_handlers(

jupyterlab_leetcode/handlers/cookie_handler.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from tornado.httpclient import AsyncHTTPClient
99
from tornado.httputil import HTTPHeaders
1010

11+
from ..utils.utils import first
1112
from .base_handler import BaseHandler
1213

1314
BROWSER_COOKIE_METHOD_MAP = {
@@ -43,8 +44,8 @@ def get(self):
4344
return
4445

4546
cj = BROWSER_COOKIE_METHOD_MAP[browser](domain_name="leetcode.com")
46-
cookie_session = next((c for c in cj if c.name == "LEETCODE_SESSION"), None)
47-
cookie_csrf = next((c for c in cj if c.name == "csrftoken"), None)
47+
cookie_session = first(cj, lambda c: c.name == "LEETCODE_SESSION")
48+
cookie_csrf = first(cj, lambda c: c.name == "csrftoken")
4849
exist = bool(cookie_session and cookie_csrf)
4950
expired = exist and (
5051
cast(Cookie, cookie_session).is_expired()

jupyterlab_leetcode/handlers/leetcode_handler.py

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import json
2-
from typing import Any, Mapping, cast
2+
import os
3+
from typing import Any, Mapping, cast, overload
34

45
import tornado
56
from tornado.gen import multi
67
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPResponse
78
from tornado.httputil import HTTPHeaders
89

10+
from ..utils.notebook_generator import NotebookGenerator
911
from .base_handler import BaseHandler
1012

1113
LEETCODE_GRAPHQL_URL = "https://leetcode.com/graphql"
@@ -16,7 +18,15 @@
1618
class LeetCodeHandler(BaseHandler):
1719
"""Base handler for LeetCode-related requests."""
1820

19-
async def graphql(self, name: str, query: QueryType) -> None:
21+
@overload
22+
async def graphql(self, name: str, query: QueryType) -> None: ...
23+
24+
@overload
25+
async def graphql(
26+
self, name: str, query: QueryType, returnJson=True
27+
) -> dict[str, Any]: ...
28+
29+
async def graphql(self, name: str, query: QueryType, returnJson=False):
2030
self.log.debug(f"Fetching LeetCode {name} data...")
2131
client = AsyncHTTPClient()
2232
req = HTTPRequest(
@@ -34,6 +44,8 @@ async def graphql(self, name: str, query: QueryType) -> None:
3444
self.finish(json.dumps({"message": f"Failed to fetch LeetCode {name}"}))
3545
return
3646
else:
47+
if returnJson:
48+
return json.loads(resp.body)
3749
self.finish(resp.body)
3850

3951
async def graphql_multi(
@@ -70,6 +82,55 @@ async def graphql_multi(
7082
else:
7183
return cast("dict[str, HTTPResponse]", responses)
7284

85+
async def get_question_detail(self, title_slug: str) -> dict[str, Any]:
86+
resp = await self.graphql(
87+
name="question_detail",
88+
query={
89+
"query": """query questionData($titleSlug: String!) {
90+
question(titleSlug: $titleSlug) {
91+
questionId
92+
questionFrontendId
93+
submitUrl
94+
questionDetailUrl
95+
title
96+
titleSlug
97+
content
98+
isPaidOnly
99+
difficulty
100+
likes
101+
dislikes
102+
isLiked
103+
similarQuestions
104+
exampleTestcaseList
105+
topicTags {
106+
name
107+
slug
108+
translatedName
109+
}
110+
codeSnippets {
111+
lang
112+
langSlug
113+
code
114+
}
115+
stats
116+
hints
117+
solution {
118+
id
119+
canSeeDetail
120+
paidOnly
121+
hasVideoSolution
122+
paidOnlyVideo
123+
}
124+
status
125+
sampleTestCase
126+
}
127+
}""",
128+
"variables": {"titleSlug": title_slug},
129+
},
130+
returnJson=True,
131+
)
132+
return resp
133+
73134

74135
class LeetCodeProfileHandler(LeetCodeHandler):
75136
route = r"leetcode/profile"
@@ -264,3 +325,50 @@ async def post(self):
264325
},
265326
},
266327
)
328+
329+
330+
class CreateNotebookHandler(LeetCodeHandler):
331+
route = r"notebook/create"
332+
333+
@tornado.web.authenticated
334+
async def post(self):
335+
body = self.get_json_body()
336+
if not body:
337+
self.set_status(400)
338+
self.finish({"message": "Request body is required"})
339+
return
340+
341+
body = cast("dict[str, str]", body)
342+
title_slug = cast(str, body.get("titleSlug", ""))
343+
if not title_slug:
344+
self.set_status(400)
345+
self.finish({"message": "titleSlug is required"})
346+
return
347+
348+
question = await self.get_question_detail(title_slug)
349+
question = question.get("data", {}).get("question")
350+
if not question:
351+
self.set_status(404)
352+
self.finish({"message": "Question not found"})
353+
return
354+
355+
self.log.info(question)
356+
357+
notebook_generator = self.settings.get("notebook_generator")
358+
if not notebook_generator:
359+
template_path = os.path.join(
360+
os.path.dirname(os.path.realpath(__file__)),
361+
"..",
362+
"utils",
363+
"notebook.template.json",
364+
)
365+
if not os.path.exists(template_path):
366+
self.set_status(500)
367+
self.finish({"message": "Notebook template not found"})
368+
return
369+
370+
notebook_generator = NotebookGenerator(template_path)
371+
self.settings.update(notebook_generator=notebook_generator)
372+
373+
file_path = notebook_generator.generate(question)
374+
self.finish({"filePath": file_path})
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
{
2+
"nbformat": 4,
3+
"nbformat_minor": 2,
4+
"metadata": {
5+
"kernelspec": {
6+
"display_name": "Python 3",
7+
"language": "python",
8+
"name": "python3"
9+
},
10+
"authors": [
11+
{
12+
"name": "jupyterlab-leetcode"
13+
}
14+
],
15+
"language_info": {
16+
"codemirror_mode": {
17+
"name": "ipython",
18+
"version": 3
19+
},
20+
"file_extension": ".py",
21+
"mimetype": "text/x-python",
22+
"name": "python",
23+
"nbconvert_exporter": "python",
24+
"pygments_lexer": "ipython3",
25+
"version": null
26+
},
27+
"leetcode_question_info": {
28+
"submitUrl": null,
29+
"sampleTestCase": null,
30+
"questionId": null,
31+
"questionFrontendId": null,
32+
"questionDetailUrl": null
33+
}
34+
},
35+
"cells": [
36+
{
37+
"cell_type": "markdown",
38+
"metadata": {
39+
"id": "title"
40+
},
41+
"source": [
42+
"### Id. Title"
43+
]
44+
},
45+
{
46+
"cell_type": "markdown",
47+
"metadata": {
48+
"id": "content"
49+
},
50+
"source": [
51+
"HTML content description"
52+
]
53+
},
54+
{
55+
"cell_type": "markdown",
56+
"metadata": {
57+
"id": "extra"
58+
},
59+
"source": [
60+
"difficulty",
61+
"- Topic tags",
62+
"urls",
63+
"hints"
64+
]
65+
},
66+
{
67+
"cell_type": "markdown",
68+
"metadata": {
69+
"id": "test"
70+
},
71+
"source": [
72+
"sample test case"
73+
]
74+
},
75+
{
76+
"cell_type": "markdown",
77+
"metadata": {
78+
"id": "idea"
79+
},
80+
"source": [
81+
"---\n",
82+
"What's your idea?\n",
83+
"\n",
84+
"---"
85+
]
86+
},
87+
{
88+
"cell_type": "code",
89+
"execution_count": null,
90+
"metadata": {
91+
"id": "code"
92+
},
93+
"outputs": [],
94+
"source": [
95+
"Python 3 code definition"
96+
]
97+
},
98+
{
99+
"cell_type": "code",
100+
"execution_count": null,
101+
"metadata": {
102+
"id": "run"
103+
},
104+
"outputs": [],
105+
"source": [
106+
"s = Solution()\n",
107+
"s.func()"
108+
]
109+
}
110+
]
111+
}

0 commit comments

Comments
 (0)