Skip to content

Commit 0f53aee

Browse files
committed
support submit notebook to leetcode to evaluate
1 parent 94a3896 commit 0f53aee

File tree

9 files changed

+246
-45
lines changed

9 files changed

+246
-45
lines changed

jupyterlab_leetcode/handlers/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from .cookie_handler import GetCookieHandler
55
from .leetcode_handler import (CreateNotebookHandler, LeetCodeProfileHandler,
66
LeetCodeQuestionHandler,
7-
LeetCodeStatisticsHandler)
7+
LeetCodeStatisticsHandler,
8+
SubmitNotebookHandler)
89

910

1011
def setup_handlers(web_app):
@@ -16,6 +17,7 @@ def setup_handlers(web_app):
1617
LeetCodeStatisticsHandler,
1718
LeetCodeQuestionHandler,
1819
CreateNotebookHandler,
20+
SubmitNotebookHandler,
1921
]
2022

2123
web_app.add_handlers(

jupyterlab_leetcode/handlers/leetcode_handler.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import os
23
from typing import Any, Mapping, cast, overload
34

45
import tornado
@@ -7,9 +8,11 @@
78
from tornado.httputil import HTTPHeaders
89

910
from ..utils.notebook_generator import NotebookGenerator
11+
from ..utils.utils import first
1012
from .base_handler import BaseHandler
1113

12-
LEETCODE_GRAPHQL_URL = "https://leetcode.com/graphql"
14+
LEETCODE_URL = "https://leetcode.com"
15+
LEETCODE_GRAPHQL_URL = f"{LEETCODE_URL}/graphql"
1316

1417
type QueryType = dict[str, str | Mapping[str, Any]]
1518

@@ -81,6 +84,25 @@ async def graphql_multi(
8184
else:
8285
return cast("dict[str, HTTPResponse]", responses)
8386

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+
84106
async def get_question_detail(self, title_slug: str) -> dict[str, Any]:
85107
resp = await self.graphql(
86108
name="question_detail",
@@ -358,3 +380,85 @@ async def post(self):
358380

359381
file_path = notebook_generator.generate(question)
360382
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)

jupyterlab_leetcode/utils/notebook_generator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ def __populate_metadata(self, q):
3737

3838
metadata_question_info = self.template["metadata"]["leetcode_question_info"]
3939
metadata_question_info["submitUrl"] = q["submitUrl"]
40-
metadata_question_info["sampleTestCase"] = q["sampleTestCase"]
4140
metadata_question_info["questionId"] = q["questionId"]
4241
metadata_question_info["questionFrontendId"] = q["questionFrontendId"]
4342
metadata_question_info["questionDetailUrl"] = q["questionDetailUrl"]
43+
metadata_question_info["sampleTestCase"] = q["sampleTestCase"]
44+
metadata_question_info["exampleTestcaseList"] = q["exampleTestcaseList"]
4445

4546
def __populate_title(self, q):
4647
title_cell = first(

src/components/LeetCode.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
import React, { useEffect, useState } from 'react';
22
import { IDocumentManager } from '@jupyterlab/docmanager';
3+
import { IDocumentWidget } from '@jupyterlab/docregistry';
4+
import { JupyterFrontEnd, LabShell } from '@jupyterlab/application';
35
import { getProfile } from '../services/leetcode';
46
import { LeetCodeProfile } from '../types/leetcode';
57
import Profile from './Profile';
68
import Statistics from './Statistics';
79
import QuestionList from './QuestionList';
810

9-
const LeetCode: React.FC<{ docManager: IDocumentManager }> = ({
10-
docManager
11-
}) => {
11+
export function getCurrentOpenFilePath(
12+
shell: LabShell,
13+
docManager: IDocumentManager,
14+
widget?: IDocumentWidget
15+
): string | null {
16+
const currentWidget = widget ?? shell.currentWidget;
17+
if (!currentWidget || !docManager) {
18+
return null;
19+
}
20+
const context = docManager.contextForWidget(currentWidget);
21+
if (!context) {
22+
return null;
23+
}
24+
return context.path;
25+
}
26+
27+
const LeetCode: React.FC<{
28+
app: JupyterFrontEnd;
29+
docManager: IDocumentManager;
30+
}> = ({ app, docManager }) => {
1231
const [profile, setProfile] = useState<LeetCodeProfile | null>(null);
1332

1433
useEffect(() => {

src/components/LeetCodeMainArea.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { IDocumentManager } from '@jupyterlab/docmanager';
3+
import { JupyterFrontEnd } from '@jupyterlab/application';
4+
import LandingPage from './LandingPage';
5+
import LeetCode from './LeetCode';
6+
import { getCookie } from '../services/cookie';
7+
8+
const LeetCodeMainArea: React.FC<{
9+
app: JupyterFrontEnd;
10+
docManager: IDocumentManager;
11+
}> = ({ app, docManager }) => {
12+
const [cookieLoggedIn, setCookieLoggedIn] = useState('');
13+
14+
useEffect(() => {
15+
const leetcode_browser = document.cookie
16+
.split('; ')
17+
.find(cookie => cookie.startsWith('leetcode_browser='))
18+
?.split('=')[1];
19+
if (leetcode_browser) {
20+
getCookie(leetcode_browser).then(resp => {
21+
if (resp['checked']) {
22+
setCookieLoggedIn(leetcode_browser);
23+
}
24+
});
25+
}
26+
});
27+
28+
return cookieLoggedIn ? (
29+
<LeetCode app={app} docManager={docManager} />
30+
) : (
31+
<LandingPage setCookieLoggedIn={b => setCookieLoggedIn(b)} />
32+
);
33+
};
34+
35+
export default LeetCodeMainArea;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import { NotebookPanel } from '@jupyterlab/notebook';
3+
import { submitNotebook } from '../services/notebook';
4+
5+
const LeetCodeNotebookHeader: React.FC<{ notebook: NotebookPanel }> = ({
6+
notebook
7+
}) => {
8+
const submit = () => {
9+
notebook.context.save().then(() => {
10+
const path = notebook.context.path;
11+
submitNotebook(path).then(() => {
12+
console.log('Notebook submitted successfully');
13+
});
14+
});
15+
};
16+
17+
return (
18+
<div>
19+
<button onClick={submit}>Submit</button>
20+
</div>
21+
);
22+
};
23+
24+
export default LeetCodeNotebookHeader;

src/index.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
} from '@jupyterlab/application';
66
import { ICommandPalette, WidgetTracker } from '@jupyterlab/apputils';
77
import { IDocumentManager } from '@jupyterlab/docmanager';
8+
import { NotebookPanel } from '@jupyterlab/notebook';
89

9-
import LeetCodeWidget from './widget';
10+
import { LeetCodeMainWidget, LeetCodeHeaderWidget } from './widget';
1011

1112
const PLUGIN_ID = 'jupyterlab-leetcode:plugin';
1213

@@ -25,14 +26,14 @@ const plugin: JupyterFrontEndPlugin<void> = {
2526
docManager: IDocumentManager,
2627
restorer: ILayoutRestorer | null
2728
) => {
28-
let leetcodeWidget: LeetCodeWidget;
29+
let leetcodeWidget: LeetCodeMainWidget;
2930

3031
const command = 'leetcode-widget:open';
3132
app.commands.addCommand(command, {
3233
label: 'Open LeetCode Widget',
3334
execute: () => {
3435
if (!leetcodeWidget || leetcodeWidget.isDisposed) {
35-
leetcodeWidget = new LeetCodeWidget(docManager);
36+
leetcodeWidget = new LeetCodeMainWidget(app, docManager);
3637
}
3738
if (!tracker.has(leetcodeWidget)) {
3839
tracker.add(leetcodeWidget);
@@ -43,9 +44,24 @@ const plugin: JupyterFrontEndPlugin<void> = {
4344
app.shell.activateById(leetcodeWidget.id);
4445
}
4546
});
46-
4747
palette.addItem({ command, category: 'LeetCode' });
48-
const tracker = new WidgetTracker<LeetCodeWidget>({
48+
49+
const addHeaderCommand = 'leetcode-widget:add-header';
50+
app.commands.addCommand(addHeaderCommand, {
51+
label: 'Add Header to LeetCode Widget',
52+
caption: 'Add Header to LeetCode Widget',
53+
execute: () => {
54+
const main = app.shell.currentWidget;
55+
if (main instanceof NotebookPanel) {
56+
const widget = new LeetCodeHeaderWidget(main);
57+
widget.node.style.minHeight = '20px';
58+
main.contentHeader.addWidget(widget);
59+
}
60+
}
61+
});
62+
palette.addItem({ command: addHeaderCommand, category: 'LeetCode' });
63+
64+
const tracker = new WidgetTracker<LeetCodeMainWidget>({
4965
namespace: 'leetcode-widget'
5066
});
5167
if (restorer) {

src/services/notebook.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ export async function generateNotebook(titleSlug: string) {
66
body: JSON.stringify({ titleSlug })
77
}).catch(() => null);
88
}
9+
10+
export async function submitNotebook(path: string) {
11+
return requestAPI<void>('/notebook/submit', {
12+
method: 'POST',
13+
body: JSON.stringify({ filePath: path })
14+
}).catch(() => null);
15+
}

0 commit comments

Comments
 (0)