Skip to content

Commit fc9ee78

Browse files
committed
[🚀] Project init
1 parent 622c8c4 commit fc9ee78

File tree

12 files changed

+1508
-0
lines changed

12 files changed

+1508
-0
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# local-leetcode
2+
3+
**LeetCli**
4+
A Command-Line Interface (CLI) tool to browse and solve LeetCode problems directly from the terminal.
5+
6+
![Python](https://img.shields.io/badge/python-3.9+-blue)
7+
![License](https://img.shields.io/badge/license-MIT-green)
8+
9+
---
10+
11+
## Features
12+
13+
LeetCli provides a convenient way to:
14+
15+
- Log in / log out to LeetCode
16+
- Check user status and problem-solving progress
17+
- Download problems (including the daily problem)
18+
- Submit solutions
19+
- Set a default programming language for submissions
20+
21+
---
22+
23+
## Installation
24+
25+
You can install LeetCli via **PyPI**:
26+
27+
```bash
28+
pip install local-leetcode

leetcli/auth/user.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import os
2+
import json
3+
import platform
4+
from typing import (
5+
Optional,
6+
Tuple,
7+
Union
8+
)
9+
from http.cookiejar import CookieJar
10+
import requests
11+
import browser_cookie3
12+
from tabulate import tabulate
13+
from pathlib import Path
14+
from leetcli.utils.req import (
15+
_req_header,
16+
_req_cookies,
17+
)
18+
from leetcli.utils.query import (
19+
_req_user_progress_v2_query
20+
)
21+
from leetcli.utils.variable import (
22+
_req_user_progress_variable
23+
)
24+
25+
LEETCODE_URL = "https://leetcode.com/graphql"
26+
USERINFO_DIR = Path(os.path.expanduser("~/.leetcode-cli"))
27+
USERINFO_FILE = USERINFO_DIR / "session.json"
28+
LEETCODE_DOMAIN = "leetcode.com"
29+
30+
31+
class UserInfoManager:
32+
def __init__(self):
33+
pass
34+
35+
def _get_userinfo(
36+
self,
37+
url
38+
) -> Optional[Tuple[str, bool]]:
39+
try:
40+
with open(USERINFO_FILE, "r") as f:
41+
cookies_dict = json.load(f)
42+
session = requests.Session()
43+
for k, v in cookies_dict.items():
44+
session.cookies.set(k, v)
45+
username, is_active = self._test_userinfo(url, session)
46+
return username, is_active
47+
48+
except FileNotFoundError as e:
49+
raise e
50+
51+
except ConnectionError as e:
52+
raise e
53+
54+
except Exception as e:
55+
raise e
56+
57+
def _create_userinfo(
58+
self
59+
) -> Optional[Union[CookieJar, str]]:
60+
"""
61+
Create New Session from Leetcode
62+
"""
63+
cookies = None
64+
current_os = platform.system()
65+
browsers_support = {
66+
"chrome": ["Linux", "Darwin", "Windows"],
67+
"firefox": ["Linux", "Darwin", "Windows"],
68+
"librewolf": ["Linux", "Darwin", "Windows"],
69+
"opera": ["Linux", "Darwin", "Windows"],
70+
"opera_gx": ["Darwin", "Windows"],
71+
"edge": ["Linux", "Darwin", "Windows"],
72+
"chromium": ["Linux", "Darwin", "Windows"],
73+
"brave": ["Linux", "Darwin", "Windows"],
74+
"vivaldi": ["Linux", "Darwin", "Windows"],
75+
"w3m": ["Linux"],
76+
"lynx": ["Linux"],
77+
"safari": ["Darwin"],
78+
}
79+
80+
for browser_name, os_list in browsers_support.items():
81+
if current_os not in os_list:
82+
continue
83+
try:
84+
func = getattr(browser_cookie3, browser_name)
85+
except AttributeError:
86+
continue
87+
88+
try:
89+
cookies = func(domain_name="leetcode.com")
90+
if cookies:
91+
break
92+
except Exception as e:
93+
continue
94+
return cookies
95+
96+
def _save_userinfo(
97+
self,
98+
cookies,
99+
default_language: str | None,
100+
LEETCODE_LANGUAGES
101+
) -> None:
102+
dir_path = os.path.dirname(USERINFO_FILE)
103+
os.makedirs(dir_path, exist_ok=True)
104+
important_info = {}
105+
for cookie in cookies:
106+
if cookie.name in ("LEETCODE_SESSION", "csrftoken"):
107+
important_info[cookie.name] = cookie.value
108+
if default_language and LEETCODE_LANGUAGES:
109+
lang_key = default_language.strip().lower()
110+
if lang_key not in LEETCODE_LANGUAGES:
111+
raise ValueError(
112+
f"Unsupported language: {default_language}\n"
113+
f"Available options: {', '.join(sorted(set(LEETCODE_LANGUAGES.values())))}"
114+
)
115+
important_info["language"] = LEETCODE_LANGUAGES[lang_key]
116+
with open(USERINFO_FILE, "w") as f:
117+
json.dump(important_info, f, indent=2)
118+
119+
def _test_userinfo(
120+
self,
121+
url,
122+
session
123+
) -> Tuple[bool, Optional[str]]:
124+
try:
125+
headers = {
126+
"Referer": "https://leetcode.com/",
127+
"Content-Type": "application/json",
128+
"x-csrftoken": session.cookies.get("csrftoken"),
129+
"User-Agent": "Mozilla/5.0",
130+
}
131+
132+
query = {
133+
"operationName": "globalData",
134+
"variables": {},
135+
"query": """
136+
query globalData {
137+
userStatus {
138+
isSignedIn
139+
username
140+
}
141+
}
142+
"""
143+
}
144+
resp = session.post(url, headers=headers, json=query)
145+
data = resp.json()
146+
user_status = data.get("data", {}).get("userStatus", {})
147+
is_signed_in = user_status.get("isSignedIn", False)
148+
username = user_status.get("username")
149+
150+
if is_signed_in and username:
151+
return True, username
152+
else:
153+
return False, None
154+
155+
except Exception as e:
156+
raise ConnectionError
157+
158+
def _delete_userinfo(
159+
self
160+
) -> bool:
161+
try:
162+
if USERINFO_FILE.exists():
163+
USERINFO_FILE.unlink()
164+
return True
165+
else:
166+
return False
167+
except Exception as e:
168+
return False
169+
170+
def _get_csrftoken(
171+
self
172+
) -> str:
173+
with open(USERINFO_FILE, "r") as f:
174+
userinfo_dict = json.load(f)
175+
return userinfo_dict['csrftoken']
176+
177+
def _get_session(
178+
self
179+
) -> str:
180+
with open(USERINFO_FILE, "r") as f:
181+
userinfo_dict = json.load(f)
182+
return userinfo_dict['LEETCODE_SESSION']
183+
184+
def _set_lang(
185+
self,
186+
language,
187+
LEETCODE_LANGUAGES
188+
) -> None:
189+
if not USERINFO_FILE.exists():
190+
raise FileNotFoundError("User info file not found.")
191+
lang_key = language.strip().lower()
192+
if lang_key not in LEETCODE_LANGUAGES:
193+
raise ValueError(
194+
f"Unsupported language: {language}\n"
195+
f"Available options: {', '.join(sorted(set(LEETCODE_LANGUAGES.values())))}"
196+
)
197+
selected_lang = LEETCODE_LANGUAGES[lang_key]
198+
with open(USERINFO_FILE, "r") as f:
199+
data = json.load(f)
200+
data["language"] = selected_lang
201+
202+
with open(USERINFO_FILE, "w") as f:
203+
json.dump(data, f, indent=2)
204+
205+
return selected_lang
206+
207+
def _get_lang(
208+
self
209+
) -> str:
210+
try:
211+
with open(USERINFO_FILE, "r") as f:
212+
userinfo_dict = json.load(f)
213+
return userinfo_dict['language']
214+
except Exception as e:
215+
raise FileNotFoundError("User info file not found.")
216+
217+
def _get_user_progress(
218+
self,
219+
csrftoken,
220+
session,
221+
userinfo
222+
):
223+
try:
224+
result_table = []
225+
headers = _req_header(csrftoken)
226+
cookies = _req_cookies(
227+
session,
228+
csrftoken,
229+
)
230+
query = _req_user_progress_v2_query()
231+
variables = _req_user_progress_variable(userinfo[1])
232+
233+
response = requests.post(
234+
LEETCODE_URL,
235+
headers=headers,
236+
cookies=cookies,
237+
data=json.dumps({"query": query, "variables": variables})
238+
)
239+
240+
data = json.loads(response.content)
241+
progress_result = data["data"]["userProfileUserQuestionProgressV2"]
242+
243+
accepted = {x["difficulty"]: x["count"] for x in progress_result["numAcceptedQuestions"]}
244+
failed = {x["difficulty"]: x["count"] for x in progress_result["numFailedQuestions"]}
245+
untouched = {x["difficulty"]: x["count"] for x in progress_result["numUntouchedQuestions"]}
246+
beats = {x["difficulty"]: x["percentage"] for x in progress_result["userSessionBeatsPercentage"]}
247+
248+
for diff in ["EASY", "MEDIUM", "HARD"]:
249+
result_table.append([
250+
diff.capitalize(),
251+
accepted.get(diff, 0),
252+
failed.get(diff, 0),
253+
untouched.get(diff, 0),
254+
f"{beats.get(diff):.2f}" if beats.get(diff) is not None else "-"
255+
])
256+
return tabulate(result_table, headers=["Difficulty", "Accepted", "Failed", "Untouched", "Beats (%)"], tablefmt="fancy_grid")
257+
258+
except Exception as e:
259+
raise e

0 commit comments

Comments
 (0)