jupyterlab-leetcode 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jupyterlab_leetcode/__init__.py +32 -0
- jupyterlab_leetcode/_version.py +4 -0
- jupyterlab_leetcode/handlers/__init__.py +31 -0
- jupyterlab_leetcode/handlers/base_handler.py +10 -0
- jupyterlab_leetcode/handlers/cookie_handler.py +87 -0
- jupyterlab_leetcode/handlers/leetcode_handler.py +464 -0
- jupyterlab_leetcode/labextension/package.json +217 -0
- jupyterlab_leetcode/labextension/schemas/jupyterlab-leetcode/package.json.orig +212 -0
- jupyterlab_leetcode/labextension/schemas/jupyterlab-leetcode/plugin.json +8 -0
- jupyterlab_leetcode/labextension/static/266.1f3531e8ca41f032af95.js +1 -0
- jupyterlab_leetcode/labextension/static/728.369f44bbf921c6ebc9ee.js +1 -0
- jupyterlab_leetcode/labextension/static/880.2d185bdba596d2d9e8b0.js +1 -0
- jupyterlab_leetcode/labextension/static/remoteEntry.6f8397bd75fe51149f55.js +1 -0
- jupyterlab_leetcode/labextension/static/style.js +4 -0
- jupyterlab_leetcode/labextension/static/third-party-licenses.json +22 -0
- jupyterlab_leetcode/tests/__init__.py +1 -0
- jupyterlab_leetcode/tests/test_handlers.py +13 -0
- jupyterlab_leetcode/utils/notebook.template.json +111 -0
- jupyterlab_leetcode/utils/notebook_generator.py +190 -0
- jupyterlab_leetcode/utils/utils.py +9 -0
- jupyterlab_leetcode-0.1.1.data/data/etc/jupyter/jupyter_server_config.d/jupyterlab_leetcode.json +7 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/install.json +5 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/package.json +217 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/schemas/jupyterlab-leetcode/package.json.orig +212 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/schemas/jupyterlab-leetcode/plugin.json +8 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/static/266.1f3531e8ca41f032af95.js +1 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/static/728.369f44bbf921c6ebc9ee.js +1 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/static/880.2d185bdba596d2d9e8b0.js +1 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/static/remoteEntry.6f8397bd75fe51149f55.js +1 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/static/style.js +4 -0
- jupyterlab_leetcode-0.1.1.data/data/share/jupyter/labextensions/jupyterlab-leetcode/static/third-party-licenses.json +22 -0
- jupyterlab_leetcode-0.1.1.dist-info/METADATA +202 -0
- jupyterlab_leetcode-0.1.1.dist-info/RECORD +35 -0
- jupyterlab_leetcode-0.1.1.dist-info/WHEEL +4 -0
- jupyterlab_leetcode-0.1.1.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from ._version import __version__
|
|
3
|
+
except ImportError:
|
|
4
|
+
# Fallback when using the package in dev mode without installing
|
|
5
|
+
# in editable mode with pip. It is highly recommended to install
|
|
6
|
+
# the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
warnings.warn("Importing 'jupyterlab_leetcode' outside a proper installation.")
|
|
10
|
+
__version__ = "dev"
|
|
11
|
+
from .handlers import setup_handlers
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _jupyter_labextension_paths():
|
|
15
|
+
return [{"src": "labextension", "dest": "jupyterlab-leetcode"}]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _jupyter_server_extension_points():
|
|
19
|
+
return [{"module": "jupyterlab_leetcode"}]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_jupyter_server_extension(server_app):
|
|
23
|
+
"""Registers the API handler to receive HTTP requests from the frontend extension.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
server_app: jupyterlab.labapp.LabApp
|
|
28
|
+
JupyterLab application instance
|
|
29
|
+
"""
|
|
30
|
+
setup_handlers(server_app.web_app)
|
|
31
|
+
name = "jupyterlab_leetcode"
|
|
32
|
+
server_app.log.info(f"Registered {name} server extension")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from jupyter_server.utils import url_path_join
|
|
2
|
+
|
|
3
|
+
from .base_handler import BaseHandler
|
|
4
|
+
from .cookie_handler import GetCookieHandler
|
|
5
|
+
from .leetcode_handler import (CreateNotebookHandler, LeetCodeProfileHandler,
|
|
6
|
+
LeetCodeQuestionHandler,
|
|
7
|
+
LeetCodeStatisticsHandler,
|
|
8
|
+
SubmitNotebookHandler)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_handlers(web_app):
|
|
12
|
+
host_pattern = ".*$"
|
|
13
|
+
base_url = web_app.settings["base_url"]
|
|
14
|
+
handlers: list[type[BaseHandler]] = [
|
|
15
|
+
GetCookieHandler,
|
|
16
|
+
LeetCodeProfileHandler,
|
|
17
|
+
LeetCodeStatisticsHandler,
|
|
18
|
+
LeetCodeQuestionHandler,
|
|
19
|
+
CreateNotebookHandler,
|
|
20
|
+
SubmitNotebookHandler,
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
web_app.add_handlers(
|
|
24
|
+
host_pattern,
|
|
25
|
+
list(
|
|
26
|
+
map(
|
|
27
|
+
lambda h: (url_path_join(base_url, "jupyterlab-leetcode", h.route), h),
|
|
28
|
+
handlers,
|
|
29
|
+
)
|
|
30
|
+
),
|
|
31
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from jupyter_server.base.handlers import APIHandler
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseHandler(APIHandler):
|
|
5
|
+
"""Base handler for JupyterLab LeetCode extension.
|
|
6
|
+
This class extends APIHandler to provide a base for other handlers.
|
|
7
|
+
It can be used to define common functionality or properties for all handlers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
route = r""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from http.cookiejar import Cookie
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import browser_cookie3
|
|
7
|
+
import tornado
|
|
8
|
+
from tornado.httpclient import AsyncHTTPClient
|
|
9
|
+
from tornado.httputil import HTTPHeaders
|
|
10
|
+
|
|
11
|
+
from ..utils.utils import first
|
|
12
|
+
from .base_handler import BaseHandler
|
|
13
|
+
|
|
14
|
+
BROWSER_COOKIE_METHOD_MAP = {
|
|
15
|
+
"chrome": browser_cookie3.chrome,
|
|
16
|
+
"chromium": browser_cookie3.chromium,
|
|
17
|
+
"opera": browser_cookie3.opera,
|
|
18
|
+
"opera_gx": browser_cookie3.opera_gx,
|
|
19
|
+
"brave": browser_cookie3.brave,
|
|
20
|
+
"edge": browser_cookie3.edge,
|
|
21
|
+
"vivaldi": browser_cookie3.vivaldi,
|
|
22
|
+
"firefox": browser_cookie3.firefox,
|
|
23
|
+
"librewolf": browser_cookie3.librewolf,
|
|
24
|
+
"safari": browser_cookie3.safari,
|
|
25
|
+
"arc": browser_cookie3.arc,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GetCookieHandler(BaseHandler):
|
|
30
|
+
route = r"cookies"
|
|
31
|
+
|
|
32
|
+
@tornado.web.authenticated
|
|
33
|
+
def get(self):
|
|
34
|
+
self.log.debug("Loading all cookies for LeetCode...")
|
|
35
|
+
browser = self.get_query_argument("browser", "", strip=True)
|
|
36
|
+
if not browser:
|
|
37
|
+
self.set_status(400)
|
|
38
|
+
self.finish(json.dumps({"message": "Browser parameter is required"}))
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if browser not in BROWSER_COOKIE_METHOD_MAP:
|
|
42
|
+
self.set_status(400)
|
|
43
|
+
self.finish(json.dumps({"message": f"Unsupported browser: {browser}"}))
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
cj = BROWSER_COOKIE_METHOD_MAP[browser](domain_name="leetcode.com")
|
|
47
|
+
cookie_session = first(cj, lambda c: c.name == "LEETCODE_SESSION")
|
|
48
|
+
cookie_csrf = first(cj, lambda c: c.name == "csrftoken")
|
|
49
|
+
exist = bool(cookie_session and cookie_csrf)
|
|
50
|
+
expired = exist and (
|
|
51
|
+
cast(Cookie, cookie_session).is_expired()
|
|
52
|
+
or cast(Cookie, cookie_csrf).is_expired()
|
|
53
|
+
)
|
|
54
|
+
checked = exist and not expired
|
|
55
|
+
|
|
56
|
+
resp = {"exist": exist, "expired": expired, "checked": checked}
|
|
57
|
+
|
|
58
|
+
if checked:
|
|
59
|
+
cookie_session_expires = cast(Cookie, cookie_session).expires
|
|
60
|
+
max_age = (
|
|
61
|
+
cookie_session_expires - int(time.time())
|
|
62
|
+
if cookie_session_expires is not None
|
|
63
|
+
else 3600 * 24 * 14
|
|
64
|
+
)
|
|
65
|
+
self.set_cookie("leetcode_browser", browser, max_age=max_age)
|
|
66
|
+
self.settings.update(
|
|
67
|
+
leetcode_browser=browser,
|
|
68
|
+
leetcode_cookiejar=cj,
|
|
69
|
+
leetcode_headers=HTTPHeaders(
|
|
70
|
+
{
|
|
71
|
+
"Cookie": "; ".join(f"{c.name}={c.value}" for c in cj),
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"Origin": "https://leetcode.com",
|
|
74
|
+
"Referer": "https://leetcode.com/",
|
|
75
|
+
"X-CsrfToken": (
|
|
76
|
+
cookie_csrf.value
|
|
77
|
+
if cookie_csrf and cookie_csrf.value
|
|
78
|
+
else ""
|
|
79
|
+
),
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
AsyncHTTPClient.configure(
|
|
84
|
+
None, defaults=dict(user_agent=self.request.headers.get("user-agent"))
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self.finish(json.dumps(resp))
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Mapping, cast, overload
|
|
4
|
+
|
|
5
|
+
import tornado
|
|
6
|
+
from tornado.gen import multi
|
|
7
|
+
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPResponse
|
|
8
|
+
from tornado.httputil import HTTPHeaders
|
|
9
|
+
|
|
10
|
+
from ..utils.notebook_generator import NotebookGenerator
|
|
11
|
+
from ..utils.utils import first
|
|
12
|
+
from .base_handler import BaseHandler
|
|
13
|
+
|
|
14
|
+
LEETCODE_URL = "https://leetcode.com"
|
|
15
|
+
LEETCODE_GRAPHQL_URL = f"{LEETCODE_URL}/graphql"
|
|
16
|
+
|
|
17
|
+
type QueryType = dict[str, str | Mapping[str, Any]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LeetCodeHandler(BaseHandler):
|
|
21
|
+
"""Base handler for LeetCode-related requests."""
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
async def graphql(self, name: str, query: QueryType) -> None: ...
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
async def graphql(
|
|
28
|
+
self, name: str, query: QueryType, returnJson=True
|
|
29
|
+
) -> dict[str, Any]: ...
|
|
30
|
+
|
|
31
|
+
async def graphql(self, name: str, query: QueryType, returnJson=False):
|
|
32
|
+
self.log.debug(f"Fetching LeetCode {name} data...")
|
|
33
|
+
client = AsyncHTTPClient()
|
|
34
|
+
req = HTTPRequest(
|
|
35
|
+
url=LEETCODE_GRAPHQL_URL,
|
|
36
|
+
method="POST",
|
|
37
|
+
headers=HTTPHeaders(self.settings.get("leetcode_headers", {})),
|
|
38
|
+
body=json.dumps(query),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
resp = await client.fetch(req)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
self.log.error(f"Error fetching LeetCode {name}: {e}")
|
|
45
|
+
self.set_status(500)
|
|
46
|
+
self.finish(json.dumps({"message": f"Failed to fetch LeetCode {name}"}))
|
|
47
|
+
return
|
|
48
|
+
else:
|
|
49
|
+
if returnJson:
|
|
50
|
+
return json.loads(resp.body)
|
|
51
|
+
self.finish(resp.body)
|
|
52
|
+
|
|
53
|
+
async def graphql_multi(
|
|
54
|
+
self, name: str, queries: dict[str, QueryType]
|
|
55
|
+
) -> dict[str, HTTPResponse]:
|
|
56
|
+
self.log.debug(f"Fetching LeetCode {name} data...")
|
|
57
|
+
client = AsyncHTTPClient()
|
|
58
|
+
request_futures = dict(
|
|
59
|
+
map(
|
|
60
|
+
lambda kv: (
|
|
61
|
+
kv[0],
|
|
62
|
+
client.fetch(
|
|
63
|
+
HTTPRequest(
|
|
64
|
+
url=LEETCODE_GRAPHQL_URL,
|
|
65
|
+
method="POST",
|
|
66
|
+
headers=HTTPHeaders(
|
|
67
|
+
self.settings.get("leetcode_headers", {})
|
|
68
|
+
),
|
|
69
|
+
body=json.dumps(kv[1]),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
queries.items(),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
responses = await multi(request_futures)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.log.error(f"Error fetching LeetCode {name}: {e}")
|
|
81
|
+
self.set_status(500)
|
|
82
|
+
self.finish(json.dumps({"message": f"Failed to fetch LeetCode {name}"}))
|
|
83
|
+
return {}
|
|
84
|
+
else:
|
|
85
|
+
return cast("dict[str, HTTPResponse]", responses)
|
|
86
|
+
|
|
87
|
+
async def request_api(self, url: str, method: str, body: Mapping[str, Any]):
|
|
88
|
+
self.log.debug(f"Requesting LeetCode API: {url} with method {method}")
|
|
89
|
+
client = AsyncHTTPClient()
|
|
90
|
+
req = HTTPRequest(
|
|
91
|
+
url=f"{LEETCODE_URL}{url}",
|
|
92
|
+
method=method,
|
|
93
|
+
headers=HTTPHeaders(self.settings.get("leetcode_headers", {})),
|
|
94
|
+
body=json.dumps(body),
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
resp = await client.fetch(req)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
self.log.error(f"Error requesting LeetCode API: {e}")
|
|
100
|
+
self.set_status(500)
|
|
101
|
+
self.finish(json.dumps({"message": "Failed to request LeetCode API"}))
|
|
102
|
+
return None
|
|
103
|
+
else:
|
|
104
|
+
return json.loads(resp.body) if resp.body else {}
|
|
105
|
+
|
|
106
|
+
async def get_question_detail(self, title_slug: str) -> dict[str, Any]:
|
|
107
|
+
resp = await self.graphql(
|
|
108
|
+
name="question_detail",
|
|
109
|
+
query={
|
|
110
|
+
"query": """query questionData($titleSlug: String!) {
|
|
111
|
+
question(titleSlug: $titleSlug) {
|
|
112
|
+
questionId
|
|
113
|
+
questionFrontendId
|
|
114
|
+
submitUrl
|
|
115
|
+
questionDetailUrl
|
|
116
|
+
title
|
|
117
|
+
titleSlug
|
|
118
|
+
content
|
|
119
|
+
isPaidOnly
|
|
120
|
+
difficulty
|
|
121
|
+
likes
|
|
122
|
+
dislikes
|
|
123
|
+
isLiked
|
|
124
|
+
similarQuestions
|
|
125
|
+
exampleTestcaseList
|
|
126
|
+
topicTags {
|
|
127
|
+
name
|
|
128
|
+
slug
|
|
129
|
+
translatedName
|
|
130
|
+
}
|
|
131
|
+
codeSnippets {
|
|
132
|
+
lang
|
|
133
|
+
langSlug
|
|
134
|
+
code
|
|
135
|
+
}
|
|
136
|
+
stats
|
|
137
|
+
hints
|
|
138
|
+
solution {
|
|
139
|
+
id
|
|
140
|
+
canSeeDetail
|
|
141
|
+
paidOnly
|
|
142
|
+
hasVideoSolution
|
|
143
|
+
paidOnlyVideo
|
|
144
|
+
}
|
|
145
|
+
status
|
|
146
|
+
sampleTestCase
|
|
147
|
+
}
|
|
148
|
+
}""",
|
|
149
|
+
"variables": {"titleSlug": title_slug},
|
|
150
|
+
},
|
|
151
|
+
returnJson=True,
|
|
152
|
+
)
|
|
153
|
+
return resp
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class LeetCodeProfileHandler(LeetCodeHandler):
|
|
157
|
+
route = r"leetcode/profile"
|
|
158
|
+
|
|
159
|
+
@tornado.web.authenticated
|
|
160
|
+
async def get(self):
|
|
161
|
+
await self.graphql(
|
|
162
|
+
name="profile",
|
|
163
|
+
query={
|
|
164
|
+
"query": """query globalData {
|
|
165
|
+
userStatus {
|
|
166
|
+
isSignedIn
|
|
167
|
+
username
|
|
168
|
+
realName
|
|
169
|
+
avatar
|
|
170
|
+
}
|
|
171
|
+
}"""
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class LeetCodeStatisticsHandler(LeetCodeHandler):
|
|
177
|
+
route = r"leetcode/statistics"
|
|
178
|
+
|
|
179
|
+
@tornado.web.authenticated
|
|
180
|
+
async def get(self):
|
|
181
|
+
username = self.get_query_argument("username", "", strip=True)
|
|
182
|
+
if not username:
|
|
183
|
+
self.set_status(400)
|
|
184
|
+
self.finish(json.dumps({"message": "Username parameter is required"}))
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
responses = await self.graphql_multi(
|
|
188
|
+
name="statistics",
|
|
189
|
+
queries={
|
|
190
|
+
"userSessionProgress": {
|
|
191
|
+
"query": """query userSessionProgress($username: String!) {
|
|
192
|
+
allQuestionsCount {
|
|
193
|
+
difficulty
|
|
194
|
+
count
|
|
195
|
+
}
|
|
196
|
+
matchedUser(username: $username) {
|
|
197
|
+
submitStats {
|
|
198
|
+
acSubmissionNum {
|
|
199
|
+
difficulty
|
|
200
|
+
count
|
|
201
|
+
}
|
|
202
|
+
totalSubmissionNum {
|
|
203
|
+
difficulty
|
|
204
|
+
count
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}""",
|
|
209
|
+
"variables": {"username": username},
|
|
210
|
+
},
|
|
211
|
+
"userProfileUserQuestionProgressV2": {
|
|
212
|
+
"query": """query userProfileUserQuestionProgressV2($userSlug: String!) {
|
|
213
|
+
userProfileUserQuestionProgressV2(userSlug: $userSlug) {
|
|
214
|
+
numAcceptedQuestions {
|
|
215
|
+
count
|
|
216
|
+
difficulty
|
|
217
|
+
}
|
|
218
|
+
numFailedQuestions {
|
|
219
|
+
count
|
|
220
|
+
difficulty
|
|
221
|
+
}
|
|
222
|
+
numUntouchedQuestions {
|
|
223
|
+
count
|
|
224
|
+
difficulty
|
|
225
|
+
}
|
|
226
|
+
userSessionBeatsPercentage {
|
|
227
|
+
difficulty
|
|
228
|
+
percentage
|
|
229
|
+
}
|
|
230
|
+
totalQuestionBeatsPercentage
|
|
231
|
+
}
|
|
232
|
+
}""",
|
|
233
|
+
"variables": {"userSlug": username},
|
|
234
|
+
},
|
|
235
|
+
"userPublicProfile": {
|
|
236
|
+
"query": """query userPublicProfile($username: String!) {
|
|
237
|
+
matchedUser(username: $username) {
|
|
238
|
+
username
|
|
239
|
+
profile {
|
|
240
|
+
ranking
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}""",
|
|
244
|
+
"variables": {"username": username},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if not responses:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
res = dict(
|
|
253
|
+
map(
|
|
254
|
+
lambda kv: (kv[0], json.loads(kv[1].body).get("data", {})),
|
|
255
|
+
responses.items(),
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
self.finish(res)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class LeetCodeQuestionHandler(LeetCodeHandler):
|
|
262
|
+
route = r"leetcode/questions"
|
|
263
|
+
|
|
264
|
+
@tornado.web.authenticated
|
|
265
|
+
async def post(self):
|
|
266
|
+
body = self.get_json_body()
|
|
267
|
+
if not body:
|
|
268
|
+
self.set_status(400)
|
|
269
|
+
self.finish(json.dumps({"message": "Request body is required"}))
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
body = cast("dict[str, str|int]", body)
|
|
273
|
+
skip = cast(int, body.get("skip", 0))
|
|
274
|
+
limit = cast(int, body.get("limit", 0))
|
|
275
|
+
keyword = cast(str, body.get("keyword", ""))
|
|
276
|
+
sortField = cast(str, body.get("sortField", "CUSTOM"))
|
|
277
|
+
sortOrder = cast(str, body.get("sortOrder", "ASCENDING"))
|
|
278
|
+
|
|
279
|
+
await self.graphql(
|
|
280
|
+
name="question_list",
|
|
281
|
+
query={
|
|
282
|
+
"query": """query problemsetQuestionListV2($filters: QuestionFilterInput,
|
|
283
|
+
$limit: Int,
|
|
284
|
+
$searchKeyword: String,
|
|
285
|
+
$skip: Int,
|
|
286
|
+
$sortBy: QuestionSortByInput,
|
|
287
|
+
$categorySlug: String) {
|
|
288
|
+
problemsetQuestionListV2(
|
|
289
|
+
filters: $filters
|
|
290
|
+
limit: $limit
|
|
291
|
+
searchKeyword: $searchKeyword
|
|
292
|
+
skip: $skip
|
|
293
|
+
sortBy: $sortBy
|
|
294
|
+
categorySlug: $categorySlug
|
|
295
|
+
) {
|
|
296
|
+
questions {
|
|
297
|
+
id
|
|
298
|
+
titleSlug
|
|
299
|
+
title
|
|
300
|
+
translatedTitle
|
|
301
|
+
questionFrontendId
|
|
302
|
+
paidOnly
|
|
303
|
+
difficulty
|
|
304
|
+
topicTags {
|
|
305
|
+
name
|
|
306
|
+
slug
|
|
307
|
+
nameTranslated
|
|
308
|
+
}
|
|
309
|
+
status
|
|
310
|
+
isInMyFavorites
|
|
311
|
+
frequency
|
|
312
|
+
acRate
|
|
313
|
+
}
|
|
314
|
+
totalLength
|
|
315
|
+
finishedLength
|
|
316
|
+
hasMore
|
|
317
|
+
}
|
|
318
|
+
}""",
|
|
319
|
+
"variables": {
|
|
320
|
+
"skip": skip,
|
|
321
|
+
"limit": limit,
|
|
322
|
+
"searchKeyword": keyword,
|
|
323
|
+
"categorySlug": "algorithms",
|
|
324
|
+
"filters": {
|
|
325
|
+
"filterCombineType": "ALL",
|
|
326
|
+
"statusFilter": {
|
|
327
|
+
"questionStatuses": ["TO_DO"],
|
|
328
|
+
"operator": "IS",
|
|
329
|
+
},
|
|
330
|
+
"difficultyFilter": {
|
|
331
|
+
"difficulties": ["MEDIUM", "HARD"],
|
|
332
|
+
"operator": "IS",
|
|
333
|
+
},
|
|
334
|
+
"languageFilter": {"languageSlugs": [], "operator": "IS"},
|
|
335
|
+
"topicFilter": {"topicSlugs": [], "operator": "IS"},
|
|
336
|
+
"acceptanceFilter": {},
|
|
337
|
+
"frequencyFilter": {},
|
|
338
|
+
"frontendIdFilter": {},
|
|
339
|
+
"lastSubmittedFilter": {},
|
|
340
|
+
"publishedFilter": {},
|
|
341
|
+
"companyFilter": {"companySlugs": [], "operator": "IS"},
|
|
342
|
+
"positionFilter": {"positionSlugs": [], "operator": "IS"},
|
|
343
|
+
"premiumFilter": {"premiumStatus": [], "operator": "IS"},
|
|
344
|
+
},
|
|
345
|
+
"sortBy": {"sortField": sortField, "sortOrder": sortOrder},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class CreateNotebookHandler(LeetCodeHandler):
|
|
352
|
+
route = r"notebook/create"
|
|
353
|
+
|
|
354
|
+
@tornado.web.authenticated
|
|
355
|
+
async def post(self):
|
|
356
|
+
body = self.get_json_body()
|
|
357
|
+
if not body:
|
|
358
|
+
self.set_status(400)
|
|
359
|
+
self.finish({"message": "Request body is required"})
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
body = cast("dict[str, str]", body)
|
|
363
|
+
title_slug = cast(str, body.get("titleSlug", ""))
|
|
364
|
+
if not title_slug:
|
|
365
|
+
self.set_status(400)
|
|
366
|
+
self.finish({"message": "titleSlug is required"})
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
question = await self.get_question_detail(title_slug)
|
|
370
|
+
question = question.get("data", {}).get("question")
|
|
371
|
+
if not question:
|
|
372
|
+
self.set_status(404)
|
|
373
|
+
self.finish({"message": "Question not found"})
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
notebook_generator = self.settings.get("notebook_generator")
|
|
377
|
+
if not notebook_generator:
|
|
378
|
+
notebook_generator = NotebookGenerator()
|
|
379
|
+
self.settings.update(notebook_generator=notebook_generator)
|
|
380
|
+
|
|
381
|
+
file_path = notebook_generator.generate(question)
|
|
382
|
+
self.finish({"filePath": file_path})
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class SubmitNotebookHandler(LeetCodeHandler):
|
|
386
|
+
route = r"notebook/submit"
|
|
387
|
+
|
|
388
|
+
def get_solution(self, notebook):
|
|
389
|
+
solution_cell = first(
|
|
390
|
+
notebook["cells"],
|
|
391
|
+
lambda c: c["cell_type"] == "code" and c["metadata"].get("isSolutionCode"),
|
|
392
|
+
)
|
|
393
|
+
if not solution_cell:
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
code = "".join(solution_cell["source"]).strip()
|
|
397
|
+
return code if not code.endswith("pass") else None
|
|
398
|
+
|
|
399
|
+
async def submit(self, file_path: str):
|
|
400
|
+
if not os.path.exists(file_path):
|
|
401
|
+
self.set_status(404)
|
|
402
|
+
self.finish({"message": "Notebook file not found"})
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
406
|
+
notebook = json.load(f)
|
|
407
|
+
|
|
408
|
+
question_info = notebook["metadata"]["leetcode_question_info"]
|
|
409
|
+
if not question_info:
|
|
410
|
+
self.set_status(400)
|
|
411
|
+
self.finish({"message": "Notebook does not contain LeetCode question info"})
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
question_frontend_id = question_info["questionFrontendId"]
|
|
415
|
+
question_submit_id = question_info["questionId"]
|
|
416
|
+
submit_url = question_info["submitUrl"]
|
|
417
|
+
sample_testcase = question_info["sampleTestCase"]
|
|
418
|
+
if (
|
|
419
|
+
not question_frontend_id
|
|
420
|
+
or not question_submit_id
|
|
421
|
+
or not submit_url
|
|
422
|
+
or not sample_testcase
|
|
423
|
+
):
|
|
424
|
+
self.set_status(400)
|
|
425
|
+
self.finish({"message": "Invalid question info in notebook"})
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
solution_code = self.get_solution(notebook)
|
|
429
|
+
if not solution_code:
|
|
430
|
+
self.set_status(400)
|
|
431
|
+
self.finish({"message": "No solution code found in notebook"})
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
resp = await self.request_api(
|
|
435
|
+
submit_url,
|
|
436
|
+
"POST",
|
|
437
|
+
{
|
|
438
|
+
"question_id": str(question_submit_id),
|
|
439
|
+
"data_input": sample_testcase,
|
|
440
|
+
"lang": "python3",
|
|
441
|
+
"typed_code": solution_code,
|
|
442
|
+
"test_mode": False,
|
|
443
|
+
"judge_type": "large",
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
self.finish(resp)
|
|
448
|
+
|
|
449
|
+
@tornado.web.authenticated
|
|
450
|
+
async def post(self):
|
|
451
|
+
body = self.get_json_body()
|
|
452
|
+
if not body:
|
|
453
|
+
self.set_status(400)
|
|
454
|
+
self.finish({"message": "Request body is required"})
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
body = cast("dict[str, str]", body)
|
|
458
|
+
file_path = cast(str, body.get("filePath", ""))
|
|
459
|
+
if not file_path:
|
|
460
|
+
self.set_status(400)
|
|
461
|
+
self.finish({"message": "filePath is required"})
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
await self.submit(file_path)
|