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.
- leetcode_local_cli/__init__.py +1 -0
- leetcode_local_cli/__main__.py +3 -0
- leetcode_local_cli/auth.py +215 -0
- leetcode_local_cli/cli.py +217 -0
- leetcode_local_cli/client.py +373 -0
- leetcode_local_cli/doctor.py +277 -0
- leetcode_local_cli/problem.py +166 -0
- leetcode_local_cli/service.py +269 -0
- leetcode_local_cli/ui.py +250 -0
- leetcode_local_cli/version.py +12 -0
- leetcode_local_cli/workspace.py +228 -0
- leetcode_local_cli-0.7.0.dist-info/METADATA +253 -0
- leetcode_local_cli-0.7.0.dist-info/RECORD +16 -0
- leetcode_local_cli-0.7.0.dist-info/WHEEL +4 -0
- leetcode_local_cli-0.7.0.dist-info/entry_points.txt +3 -0
- leetcode_local_cli-0.7.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
PROJECT_ROOT = Path.cwd()
|
|
9
|
+
SOLUTION_FILE = PROJECT_ROOT / "solution.py"
|
|
10
|
+
|
|
11
|
+
METADATA_PREFIX = "# @lc "
|
|
12
|
+
START_FLAG = "# @lc submit_begin"
|
|
13
|
+
END_FLAG = "# @lc submit_end"
|
|
14
|
+
|
|
15
|
+
SOLUTION_FILE_HEADER = """# pyright: reportUnusedImport=false, reportUnusedVariable=false
|
|
16
|
+
# ruff: noqa: F401, F841
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
SOLUTION_IMPORTS = """from typing import Any, Dict, List, Optional, Set, Tuple
|
|
20
|
+
from collections import Counter, defaultdict, deque
|
|
21
|
+
from functools import cache, lru_cache
|
|
22
|
+
from itertools import accumulate, combinations, permutations, product
|
|
23
|
+
from bisect import bisect_left, bisect_right, insort
|
|
24
|
+
from heapq import heapify, heappop, heappush
|
|
25
|
+
from math import gcd, inf, isqrt, lcm
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
SOLUTION_CASE = """
|
|
29
|
+
def run_cases() -> None:
|
|
30
|
+
solution = Solution()
|
|
31
|
+
# Add local assertions here, for example:
|
|
32
|
+
# assert solution.twoSum([2, 7, 11, 15], 9) == [0, 1]
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
run_cases()
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ProblemMetadata:
|
|
43
|
+
problem_id: str
|
|
44
|
+
submit_question_id: str
|
|
45
|
+
title: str
|
|
46
|
+
title_slug: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WorkspaceError(ValueError):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SolutionFileStatus(str, Enum):
|
|
54
|
+
READY = "ready"
|
|
55
|
+
MISSING = "missing"
|
|
56
|
+
EMPTY = "empty"
|
|
57
|
+
READ_ERROR = "read_error"
|
|
58
|
+
INVALID_SYNTAX = "invalid_syntax"
|
|
59
|
+
NOT_SUBMITTABLE = "not_submittable"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class SolutionFileInspection:
|
|
64
|
+
status: SolutionFileStatus
|
|
65
|
+
metadata: ProblemMetadata | None = None
|
|
66
|
+
detail: str = ""
|
|
67
|
+
syntax_line: int | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _normalize_python_code(python_code: str) -> str:
|
|
71
|
+
code = python_code.rstrip()
|
|
72
|
+
if code and code.splitlines()[-1].rstrip().endswith(":"):
|
|
73
|
+
return f"{code} pass"
|
|
74
|
+
return code
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_solution_content(python_code: str, metadata: ProblemMetadata) -> str:
|
|
78
|
+
metadata_content = (
|
|
79
|
+
f"{METADATA_PREFIX}problem_id: {metadata.problem_id}\n"
|
|
80
|
+
f"{METADATA_PREFIX}submit_question_id: {metadata.submit_question_id}\n"
|
|
81
|
+
f"{METADATA_PREFIX}title: {metadata.title}\n"
|
|
82
|
+
f"{METADATA_PREFIX}title_slug: {metadata.title_slug}\n\n"
|
|
83
|
+
)
|
|
84
|
+
return (
|
|
85
|
+
f"{SOLUTION_FILE_HEADER}"
|
|
86
|
+
f"{metadata_content}"
|
|
87
|
+
f"{SOLUTION_IMPORTS}\n\n"
|
|
88
|
+
f"{START_FLAG}\n"
|
|
89
|
+
f"{_normalize_python_code(python_code)}\n"
|
|
90
|
+
f"{END_FLAG}\n\n"
|
|
91
|
+
f"{SOLUTION_CASE}\n"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write_solution_file(python_code: str, metadata: ProblemMetadata) -> None:
|
|
96
|
+
with open(SOLUTION_FILE, "w", encoding="utf-8") as file:
|
|
97
|
+
file.write(build_solution_content(python_code, metadata))
|
|
98
|
+
open_path(SOLUTION_FILE)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def open_path(path: Path) -> None:
|
|
102
|
+
if sys.platform == "win32":
|
|
103
|
+
import os
|
|
104
|
+
|
|
105
|
+
os.startfile(path)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
command = "open" if sys.platform == "darwin" else "xdg-open"
|
|
109
|
+
try:
|
|
110
|
+
subprocess.Popen(
|
|
111
|
+
[command, str(path)],
|
|
112
|
+
stdout=subprocess.DEVNULL,
|
|
113
|
+
stderr=subprocess.DEVNULL,
|
|
114
|
+
)
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_solution_file(
|
|
120
|
+
path: Path | None = None,
|
|
121
|
+
*,
|
|
122
|
+
timeout: float | None = None,
|
|
123
|
+
) -> subprocess.CompletedProcess[str]:
|
|
124
|
+
solution_file = path or SOLUTION_FILE
|
|
125
|
+
return subprocess.run(
|
|
126
|
+
[sys.executable, str(solution_file)],
|
|
127
|
+
capture_output=True,
|
|
128
|
+
text=True,
|
|
129
|
+
cwd=PROJECT_ROOT,
|
|
130
|
+
timeout=timeout,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_solution_submission_content(content: str) -> tuple[ProblemMetadata, str]:
|
|
135
|
+
metadata: dict[str, str] = {}
|
|
136
|
+
start_flag = end_flag = False
|
|
137
|
+
submission_lines: list[str] = []
|
|
138
|
+
for string in content.splitlines(keepends=True):
|
|
139
|
+
line = string.strip()
|
|
140
|
+
if line == END_FLAG:
|
|
141
|
+
end_flag = True
|
|
142
|
+
break
|
|
143
|
+
if start_flag:
|
|
144
|
+
submission_lines.append(string)
|
|
145
|
+
if line.startswith(METADATA_PREFIX):
|
|
146
|
+
item = line.removeprefix(METADATA_PREFIX)
|
|
147
|
+
key, separator, value = item.partition(":")
|
|
148
|
+
if separator:
|
|
149
|
+
metadata[key.strip()] = value.strip()
|
|
150
|
+
if line == START_FLAG:
|
|
151
|
+
start_flag = True
|
|
152
|
+
|
|
153
|
+
if not start_flag or not end_flag:
|
|
154
|
+
raise WorkspaceError("solution.py 提交区域标记不完整,请先执行 lc solve <题号>")
|
|
155
|
+
|
|
156
|
+
submission_code = "".join(submission_lines).strip()
|
|
157
|
+
if not submission_code:
|
|
158
|
+
raise WorkspaceError("solution.py 提交区域为空")
|
|
159
|
+
|
|
160
|
+
if not all(
|
|
161
|
+
[
|
|
162
|
+
metadata.get("problem_id"),
|
|
163
|
+
metadata.get("submit_question_id"),
|
|
164
|
+
metadata.get("title"),
|
|
165
|
+
metadata.get("title_slug"),
|
|
166
|
+
]
|
|
167
|
+
):
|
|
168
|
+
raise WorkspaceError(
|
|
169
|
+
"solution.py 缺少元数据:problem_id、submit_question_id、title、title_slug"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
ProblemMetadata(
|
|
174
|
+
problem_id=metadata["problem_id"],
|
|
175
|
+
submit_question_id=metadata["submit_question_id"],
|
|
176
|
+
title=metadata["title"],
|
|
177
|
+
title_slug=metadata["title_slug"],
|
|
178
|
+
),
|
|
179
|
+
submission_code,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def parse_solution_submission(
|
|
184
|
+
path: Path | None = None,
|
|
185
|
+
) -> tuple[ProblemMetadata, str]:
|
|
186
|
+
solution_file = path or SOLUTION_FILE
|
|
187
|
+
try:
|
|
188
|
+
content = solution_file.read_text(encoding="utf-8")
|
|
189
|
+
except FileNotFoundError as exc:
|
|
190
|
+
raise WorkspaceError("未找到 solution.py,请先执行 lc solve <题号>") from exc
|
|
191
|
+
except OSError as exc:
|
|
192
|
+
raise WorkspaceError("无法读取 solution.py,请检查文件权限") from exc
|
|
193
|
+
return _parse_solution_submission_content(content)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def inspect_solution_file(path: Path | None = None) -> SolutionFileInspection:
|
|
197
|
+
solution_file = path or SOLUTION_FILE
|
|
198
|
+
try:
|
|
199
|
+
content = solution_file.read_text(encoding="utf-8")
|
|
200
|
+
except FileNotFoundError:
|
|
201
|
+
return SolutionFileInspection(status=SolutionFileStatus.MISSING)
|
|
202
|
+
except OSError:
|
|
203
|
+
return SolutionFileInspection(status=SolutionFileStatus.READ_ERROR)
|
|
204
|
+
|
|
205
|
+
if not content.strip():
|
|
206
|
+
return SolutionFileInspection(status=SolutionFileStatus.EMPTY)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
compile(content, str(solution_file), "exec")
|
|
210
|
+
except SyntaxError as exc:
|
|
211
|
+
return SolutionFileInspection(
|
|
212
|
+
status=SolutionFileStatus.INVALID_SYNTAX,
|
|
213
|
+
detail=exc.msg,
|
|
214
|
+
syntax_line=exc.lineno,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
metadata, _ = _parse_solution_submission_content(content)
|
|
219
|
+
except WorkspaceError as exc:
|
|
220
|
+
return SolutionFileInspection(
|
|
221
|
+
status=SolutionFileStatus.NOT_SUBMITTABLE,
|
|
222
|
+
detail=str(exc),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return SolutionFileInspection(
|
|
226
|
+
status=SolutionFileStatus.READY,
|
|
227
|
+
metadata=metadata,
|
|
228
|
+
)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leetcode-local-cli
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Local coding workflow CLI for LeetCode CN.
|
|
5
|
+
Keywords: cli,leetcode,leetcode-cn,uv
|
|
6
|
+
Author: Aetherialter
|
|
7
|
+
Author-email: Aetherialter <Aetherialter@outlook.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: browser-cookie3>=0.20.1
|
|
16
|
+
Requires-Dist: httpx>=0.28.1
|
|
17
|
+
Requires-Dist: rich>=15.0.0
|
|
18
|
+
Requires-Dist: typer>=0.25.1
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Project-URL: Homepage, https://github.com/Aetherialter/leetcode-local-cli
|
|
21
|
+
Project-URL: Issues, https://github.com/Aetherialter/leetcode-local-cli/issues
|
|
22
|
+
Project-URL: Repository, https://github.com/Aetherialter/leetcode-local-cli
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# 力扣中文站本地化刷题 CLI 工具
|
|
26
|
+
|
|
27
|
+
一个面向 LeetCode 中文站的轻量本地刷题 CLI。它复用浏览器登录态,在线获取题目,在当前工作目录生成单文件 `solution.py`,并支持本地测试和远程提交。
|
|
28
|
+
|
|
29
|
+
当前版本:`v0.7.0`
|
|
30
|
+
|
|
31
|
+
## 核心能力
|
|
32
|
+
|
|
33
|
+
- 读取 Chrome 中的 `leetcode.cn` Cookie,复用登录态。
|
|
34
|
+
- 在线获取题目详情和题目索引。
|
|
35
|
+
- 使用当前工作目录的 `solution.py` 作为唯一主要工作区。
|
|
36
|
+
- `lc solve <题号>` 生成可编辑的 Python 解题模板。
|
|
37
|
+
- 生成模板后会按当前系统尝试打开 `solution.py`。
|
|
38
|
+
- `lc test` 运行 `solution.py` 中的 `run_cases()`。
|
|
39
|
+
- `lc doctor` 一次检查 Session 文件、站点连通性、Cookie 登录态和本地解题文件。
|
|
40
|
+
- `lc submit` 提交 marker 区域代码到 LeetCode 中文站,并轮询判题结果。
|
|
41
|
+
- 生成模板时写入 `problem_id` 和 `submit_question_id`,避免展示题号和 LeetCode 内部提交 ID 混用。
|
|
42
|
+
- 生成模板时加入 Pyright/Ruff 文件级配置,减少刷题工作区的未使用导入、未使用变量和 star import 相关提示。
|
|
43
|
+
- 使用统一的客户端错误结果类型区分网络、HTTP、JSON、接口结构和登录态错误。
|
|
44
|
+
|
|
45
|
+
## 环境要求
|
|
46
|
+
|
|
47
|
+
- Windows / Linux / macOS
|
|
48
|
+
- Python 3.12+
|
|
49
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
50
|
+
- 已在浏览器中登录 LeetCode 中文站
|
|
51
|
+
|
|
52
|
+
## 安装
|
|
53
|
+
|
|
54
|
+
Linux / macOS 一键安装:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
curl -LsSf https://raw.githubusercontent.com/Aetherialter/leetcode-local-cli/v0.7.0/scripts/install.sh | sh
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Windows PowerShell 一键安装:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
powershell -ExecutionPolicy ByPass -Command "irm https://raw.githubusercontent.com/Aetherialter/leetcode-local-cli/v0.7.0/scripts/install.ps1 | iex"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
安装器会在缺少 uv 时通过 uv 官方 HTTPS 安装器完成引导,然后使用 `uv tool install` 安装 `leetcode-local-cli`,并执行 `lc --version` 验证结果。安装过程不使用 `sudo`,也不会保存 PyPI 或 GitHub 凭据。
|
|
67
|
+
|
|
68
|
+
已经安装 uv 时,也可以直接安装:
|
|
69
|
+
|
|
70
|
+
```shell
|
|
71
|
+
uv tool install leetcode-local-cli
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
升级或卸载:
|
|
75
|
+
|
|
76
|
+
```shell
|
|
77
|
+
uv tool upgrade leetcode-local-cli
|
|
78
|
+
uv tool uninstall leetcode-local-cli
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
如果安装完成后当前终端仍找不到 `lc`,请重新打开终端,或执行 `uv tool update-shell` 后重载 shell 配置。
|
|
82
|
+
|
|
83
|
+
## 基本工作流
|
|
84
|
+
|
|
85
|
+
```shell
|
|
86
|
+
lc login
|
|
87
|
+
lc doctor
|
|
88
|
+
lc status
|
|
89
|
+
lc get 1
|
|
90
|
+
lc solve 1
|
|
91
|
+
lc test
|
|
92
|
+
lc submit
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
常用命令:
|
|
96
|
+
|
|
97
|
+
| 命令 | 作用 |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `lc login` | 读取或手动录入 LeetCode 中文站 Cookie |
|
|
100
|
+
| `lc status` | 检查当前登录态 |
|
|
101
|
+
| `lc profile` | 展示账号和刷题统计 |
|
|
102
|
+
| `lc show --limit 20 --skip 0` | 分页展示题目索引,`limit` 单次最大为 100 |
|
|
103
|
+
| `lc get <题号>` | 在线展示题目详情 |
|
|
104
|
+
| `lc solve <题号>` | 覆盖生成当前目录的 `solution.py` |
|
|
105
|
+
| `lc test` | 运行本地 `solution.py` |
|
|
106
|
+
| `lc doctor` | 诊断 Session、网络、Cookie 和 `solution.py` |
|
|
107
|
+
| `lc submit` | 提交当前 `solution.py` 的提交区域代码 |
|
|
108
|
+
|
|
109
|
+
## solution.py 规则
|
|
110
|
+
|
|
111
|
+
`lc solve` 会覆盖当前目录的 `solution.py`。切换题目前请自行保存当前解法。
|
|
112
|
+
|
|
113
|
+
生成文件会包含题目元信息和提交区域:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# pyright: reportUnusedImport=false, reportUnusedVariable=false
|
|
117
|
+
# ruff: noqa: F401, F841
|
|
118
|
+
# @lc problem_id: 1
|
|
119
|
+
# @lc submit_question_id: 1
|
|
120
|
+
# @lc title: Two Sum
|
|
121
|
+
# @lc title_slug: two-sum
|
|
122
|
+
|
|
123
|
+
# @lc submit_begin
|
|
124
|
+
class Solution:
|
|
125
|
+
def twoSum(self, nums: List[int], target: int) -> List[int]: pass
|
|
126
|
+
# @lc submit_end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`lc submit` 只提交:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
# @lc submit_begin
|
|
133
|
+
...
|
|
134
|
+
# @lc submit_end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
之间的代码。
|
|
138
|
+
|
|
139
|
+
## 当前限制
|
|
140
|
+
|
|
141
|
+
- 当前仅支持 LeetCode 中文站。
|
|
142
|
+
- 当前仅自动读取 Chrome Cookie。
|
|
143
|
+
- `lc solve` 生成模板后会尝试打开 `solution.py`;Windows 使用系统默认打开方式,macOS 使用 `open`,Linux 使用 `xdg-open`。
|
|
144
|
+
- 当前远程提交仅支持 Python3。
|
|
145
|
+
- 当前只维护 CLI 启动目录中的单个 `solution.py`,不会生成每题独立目录。
|
|
146
|
+
- 当前不保存完整题面到本地,也不引入本地数据库。
|
|
147
|
+
- `lc solve` 会强制覆盖 `solution.py`。
|
|
148
|
+
- `lc show` 的 `limit` 必须为正整数且不超过 100,`skip` 必须为非负整数。
|
|
149
|
+
- `lc test` 默认隐藏 Python traceback,只展示本地测试通过或失败。
|
|
150
|
+
- 如果 `run_cases()` 中没有断言,`lc test` 显示通过是当前轻量化设计允许的行为。
|
|
151
|
+
- 树、链表等题型中,LeetCode 模板里的 `TreeNode` / `ListNode` 定义默认保持注释状态;如需本地构造用例,请自行取消注释并编写测试数据。
|
|
152
|
+
- `lc doctor` 会向 LeetCode 中文站发送一次登录态查询,并在本地运行语法有效的 `solution.py`;本地运行超过 10 秒会判定失败。
|
|
153
|
+
|
|
154
|
+
## 安全说明
|
|
155
|
+
|
|
156
|
+
登录态会保存到:
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
.leetcode_local_cli/session.json
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
该文件可能包含敏感 Cookie 信息,已在 `.gitignore` 中忽略。v0.7 首次读取登录态时会把旧版 `.aether_lc/session.json` 自动迁移到新目录;迁移过程不会输出 Cookie 值。发布前请确认仓库根目录的 `solution.py` 为空,避免把个人解法提交到公开仓库。
|
|
163
|
+
|
|
164
|
+
## 登录态说明
|
|
165
|
+
|
|
166
|
+
`leetcode-local-cli` 会把浏览器 Cookie 的本地副本保存到 CLI 启动目录下的 `.leetcode_local_cli/session.json`。本地保存的 Cookie 不会延长 LeetCode 登录态有效期;实际是否有效以 LeetCode 服务端验证为准。v0.8 的 `lc init` 会进一步将用户登录态与工作区分离。
|
|
167
|
+
|
|
168
|
+
如果浏览器 Cookie 刷新,或旧 Cookie 被服务端判定失效,CLI 可能在 `lc status`、`lc profile` 或 `lc submit` 时提示重新执行:
|
|
169
|
+
|
|
170
|
+
```shell
|
|
171
|
+
lc login
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`lc show` 和 `lc get` 访问公开题目数据,可能在登录态失效时仍然可用。遇到登录态、网络或本地解题文件问题时,可以执行:
|
|
175
|
+
|
|
176
|
+
```shell
|
|
177
|
+
lc doctor
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
诊断输出只包含用户名、Cookie 缺失项等安全元数据,不会展示 Cookie 值。
|
|
181
|
+
|
|
182
|
+
## 开发与验证
|
|
183
|
+
|
|
184
|
+
```shell
|
|
185
|
+
uv run ruff format src tests scripts
|
|
186
|
+
uv run ruff check src tests scripts pyproject.toml
|
|
187
|
+
uv run pyright src tests scripts
|
|
188
|
+
uv run pytest
|
|
189
|
+
uv build --no-sources
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
发布前常用手动检查:
|
|
193
|
+
|
|
194
|
+
```shell
|
|
195
|
+
uv run lc --help
|
|
196
|
+
uv run lc --version
|
|
197
|
+
uv run lc doctor
|
|
198
|
+
uv run lc get 2196
|
|
199
|
+
uv run lc solve 1
|
|
200
|
+
uv run lc test
|
|
201
|
+
uv run ruff check solution.py
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
维护者发布流程见 [RELEASING.md](https://github.com/Aetherialter/leetcode-local-cli/blob/main/RELEASING.md)。标签触发的发布工作流会在 Linux、macOS 和 Windows 上验收安装器,分别验证 wheel 与源码包,通过 PyPI Trusted Publisher 上传发行包,并创建 GitHub Release。
|
|
205
|
+
|
|
206
|
+
## 项目结构
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
src/leetcode_local_cli/
|
|
210
|
+
auth.py Cookie 读取与本地 session
|
|
211
|
+
client.py LeetCode 中文站 HTTP 客户端
|
|
212
|
+
cli.py Typer 命令入口
|
|
213
|
+
doctor.py 本地环境、网络与登录态诊断
|
|
214
|
+
problem.py 题号解析与题目数据标准化
|
|
215
|
+
service.py 应用层流程编排
|
|
216
|
+
ui.py Rich 终端输出
|
|
217
|
+
version.py 已安装发行版版本读取
|
|
218
|
+
workspace.py solution.py 生成、解析与运行
|
|
219
|
+
scripts/
|
|
220
|
+
install.sh Linux/macOS 一键安装器
|
|
221
|
+
install.ps1 Windows PowerShell 一键安装器
|
|
222
|
+
smoke_test.py wheel 与源码包发布验收
|
|
223
|
+
.github/workflows/
|
|
224
|
+
release.yml 跨平台验证、PyPI 发布与 GitHub Release
|
|
225
|
+
tests/
|
|
226
|
+
test_auth.py
|
|
227
|
+
test_cli.py
|
|
228
|
+
test_client.py
|
|
229
|
+
test_doctor.py
|
|
230
|
+
test_install_scripts.py
|
|
231
|
+
test_problem.py
|
|
232
|
+
test_service.py
|
|
233
|
+
test_ui.py
|
|
234
|
+
test_workspace.py
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## 版本路线
|
|
238
|
+
|
|
239
|
+
- `v0.5.x`: 远程提交上线后的 bugfix patch 线。
|
|
240
|
+
- `v0.5.4`: 修复 GraphQL `data: null` 导致 traceback,并精简 README。
|
|
241
|
+
- `v0.5.5`: 引入客户端错误结果类型,收敛 service 层错误处理并补充边界测试。
|
|
242
|
+
- `v0.5.6`: 收束 `lc show` 参数校验,避免非法分页参数触发远端接口异常提示。
|
|
243
|
+
- `v0.5.7`: 修复非 Windows 环境导入 `os.startfile` 导致 CLI 无法启动的问题。
|
|
244
|
+
- `v0.6.0`: 新增 `lc doctor`,完善客户端边界校验、本地文件诊断、错误提示和提交目标展示。
|
|
245
|
+
- `v0.7.0`: uv 全局工具安装、跨平台引导脚本和 PyPI Trusted Publisher 发布流程。
|
|
246
|
+
- `v0.8`: `lc init` 与正式工作区管理。
|
|
247
|
+
- `v0.9`: 轻量缓存。
|
|
248
|
+
- `v0.10`: 样例提取原型。
|
|
249
|
+
- `v1.0`: 稳定轻量 CLI。
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
本项目使用 [MIT License](LICENSE) 开源。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
leetcode_local_cli/__init__.py,sha256=B3XJFwkbcjwQG_1sXnTjOw5wJ1zRKgwm-PjLjlkZnaw,43
|
|
2
|
+
leetcode_local_cli/__main__.py,sha256=yQP0sf-rkTO-3MOotv4blvukvNGwbJFz10hQhMsxcJM,28
|
|
3
|
+
leetcode_local_cli/auth.py,sha256=qBQEu6SQJjOGbRtsMhzMAxb7cc3Z_j8H7QGdDRgNzkw,6431
|
|
4
|
+
leetcode_local_cli/cli.py,sha256=9o5Bki56j1AAWVGuSmU1dRofkDO4vDGSuNvXMzDn5VU,6375
|
|
5
|
+
leetcode_local_cli/client.py,sha256=IBkK5v2Y4dXpRHrtijN-e1Unv5m9PvZqNKUG5GCixO4,11323
|
|
6
|
+
leetcode_local_cli/doctor.py,sha256=2MW87MtYxkkr4AzbMcebRPJjb5P4FM8YbH8in6mXUnw,10354
|
|
7
|
+
leetcode_local_cli/problem.py,sha256=tcjPKNzg2LT9qZtx5tf1VCcOn_GDnRokItYx2QvwuCU,4524
|
|
8
|
+
leetcode_local_cli/service.py,sha256=YLp3FDO3Pe68PrxUIZOFqqZ5XrWfW1mYrtyoHCggO98,9424
|
|
9
|
+
leetcode_local_cli/ui.py,sha256=kzFZc-G2UpZWqkLfFth3kfptXQKi_MDT-0WxIjxUZyc,7723
|
|
10
|
+
leetcode_local_cli/version.py,sha256=lot83NuspSD8aadUsElYtf2-G7GbsfY3nQv5dycP2U0,284
|
|
11
|
+
leetcode_local_cli/workspace.py,sha256=zFJ2nR5oeX6GVTwo48BTJIKuY9_SB5s2mf-etREYO8Q,6636
|
|
12
|
+
leetcode_local_cli-0.7.0.dist-info/licenses/LICENSE,sha256=062CGAxo62KjC8aN9OAU7nTfizRZQnLzl8jgDVuUos0,1069
|
|
13
|
+
leetcode_local_cli-0.7.0.dist-info/WHEEL,sha256=CoDSoyhtC_eO_tlxRYzsTraPv1fPJRXFx91k6ISeAvA,81
|
|
14
|
+
leetcode_local_cli-0.7.0.dist-info/entry_points.txt,sha256=DrlxdzFemHi1_pUunmxb3TSrsKt1q0JkWYGY4sPODX4,51
|
|
15
|
+
leetcode_local_cli-0.7.0.dist-info/METADATA,sha256=t42rKBtsvjpJP9Lkg34txszESij9o7D_I4j1nT_kemU,9301
|
|
16
|
+
leetcode_local_cli-0.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aetherialter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|