1
+ import asyncio
1
2
import json
2
3
import os
3
- from typing import Any , Mapping , cast , overload
4
+ from collections .abc import Mapping
5
+ from typing import Any , cast , overload
4
6
5
7
import tornado
6
8
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
9
12
10
13
from ..utils .notebook_generator import NotebookGenerator
11
- from ..utils .utils import first
14
+ from ..utils .utils import first , request
12
15
from .base_handler import BaseHandler
13
16
14
17
LEETCODE_URL = "https://leetcode.com"
15
18
LEETCODE_GRAPHQL_URL = f"{ LEETCODE_URL } /graphql"
16
-
17
- type QueryType = dict [str , str | Mapping [str , Any ]]
19
+ MAX_CHECK_ATTEMPTS = 10
18
20
19
21
20
22
class LeetCodeHandler (BaseHandler ):
21
23
"""Base handler for LeetCode-related requests."""
22
24
23
25
@overload
24
- async def graphql (self , name : str , query : QueryType ) -> None : ...
26
+ async def graphql (self , name : str , query : Mapping [ str , Any ] ) -> None : ...
25
27
26
28
@overload
27
29
async def graphql (
28
- self , name : str , query : QueryType , returnJson = True
30
+ self , name : str , query : Mapping [ str , Any ] , returnJson = True
29
31
) -> dict [str , Any ]: ...
30
32
31
- async def graphql (self , name : str , query : QueryType , returnJson = False ):
33
+ async def graphql (self , name : str , query : Mapping [ str , Any ] , returnJson = False ):
32
34
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
35
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
+ )
43
42
except Exception as e :
44
43
self .log .error (f"Error fetching LeetCode { name } : { e } " )
45
44
self .set_status (500 )
@@ -51,23 +50,18 @@ async def graphql(self, name: str, query: QueryType, returnJson=False):
51
50
self .finish (resp .body )
52
51
53
52
async def graphql_multi (
54
- self , name : str , queries : dict [str , QueryType ]
53
+ self , name : str , queries : dict [str , Mapping [ str , Any ] ]
55
54
) -> dict [str , HTTPResponse ]:
56
55
self .log .debug (f"Fetching LeetCode { name } data..." )
57
- client = AsyncHTTPClient ()
58
56
request_futures = dict (
59
57
map (
60
58
lambda kv : (
61
59
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 ],
71
65
),
72
66
),
73
67
queries .items (),
@@ -84,74 +78,6 @@ async def graphql_multi(
84
78
else :
85
79
return cast ("dict[str, HTTPResponse]" , responses )
86
80
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
81
156
82
class LeetCodeProfileHandler (LeetCodeHandler ):
157
83
route = r"leetcode/profile"
@@ -323,14 +249,8 @@ async def post(self):
323
249
"categorySlug" : "algorithms" ,
324
250
"filters" : {
325
251
"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" },
334
254
"languageFilter" : {"languageSlugs" : [], "operator" : "IS" },
335
255
"topicFilter" : {"topicSlugs" : [], "operator" : "IS" },
336
256
"acceptanceFilter" : {},
@@ -351,6 +271,55 @@ async def post(self):
351
271
class CreateNotebookHandler (LeetCodeHandler ):
352
272
route = r"notebook/create"
353
273
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
+
354
323
@tornado .web .authenticated
355
324
async def post (self ):
356
325
body = self .get_json_body ()
@@ -431,10 +400,11 @@ async def submit(self, file_path: str):
431
400
self .finish ({"message" : "No solution code found in notebook" })
432
401
return
433
402
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 = {
438
408
"question_id" : str (question_submit_id ),
439
409
"data_input" : sample_testcase ,
440
410
"lang" : "python3" ,
@@ -444,7 +414,7 @@ async def submit(self, file_path: str):
444
414
},
445
415
)
446
416
447
- self .finish (resp )
417
+ self .finish (resp . body )
448
418
449
419
@tornado .web .authenticated
450
420
async def post (self ):
@@ -462,3 +432,87 @@ async def post(self):
462
432
return
463
433
464
434
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