leetcode-local-cli 0.7.0__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.
@@ -0,0 +1,166 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import Any
4
+
5
+
6
+ class QuestionIdError(str, Enum):
7
+ EMPTY = "empty"
8
+ NOT_DECIMAL = "not_decimal"
9
+ NOT_POSITIVE = "not_positive"
10
+ LEADING_ZERO = "leading_zero"
11
+
12
+
13
+ QUESTION_ID_ERROR_MESSAGES = {
14
+ QuestionIdError.EMPTY: "题号不能为空",
15
+ QuestionIdError.NOT_DECIMAL: "题号必须是正整数",
16
+ QuestionIdError.NOT_POSITIVE: "题号必须大于 0",
17
+ QuestionIdError.LEADING_ZERO: "题号不能以 0 开头",
18
+ }
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ParseQuestionIdResult:
23
+ question_id: str | None = None
24
+ error: QuestionIdError | None = None
25
+
26
+ @property
27
+ def ok(self) -> bool:
28
+ return self.error is None
29
+
30
+ @property
31
+ def error_message(self) -> str | None:
32
+ if self.error is None:
33
+ return None
34
+ return QUESTION_ID_ERROR_MESSAGES[self.error]
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ProblemSummary:
39
+ question_id: str
40
+ title: str
41
+ title_slug: str
42
+ difficulty: str
43
+ paid_only: bool
44
+ tags: list[str]
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ProblemDetail:
49
+ question_id: str
50
+ submit_question_id: str
51
+ title: str
52
+ title_slug: str
53
+ difficulty: str
54
+ tags: list[str]
55
+ content_html: str
56
+ python_code: str | None
57
+
58
+
59
+ def parse_question_id(raw: str) -> ParseQuestionIdResult:
60
+ text = raw.strip()
61
+
62
+ if not text:
63
+ return ParseQuestionIdResult(error=QuestionIdError.EMPTY)
64
+
65
+ if not text.isdecimal():
66
+ return ParseQuestionIdResult(error=QuestionIdError.NOT_DECIMAL)
67
+
68
+ if text == "0":
69
+ return ParseQuestionIdResult(error=QuestionIdError.NOT_POSITIVE)
70
+
71
+ if text.startswith("0"):
72
+ return ParseQuestionIdResult(error=QuestionIdError.LEADING_ZERO)
73
+
74
+ return ParseQuestionIdResult(question_id=text)
75
+
76
+
77
+ def normalize_problem_summary(raw: dict[str, Any]) -> ProblemSummary:
78
+ question_id = raw.get("frontendQuestionId", "")
79
+ question_id = str(question_id) if isinstance(question_id, (str, int)) else ""
80
+ title = raw.get("title", "")
81
+ title = title if isinstance(title, str) else ""
82
+ title_slug = raw.get("titleSlug", "")
83
+ title_slug = title_slug if isinstance(title_slug, str) else ""
84
+ difficulty = raw.get("difficulty", "")
85
+ difficulty = difficulty if isinstance(difficulty, str) else ""
86
+ paid_only = bool(raw.get("paidOnly", False))
87
+ raw_tags = raw.get("topicTags")
88
+ tags = (
89
+ [
90
+ name
91
+ for tag in raw_tags
92
+ if isinstance(tag, dict)
93
+ and isinstance((name := tag.get("name")), str)
94
+ and name
95
+ ]
96
+ if isinstance(raw_tags, list)
97
+ else []
98
+ )
99
+
100
+ return ProblemSummary(
101
+ question_id=question_id,
102
+ title=title,
103
+ title_slug=title_slug,
104
+ difficulty=difficulty,
105
+ paid_only=paid_only,
106
+ tags=tags,
107
+ )
108
+
109
+
110
+ def normalize_problem_summaries(
111
+ raw_items: list[dict[str, Any]],
112
+ ) -> list[ProblemSummary]:
113
+ summaries = [normalize_problem_summary(raw) for raw in raw_items]
114
+ return summaries
115
+
116
+
117
+ def find_problem_by_id(
118
+ problems: list[ProblemSummary], question_id: str
119
+ ) -> ProblemSummary | None:
120
+ for problem in problems:
121
+ if question_id == problem.question_id:
122
+ return problem
123
+ return None
124
+
125
+
126
+ def extract_python_code(code_snippets: Any) -> str | None:
127
+ if not isinstance(code_snippets, list):
128
+ return None
129
+ for snippet in code_snippets:
130
+ if not isinstance(snippet, dict):
131
+ continue
132
+ if snippet.get("langSlug") == "python3":
133
+ code = snippet.get("code")
134
+ return code if isinstance(code, str) else None
135
+
136
+ return None
137
+
138
+
139
+ def normalize_problem_detail(raw: dict[str, Any]) -> ProblemDetail:
140
+ raw_tags = raw.get("topicTags")
141
+ tags = (
142
+ [
143
+ name
144
+ for tag in raw_tags
145
+ if isinstance(tag, dict)
146
+ and isinstance((name := tag.get("name")), str)
147
+ and name
148
+ ]
149
+ if isinstance(raw_tags, list)
150
+ else []
151
+ )
152
+
153
+ def text(key: str) -> str:
154
+ value = raw.get(key)
155
+ return value if isinstance(value, str) else ""
156
+
157
+ return ProblemDetail(
158
+ question_id=text("questionFrontendId"),
159
+ submit_question_id=text("questionId"),
160
+ title=text("translatedTitle") or text("title"),
161
+ title_slug=text("titleSlug"),
162
+ difficulty=text("difficulty"),
163
+ tags=tags,
164
+ content_html=text("translatedContent") or text("content"),
165
+ python_code=extract_python_code(raw.get("codeSnippets")),
166
+ )
@@ -0,0 +1,269 @@
1
+ from time import sleep
2
+
3
+ from typer import Exit
4
+
5
+ from leetcode_local_cli.auth import (
6
+ REQUIRED_COOKIE_NAMES,
7
+ SessionFileError,
8
+ load_session,
9
+ )
10
+ from leetcode_local_cli.client import ClientErrorKind, LeetCodeClient
11
+ from leetcode_local_cli.doctor import (
12
+ DoctorReport,
13
+ DoctorStatus,
14
+ diagnose_remote,
15
+ diagnose_session,
16
+ diagnose_solution,
17
+ )
18
+ from leetcode_local_cli.problem import (
19
+ ProblemDetail,
20
+ ProblemSummary,
21
+ find_problem_by_id,
22
+ normalize_problem_detail,
23
+ normalize_problem_summaries,
24
+ parse_question_id,
25
+ )
26
+ from leetcode_local_cli.ui import error, loading, render_submission_target, warning
27
+ from leetcode_local_cli.workspace import WorkspaceError, parse_solution_submission
28
+
29
+
30
+ MAX_ATTEMPTS = 10
31
+
32
+
33
+ def client_error_message(kind: ClientErrorKind | None) -> str:
34
+ match kind:
35
+ case ClientErrorKind.NETWORK:
36
+ return "网络请求失败,请检查网络连接"
37
+
38
+ case ClientErrorKind.HTTP:
39
+ return "LeetCode 接口返回异常,请稍后重试"
40
+
41
+ case ClientErrorKind.INVALID_JSON:
42
+ return "LeetCode 返回内容无法解析,请稍后重试"
43
+
44
+ case ClientErrorKind.INVALID_RESPONSE:
45
+ return "LeetCode 接口数据结构异常,可能是接口变更"
46
+
47
+ case ClientErrorKind.UNAUTHORIZED:
48
+ return "登录态无效或已过期,请重新执行 uv run lc login"
49
+
50
+ case ClientErrorKind.MISSING_CSRF:
51
+ return "缺少提交凭证 csrftoken,请重新执行 uv run lc login"
52
+
53
+ case _:
54
+ return "未知客户端错误"
55
+
56
+
57
+ def _load_cookies_from_session() -> dict[str, str]:
58
+ session_check = diagnose_session()
59
+ if session_check.status is DoctorStatus.FAIL:
60
+ error(session_check.message)
61
+ if session_check.suggestion:
62
+ warning(session_check.suggestion)
63
+ raise Exit(1)
64
+ try:
65
+ session = load_session()
66
+ except SessionFileError as exc:
67
+ error(str(exc))
68
+ raise Exit(1)
69
+ if not isinstance(session, dict):
70
+ warning("未找到有效登录态,请先执行 uv run lc login")
71
+ raise Exit(1)
72
+ cookies = session.get("cookies")
73
+ if not isinstance(cookies, dict):
74
+ error("Session 文件结构无效,请重新执行 uv run lc login")
75
+ raise Exit(1)
76
+ valid_cookies: dict[str, str] = {}
77
+ for name in REQUIRED_COOKIE_NAMES:
78
+ value = cookies.get(name)
79
+ if not isinstance(value, str) or not value:
80
+ error(f"缺少或无效的 Cookie:{name}")
81
+ warning("请重新执行 uv run lc login")
82
+ raise Exit(1)
83
+ valid_cookies[name] = value
84
+ return valid_cookies
85
+
86
+
87
+ def _parse_question_id_or_exit(question_id: str) -> str:
88
+ parse_result = parse_question_id(question_id)
89
+ if not parse_result.ok:
90
+ error(parse_result.error_message or "题号解析失败")
91
+ raise Exit(1)
92
+ assert parse_result.question_id is not None
93
+ return parse_result.question_id
94
+
95
+
96
+ def _find_problem_summary_by_question_id_online(
97
+ client: LeetCodeClient,
98
+ question_id: str,
99
+ ) -> ProblemSummary:
100
+ limit, skip = 100, 0
101
+ while True:
102
+ problem_list_data = client.problem_list(limit=limit, skip=skip)
103
+ if not problem_list_data.ok:
104
+ error(client_error_message(problem_list_data.error))
105
+ raise Exit(1)
106
+ problem_list = problem_list_data.data
107
+ if not isinstance(problem_list, dict):
108
+ error(client_error_message(ClientErrorKind.INVALID_RESPONSE))
109
+ raise Exit(1)
110
+ questions = problem_list.get("questions", [])
111
+ problem_summaries = normalize_problem_summaries(questions)
112
+ problem_summary = find_problem_by_id(problem_summaries, question_id)
113
+ if problem_summary:
114
+ return problem_summary
115
+ skip += limit
116
+ total = problem_list.get("total") or 0
117
+ if not questions or skip >= total:
118
+ error(f"未找到题号 {question_id}")
119
+ raise Exit(1)
120
+
121
+
122
+ def _validate_show_options(limit: int, skip: int) -> None:
123
+ if limit <= 0:
124
+ error("limit 必须是正整数")
125
+ raise Exit(1)
126
+ if limit > 100:
127
+ error("limit 超过单次查询上限,最大为 100")
128
+ raise Exit(1)
129
+ if skip < 0:
130
+ error("skip 必须是非负整数")
131
+ raise Exit(1)
132
+
133
+
134
+ def get_user_status() -> dict:
135
+ cookies = _load_cookies_from_session()
136
+ with LeetCodeClient(cookies) as client:
137
+ user_status = client.user_status()
138
+ if not user_status.ok:
139
+ error(client_error_message(user_status.error))
140
+ raise Exit(1)
141
+ status = user_status.data
142
+ if not isinstance(status, dict) or not status.get("isSignedIn"):
143
+ error(client_error_message(ClientErrorKind.UNAUTHORIZED))
144
+ raise Exit(1)
145
+ return user_status.data
146
+
147
+
148
+ def get_account_profile() -> dict:
149
+ cookies = _load_cookies_from_session()
150
+ with LeetCodeClient(cookies) as client:
151
+ with loading("正在获取账户信息..."):
152
+ account_profile = client.account_profile()
153
+ if not account_profile.ok:
154
+ error(client_error_message(account_profile.error))
155
+ raise Exit(1)
156
+ if not isinstance(account_profile.data, dict):
157
+ error(client_error_message(ClientErrorKind.INVALID_RESPONSE))
158
+ raise Exit(1)
159
+ return account_profile.data
160
+
161
+
162
+ def get_problem_summaries(limit: int = 50, skip: int = 0) -> list[ProblemSummary]:
163
+ _validate_show_options(limit, skip)
164
+ cookies = _load_cookies_from_session()
165
+ with LeetCodeClient(cookies) as client:
166
+ with loading("正在获取题目索引..."):
167
+ problem_list_data = client.problem_list(limit=limit, skip=skip)
168
+ if not problem_list_data.ok:
169
+ error(client_error_message(problem_list_data.error))
170
+ raise Exit(1)
171
+ problem_list = problem_list_data.data
172
+ if not isinstance(problem_list, dict):
173
+ error(client_error_message(ClientErrorKind.INVALID_RESPONSE))
174
+ raise Exit(1)
175
+ questions = problem_list.get("questions", [])
176
+
177
+ if not isinstance(questions, list):
178
+ error("题目获取失败")
179
+ raise Exit(1)
180
+ return normalize_problem_summaries(questions)
181
+
182
+
183
+ def get_problem_detail_by_question_id(question_id: str) -> ProblemDetail:
184
+ normalized_question_id = _parse_question_id_or_exit(question_id)
185
+ cookies = _load_cookies_from_session()
186
+ with LeetCodeClient(cookies) as client:
187
+ with loading("正在获取题目索引..."):
188
+ problem_summary = _find_problem_summary_by_question_id_online(
189
+ client,
190
+ normalized_question_id,
191
+ )
192
+ with loading("正在获取题目详情..."):
193
+ problem_detail_data = client.problem_detail(problem_summary.title_slug)
194
+ if not problem_detail_data.ok:
195
+ error(client_error_message(problem_detail_data.error))
196
+ raise Exit(1)
197
+ if not isinstance(problem_detail_data.data, dict):
198
+ error(client_error_message(ClientErrorKind.INVALID_RESPONSE))
199
+ raise Exit(1)
200
+ problem_detail = normalize_problem_detail(problem_detail_data.data)
201
+ return problem_detail
202
+
203
+
204
+ def get_doctor_report() -> DoctorReport:
205
+ session_check = diagnose_session()
206
+ solution_check = diagnose_solution()
207
+
208
+ cookies: dict[str, str] | None = None
209
+ try:
210
+ session = load_session()
211
+ except SessionFileError:
212
+ session = None
213
+ if isinstance(session, dict):
214
+ raw_cookies = session.get("cookies")
215
+ if isinstance(raw_cookies, dict):
216
+ valid_cookies: dict[str, str] = {}
217
+ for name in REQUIRED_COOKIE_NAMES:
218
+ value = raw_cookies.get(name)
219
+ if not isinstance(value, str) or not value:
220
+ break
221
+ valid_cookies[name] = value
222
+ else:
223
+ cookies = valid_cookies
224
+
225
+ with LeetCodeClient(cookies) as client:
226
+ remote_result = client.user_status()
227
+ connectivity_check, authentication_check = diagnose_remote(remote_result)
228
+ return DoctorReport(
229
+ checks=(
230
+ session_check,
231
+ connectivity_check,
232
+ authentication_check,
233
+ solution_check,
234
+ )
235
+ )
236
+
237
+
238
+ def submit_current_solution() -> dict | None:
239
+ try:
240
+ metadata, code = parse_solution_submission()
241
+ submit_question_id, title_slug = (
242
+ metadata.submit_question_id,
243
+ metadata.title_slug,
244
+ )
245
+ except WorkspaceError as exc:
246
+ error(str(exc))
247
+ raise Exit(1)
248
+ render_submission_target(metadata)
249
+ cookies = _load_cookies_from_session()
250
+ with LeetCodeClient(cookies) as client:
251
+ submission_id = client.submit_solution(title_slug, submit_question_id, code)
252
+ if not submission_id.ok:
253
+ error(client_error_message(submission_id.error))
254
+ raise Exit(1)
255
+
256
+ for _ in range(MAX_ATTEMPTS):
257
+ result = client.get_submission_result(submission_id.data)
258
+ if not result.ok:
259
+ error(client_error_message(result.error))
260
+ raise Exit(1)
261
+ result_data = result.data
262
+ if not isinstance(result_data, dict):
263
+ error(client_error_message(ClientErrorKind.INVALID_RESPONSE))
264
+ raise Exit(1)
265
+ state = result_data.get("state")
266
+ if state not in {"PENDING", "STARTED"}:
267
+ return result_data
268
+ sleep(0.5)
269
+ return None
@@ -0,0 +1,250 @@
1
+ from collections.abc import Iterator
2
+ from contextlib import contextmanager
3
+ from html import unescape
4
+ from html.parser import HTMLParser
5
+ import re
6
+ import shutil
7
+ from typing import Any
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+
13
+ from leetcode_local_cli.doctor import DoctorReport, DoctorStatus
14
+
15
+ console = Console(width=shutil.get_terminal_size(fallback=(120, 24)).columns)
16
+
17
+
18
+ def _terminal_width() -> int:
19
+ return shutil.get_terminal_size(fallback=(console.width, 24)).columns
20
+
21
+
22
+ class _ProblemHTMLParser(HTMLParser):
23
+ def __init__(self) -> None:
24
+ super().__init__()
25
+ self.parts: list[str] = []
26
+
27
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
28
+ if tag in {"p", "div", "ul", "ol", "pre"}:
29
+ self.parts.append("\n")
30
+ elif tag == "li":
31
+ self.parts.append("\n- ")
32
+ elif tag == "br":
33
+ self.parts.append("\n")
34
+ elif tag == "sup":
35
+ self.parts.append("^")
36
+ elif tag == "sub":
37
+ self.parts.append("_")
38
+
39
+ def handle_endtag(self, tag: str) -> None:
40
+ if tag in {"p", "div", "ul", "ol", "pre"}:
41
+ self.parts.append("\n")
42
+
43
+ def handle_data(self, data: str) -> None:
44
+ self.parts.append(data)
45
+
46
+
47
+ def _html_to_text(content_html: str) -> str:
48
+ parser = _ProblemHTMLParser()
49
+ parser.feed(content_html)
50
+ text = unescape("".join(parser.parts))
51
+ text = text.replace("\xa0", " ")
52
+ text = re.sub(r"[ \t]+\n", "\n", text)
53
+ text = re.sub(r"\n{3,}", "\n\n", text)
54
+ return text.strip()
55
+
56
+
57
+ @contextmanager
58
+ def loading(message: str) -> Iterator[None]:
59
+ with console.status(message, spinner="dots"):
60
+ yield
61
+
62
+
63
+ def success(message: str) -> None:
64
+ console.print(f"[bold green]{message}[/bold green]")
65
+
66
+
67
+ def warning(message: str) -> None:
68
+ console.print(f"[bold yellow]{message}[/bold yellow]")
69
+
70
+
71
+ def error(message: str) -> None:
72
+ console.print(f"[bold red]{message}[/bold red]")
73
+
74
+
75
+ def render_profile(profile: dict[str, Any]) -> None:
76
+ solved = profile["solved"]
77
+ total = profile["total"]
78
+
79
+ table = Table(title="LeetCode CN Profile")
80
+ table.add_column("Item", style="cyan", no_wrap=True)
81
+ table.add_column("Value", style="white")
82
+
83
+ table.add_row("Username", str(profile.get("username") or "-"))
84
+ table.add_row("Real Name", str(profile.get("real_name") or "-"))
85
+ table.add_row("Premium", "Yes" if profile.get("is_premium") else "No")
86
+ table.add_row(
87
+ "Solved",
88
+ f"All {solved['All']} | Easy {solved['Easy']} | Medium {solved['Medium']} | Hard {solved['Hard']}",
89
+ )
90
+ table.add_row(
91
+ "Total",
92
+ f"All {total['All']} | Easy {total['Easy']} | Medium {total['Medium']} | Hard {total['Hard']}",
93
+ )
94
+
95
+ console.print(Panel(table, border_style="green", width=_terminal_width()))
96
+
97
+
98
+ def render_problem_list(problems: list[Any]) -> None:
99
+ if not problems:
100
+ warning("没有可展示的题目")
101
+ return
102
+
103
+ table = Table(
104
+ title=f"LeetCode CN Problems ({len(problems)})",
105
+ border_style="cyan",
106
+ expand=True,
107
+ width=_terminal_width(),
108
+ )
109
+ table.add_column("ID", style="cyan", justify="right", no_wrap=True)
110
+ table.add_column("Title", style="white", overflow="fold")
111
+ table.add_column("Difficulty", justify="center", no_wrap=True)
112
+ table.add_column("Paid", justify="center", no_wrap=True)
113
+ table.add_column("Tags", style="dim", overflow="fold")
114
+
115
+ difficulty_styles = {
116
+ "Easy": "green",
117
+ "Medium": "yellow",
118
+ "Hard": "red",
119
+ }
120
+
121
+ for problem in problems:
122
+ difficulty = problem.difficulty or "-"
123
+ difficulty_style = difficulty_styles.get(difficulty, "white")
124
+ paid_text = (
125
+ "[yellow]Paid[/yellow]" if problem.paid_only else "[green]Free[/green]"
126
+ )
127
+ tags = ", ".join(problem.tags[:4])
128
+ if len(problem.tags) > 4:
129
+ tags = f"{tags}, ..."
130
+
131
+ table.add_row(
132
+ problem.question_id,
133
+ problem.title or "-",
134
+ f"[{difficulty_style}]{difficulty}[/{difficulty_style}]",
135
+ paid_text,
136
+ tags or "-",
137
+ )
138
+
139
+ console.print(table)
140
+
141
+
142
+ def render_problem_detail(problem: Any) -> None:
143
+ tags = ", ".join(problem.tags) if problem.tags else "-"
144
+
145
+ meta = Table.grid(padding=(0, 2))
146
+ meta.add_column(style="cyan", no_wrap=True)
147
+ meta.add_column(style="white")
148
+ meta.add_row("ID", problem.question_id)
149
+ meta.add_row("Slug", problem.title_slug)
150
+ meta.add_row("Difficulty", problem.difficulty)
151
+ meta.add_row("Tags", tags)
152
+
153
+ title = f"{problem.question_id}. {problem.title}"
154
+ console.print(
155
+ Panel(meta, title=title, border_style="cyan", width=_terminal_width())
156
+ )
157
+
158
+ content_text = _html_to_text(problem.content_html)
159
+ console.print(
160
+ Panel(
161
+ content_text or "-",
162
+ title="题面",
163
+ border_style="white",
164
+ width=_terminal_width(),
165
+ )
166
+ )
167
+
168
+ if not problem.python_code:
169
+ warning("未找到 Python3 代码模板")
170
+
171
+
172
+ def render_submission_target(metadata: Any) -> None:
173
+ console.print(
174
+ "[bold cyan]当前提交目标:"
175
+ f"{metadata.problem_id}. {metadata.title} ({metadata.title_slug})"
176
+ "[/bold cyan]"
177
+ )
178
+
179
+
180
+ def render_doctor_report(report: DoctorReport) -> None:
181
+ labels = {
182
+ "session": "Session 文件",
183
+ "connectivity": "LeetCode 接口",
184
+ "authentication": "Cookie 登录态",
185
+ "solution": "solution.py",
186
+ }
187
+ status_labels = {
188
+ DoctorStatus.PASS: ("PASS", "green"),
189
+ DoctorStatus.WARNING: ("WARNING", "yellow"),
190
+ DoctorStatus.FAIL: ("FAIL", "red"),
191
+ }
192
+ table = Table(title="环境诊断", expand=True)
193
+ table.add_column("检查项", style="cyan", no_wrap=True)
194
+ table.add_column("状态", justify="center", no_wrap=True)
195
+ table.add_column("结果", overflow="fold")
196
+ table.add_column("建议", overflow="fold")
197
+
198
+ for check in report.checks:
199
+ status_text, status_style = status_labels[check.status]
200
+ table.add_row(
201
+ labels.get(check.name, check.name),
202
+ f"[{status_style}]{status_text}[/{status_style}]",
203
+ check.message,
204
+ check.suggestion or "-",
205
+ )
206
+
207
+ border_style = "green" if report.ok else "red"
208
+ console.print(
209
+ Panel(
210
+ table,
211
+ border_style=border_style,
212
+ width=_terminal_width(),
213
+ )
214
+ )
215
+
216
+
217
+ def render_submission_result(result: dict[str, Any] | None) -> None:
218
+ if result is None:
219
+ error("判题超时,请稍后到 LeetCode 查看结果")
220
+ return
221
+
222
+ status_msg = str(result.get("status_msg") or "-")
223
+ runtime = str(result.get("status_runtime") or result.get("runtime") or "-")
224
+ memory = str(result.get("memory") or "-")
225
+ total_correct = result.get("total_correct")
226
+ total_testcases = result.get("total_testcases")
227
+
228
+ if status_msg == "Accepted":
229
+ success("通过")
230
+ else:
231
+ error(f"提交失败:{status_msg}")
232
+
233
+ table = Table(
234
+ title="判题结果", border_style="green" if status_msg == "Accepted" else "red"
235
+ )
236
+ table.add_column("Item", style="cyan", no_wrap=True)
237
+ table.add_column("Value", style="white")
238
+ table.add_row("Status", status_msg)
239
+ table.add_row("Runtime", runtime)
240
+ table.add_row("Memory", memory)
241
+ if total_correct is not None and total_testcases is not None:
242
+ table.add_row("Cases", f"{total_correct} / {total_testcases}")
243
+
244
+ console.print(
245
+ Panel(
246
+ table,
247
+ border_style="green" if status_msg == "Accepted" else "red",
248
+ width=_terminal_width(),
249
+ )
250
+ )
@@ -0,0 +1,12 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+
4
+ PACKAGE_NAME = "leetcode-local-cli"
5
+
6
+
7
+ def get_version() -> str:
8
+ """Return the installed distribution version."""
9
+ try:
10
+ return version(PACKAGE_NAME)
11
+ except PackageNotFoundError:
12
+ return "unknown"