Skip to content

Commit c791516

Browse files
committed
add websocket to check submit result
1 parent 0f53aee commit c791516

File tree

16 files changed

+364
-169
lines changed

16 files changed

+364
-169
lines changed

jupyterlab_leetcode/handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .leetcode_handler import (CreateNotebookHandler, LeetCodeProfileHandler,
66
LeetCodeQuestionHandler,
77
LeetCodeStatisticsHandler,
8+
LeetCodeWebSocketSubmitHandler,
89
SubmitNotebookHandler)
910

1011

@@ -18,6 +19,7 @@ def setup_handlers(web_app):
1819
LeetCodeQuestionHandler,
1920
CreateNotebookHandler,
2021
SubmitNotebookHandler,
22+
LeetCodeWebSocketSubmitHandler,
2123
]
2224

2325
web_app.add_handlers(

jupyterlab_leetcode/handlers/leetcode_handler.py

Lines changed: 164 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,44 @@
1+
import asyncio
12
import json
23
import os
3-
from typing import Any, Mapping, cast, overload
4+
from collections.abc import Mapping
5+
from typing import Any, cast, overload
46

57
import tornado
68
from tornado.gen import multi
7-
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPResponse
8-
from tornado.httputil import HTTPHeaders
9+
from tornado.httpclient import HTTPResponse
10+
from tornado.httputil import HTTPServerRequest
11+
from tornado.websocket import WebSocketHandler
912

1013
from ..utils.notebook_generator import NotebookGenerator
11-
from ..utils.utils import first
14+
from ..utils.utils import first, request
1215
from .base_handler import BaseHandler
1316

1417
LEETCODE_URL = "https://leetcode.com"
1518
LEETCODE_GRAPHQL_URL = f"{LEETCODE_URL}/graphql"
16-
17-
type QueryType = dict[str, str | Mapping[str, Any]]
19+
MAX_CHECK_ATTEMPTS = 10
1820

1921

2022
class LeetCodeHandler(BaseHandler):
2123
"""Base handler for LeetCode-related requests."""
2224

2325
@overload
24-
async def graphql(self, name: str, query: QueryType) -> None: ...
26+
async def graphql(self, name: str, query: Mapping[str, Any]) -> None: ...
2527

2628
@overload
2729
async def graphql(
28-
self, name: str, query: QueryType, returnJson=True
30+
self, name: str, query: Mapping[str, Any], returnJson=True
2931
) -> dict[str, Any]: ...
3032

31-
async def graphql(self, name: str, query: QueryType, returnJson=False):
33+
async def graphql(self, name: str, query: Mapping[str, Any], returnJson=False):
3234
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-
4135
try:
42-
resp = await client.fetch(req)
36+
resp = await request(
37+
LEETCODE_GRAPHQL_URL,
38+
method="POST",
39+
headers=self.settings.get("leetcode_headers", {}),
40+
body=query,
41+
)
4342
except Exception as e:
4443
self.log.error(f"Error fetching LeetCode {name}: {e}")
4544
self.set_status(500)
@@ -51,23 +50,18 @@ async def graphql(self, name: str, query: QueryType, returnJson=False):
5150
self.finish(resp.body)
5251

5352
async def graphql_multi(
54-
self, name: str, queries: dict[str, QueryType]
53+
self, name: str, queries: dict[str, Mapping[str, Any]]
5554
) -> dict[str, HTTPResponse]:
5655
self.log.debug(f"Fetching LeetCode {name} data...")
57-
client = AsyncHTTPClient()
5856
request_futures = dict(
5957
map(
6058
lambda kv: (
6159
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-
),
60+
request(
61+
url=LEETCODE_GRAPHQL_URL,
62+
method="POST",
63+
headers=self.settings.get("leetcode_headers", {}),
64+
body=kv[1],
7165
),
7266
),
7367
queries.items(),
@@ -84,74 +78,6 @@ async def graphql_multi(
8478
else:
8579
return cast("dict[str, HTTPResponse]", responses)
8680

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-
15581

15682
class LeetCodeProfileHandler(LeetCodeHandler):
15783
route = r"leetcode/profile"
@@ -323,14 +249,8 @@ async def post(self):
323249
"categorySlug": "algorithms",
324250
"filters": {
325251
"filterCombineType": "ALL",
326-
"statusFilter": {
327-
"questionStatuses": ["TO_DO"],
328-
"operator": "IS",
329-
},
330-
"difficultyFilter": {
331-
"difficulties": ["MEDIUM", "HARD"],
332-
"operator": "IS",
333-
},
252+
"statusFilter": {"questionStatuses": [], "operator": "IS"},
253+
"difficultyFilter": {"difficulties": [], "operator": "IS"},
334254
"languageFilter": {"languageSlugs": [], "operator": "IS"},
335255
"topicFilter": {"topicSlugs": [], "operator": "IS"},
336256
"acceptanceFilter": {},
@@ -351,6 +271,55 @@ async def post(self):
351271
class CreateNotebookHandler(LeetCodeHandler):
352272
route = r"notebook/create"
353273

274+
async def get_question_detail(self, title_slug: str) -> dict[str, Any]:
275+
resp = await self.graphql(
276+
name="question_detail",
277+
query={
278+
"query": """query questionData($titleSlug: String!) {
279+
question(titleSlug: $titleSlug) {
280+
questionId
281+
questionFrontendId
282+
submitUrl
283+
questionDetailUrl
284+
title
285+
titleSlug
286+
content
287+
isPaidOnly
288+
difficulty
289+
likes
290+
dislikes
291+
isLiked
292+
similarQuestions
293+
exampleTestcaseList
294+
topicTags {
295+
name
296+
slug
297+
translatedName
298+
}
299+
codeSnippets {
300+
lang
301+
langSlug
302+
code
303+
}
304+
stats
305+
hints
306+
solution {
307+
id
308+
canSeeDetail
309+
paidOnly
310+
hasVideoSolution
311+
paidOnlyVideo
312+
}
313+
status
314+
sampleTestCase
315+
}
316+
}""",
317+
"variables": {"titleSlug": title_slug},
318+
},
319+
returnJson=True,
320+
)
321+
return resp
322+
354323
@tornado.web.authenticated
355324
async def post(self):
356325
body = self.get_json_body()
@@ -431,10 +400,11 @@ async def submit(self, file_path: str):
431400
self.finish({"message": "No solution code found in notebook"})
432401
return
433402

434-
resp = await self.request_api(
435-
submit_url,
436-
"POST",
437-
{
403+
resp = await request(
404+
f"{LEETCODE_URL}{submit_url}",
405+
method="POST",
406+
headers=self.settings.get("leetcode_headers", {}),
407+
body={
438408
"question_id": str(question_submit_id),
439409
"data_input": sample_testcase,
440410
"lang": "python3",
@@ -444,7 +414,7 @@ async def submit(self, file_path: str):
444414
},
445415
)
446416

447-
self.finish(resp)
417+
self.finish(resp.body)
448418

449419
@tornado.web.authenticated
450420
async def post(self):
@@ -462,3 +432,87 @@ async def post(self):
462432
return
463433

464434
await self.submit(file_path)
435+
436+
437+
class LeetCodeWebSocketSubmitHandler(WebSocketHandler):
438+
route = r"websocket/submit"
439+
fibonacci = [0, 1, 1, 2, 3, 5]
440+
441+
def __init__(
442+
self,
443+
application: tornado.web.Application,
444+
request: HTTPServerRequest,
445+
**kwargs: Any,
446+
) -> None:
447+
super().__init__(application, request, **kwargs)
448+
self.submission_id: int = 0
449+
self.check_task: asyncio.Task | None = None
450+
self.check_result: Mapping[str, Any] = {}
451+
452+
def open(self, *args: str, **kwargs: str):
453+
self.submission_id = int(self.get_query_argument("submission_id"))
454+
if self.submission_id:
455+
self.check_task = asyncio.create_task(self.check())
456+
457+
async def check(self, cnt=0):
458+
if cnt > MAX_CHECK_ATTEMPTS:
459+
self.write_message(
460+
{
461+
"type": "error",
462+
"error": "Submission check timed out",
463+
"submissionId": self.submission_id,
464+
}
465+
)
466+
return
467+
468+
try:
469+
resp = await request(
470+
f"{LEETCODE_URL}/submissions/detail/{self.submission_id}/check/",
471+
method="GET",
472+
headers=self.settings.get("leetcode_headers", {}),
473+
)
474+
except Exception as e:
475+
self.write_message(
476+
{
477+
"type": "error",
478+
"error": "Submission check error",
479+
"submissionId": self.submission_id,
480+
}
481+
)
482+
return
483+
484+
self.check_result = json.loads(resp.body)
485+
print(
486+
f"Checking submission {self.submission_id}, attempts: {cnt}, result: {self.check_result}"
487+
)
488+
self.write_message(
489+
{
490+
"type": "submissionResult",
491+
"result": self.check_result,
492+
"submissionId": self.submission_id,
493+
}
494+
)
495+
state = self.check_result.get("state")
496+
if state == "PENDING" or state == "STARTED":
497+
await asyncio.sleep(self.fibonacci[min(cnt, len(self.fibonacci) - 1)])
498+
await self.check(cnt + 1)
499+
500+
def on_message(self, message):
501+
msg = json.loads(message)
502+
if msg.get("submissionId") != self.submission_id:
503+
self.write_message(
504+
{
505+
"type": "error",
506+
"error": "Submission ID mismatch",
507+
"submissionId": self.submission_id,
508+
}
509+
)
510+
return
511+
512+
self.write_message(
513+
{
514+
"type": "submissionResult",
515+
"result": self.check_result,
516+
"submissionId": self.submission_id,
517+
}
518+
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy