Skip to content

Commit acb4fd3

Browse files
feat: logout, filters enabled listing
1 parent 8ced115 commit acb4fd3

File tree

8 files changed

+214
-53
lines changed

8 files changed

+214
-53
lines changed

src/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from .commands.show import show
2-
from .commands.details import details
3-
from .server.api import fetch_problem_list
2+
from .commands.profile import profile
43
__version__ = "0.1.0"
54

6-
__all__ = ["show", "details", "fetch_problem_list"]
5+
__all__ = ["show", "profile"]

src/commands/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .show import show
2-
from .details import details
2+
from .profile import profile
33

4-
__all__ = ["show", "details"]
4+
__all__ = ["show", "profile"]

src/commands/list_problems.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
2+
from rich.spinner import Spinner
3+
from rich.live import Live
4+
from typing import Optional
5+
from src.lib.ui import display_problem_list
6+
from src.server.auth import Auth
7+
8+
import typer
9+
10+
from src.server.api import fetch_problem_list
11+
12+
status_map = {
13+
"attempted": "TRIED",
14+
"solved": "AC",
15+
"todo": "NOT_STARTED",
16+
}
17+
18+
AuthManager = Auth()
19+
20+
def list_problems(
21+
difficulty: Optional[str] = typer.Option(None, "--difficulty", "-d", help="Filter by difficulty (easy/medium/hard)"),
22+
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status (todo/in-progress/done)"),
23+
tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tags (comma-separated)"),
24+
category_slug: Optional[str] = typer.Option("all-code-essentials", "--category-slug", "-c", help="Filter by category slug")
25+
):
26+
"""List available LeetCode problems with optional filters."""
27+
if (difficulty is not None or status is not None or tag is not None):
28+
if not AuthManager.is_authenticated:
29+
typer.echo(typer.style("❌ Please login first to enable problem listing with filters", fg=typer.colors.RED))
30+
raise typer.Exit(1)
31+
32+
tags = tag.split(',') if tag else []
33+
34+
with Live(Spinner('dots'), refresh_per_second=10) as live:
35+
live.console.print("[cyan]Fetching problems...")
36+
37+
session = AuthManager.session_manager.load_session()
38+
if not session:
39+
typer.echo(typer.style("❌ No valid session found. Please login first.", fg=typer.colors.RED))
40+
raise typer.Exit(1)
41+
42+
data = fetch_problem_list(
43+
csrf_token=session["csrftoken"],
44+
session_id=session["session_token"],
45+
categorySlug=category_slug or "all-code-essentials",
46+
filters={
47+
"difficulty": difficulty,
48+
"status": status_map.get(status) if status is not None else None,
49+
"tags": tags
50+
}
51+
)
52+
display_problem_list(data)

src/commands/login.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,12 @@ def login():
3131
typer.echo(typer.style(f"\n✓ Successfully logged in as {result['user_name']}!", fg=typer.colors.GREEN))
3232
else:
3333
typer.echo(typer.style(f"\n✗ Login failed: {result['message']}", fg=typer.colors.RED))
34+
35+
def logout():
36+
"""Logout from LeetCode"""
37+
if not auth_manager.is_authenticated:
38+
typer.echo(typer.style("❌ You are not logged in", fg=typer.colors.RED))
39+
return
40+
41+
auth_manager.session_manager.clear_session()
42+
typer.echo(typer.style("✓ Successfully logged out", fg=typer.colors.GREEN))
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from server.api import fetch_problem_list
1+
from server.api import fetch_user_data
22
from lib.ui import display_problem_stats
33
from rich.spinner import Spinner
44
from rich.live import Live
55

6-
def details():
6+
def profile():
77
"""List all available LeetCode problems."""
88
spinner = Spinner('dots')
99
with Live(spinner, refresh_per_second=10, transient=True) as live:
10-
live.console.print("[cyan]Fetching problem list...")
11-
data = fetch_problem_list()
10+
live.console.print("[cyan]Fetching user profile...")
11+
data = fetch_user_data()
1212
display_problem_stats(data)

src/lib/ui.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,36 @@ def display_problem_stats(data):
101101
else:
102102
console.print(Panel("No recent activity", border_style="cyan"))
103103

104+
console.print("\n")
105+
106+
def display_problem_list(data):
107+
console.clear()
108+
console.print("\n")
109+
110+
table = Table(
111+
title="Problem List",
112+
box=box.ROUNDED,
113+
border_style="cyan",
114+
pad_edge=False,
115+
show_edge=True
116+
)
117+
118+
table.add_column("ID", style="dim", width=6)
119+
table.add_column("Title", style="cyan")
120+
table.add_column("Difficulty", justify="center", width=10)
121+
table.add_column("Status", justify="center", width=8)
122+
table.add_column("AC Rate", justify="right", width=8)
123+
124+
for problem in data['problemsetQuestionList']['questions']:
125+
status_icon = "[green]✓" if problem['status'] == "ac" else "[red]✗"
126+
ac_rate = f"{problem['acRate']:.1f}%"
127+
table.add_row(
128+
problem['frontendQuestionId'],
129+
problem['title'],
130+
problem['difficulty'],
131+
status_icon,
132+
ac_rate
133+
)
134+
135+
console.print(table)
104136
console.print("\n")

src/main.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import profile
12
import typer
2-
from src import show, details
3-
from src.commands.login import login
3+
from src import show, profile
4+
from src.commands.list_problems import list_problems
5+
from src.commands.login import login, logout
46
from src.commands.submit import submit
57
from src.commands.test import test
68

79
app = typer.Typer()
810

911
app.command(name="show")(show)
10-
app.command(name="user-details")(details)
12+
app.command(name="list")(list_problems)
13+
app.command(name="profile")(profile)
1114
app.command(name="login")(login)
15+
app.command(name="logout")(logout)
1216
app.command(name="submit")(submit)
1317
app.command(name="test")(test)
1418

src/server/api.py

Lines changed: 106 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,109 @@
11
from gql import gql, Client
22
from gql.transport.requests import RequestsHTTPTransport
33

4-
transport = RequestsHTTPTransport(
5-
url='https://leetcode.com/graphql',
6-
headers={'Content-Type': 'application/json'},
7-
)
8-
9-
client = Client(
10-
transport=transport,
11-
fetch_schema_from_transport=False
12-
)
13-
14-
def fetch_problem_list(username: str = "yuvrajsinh5252"):
15-
variables = {"username": username}
16-
query = gql(
17-
"""
18-
query userProblemsSolved($username: String!) {
19-
allQuestionsCount {
20-
difficulty
21-
count
22-
}
23-
matchedUser(username: $username) {
24-
problemsSolvedBeatsStats {
25-
difficulty
26-
percentage
27-
}
28-
submitStatsGlobal {
29-
acSubmissionNum {
30-
difficulty
31-
count
32-
}
33-
}
34-
}
35-
}
36-
"""
37-
)
38-
39-
try:
40-
result = client.execute(query, variable_values=variables)
41-
return result
42-
except Exception as e:
43-
print(f"Error fetching data: {str(e)}")
44-
return None
4+
def create_leetcode_client(csrf_token: str, session_id: str):
5+
headers = {
6+
'Content-Type': 'application/json',
7+
'x-csrftoken': csrf_token,
8+
'cookie': f'csrftoken={csrf_token}; LEETCODE_SESSION={session_id}',
9+
'referer': 'https://leetcode.com',
10+
}
11+
12+
transport = RequestsHTTPTransport(
13+
url='https://leetcode.com/graphql',
14+
headers=headers,
15+
)
16+
17+
return Client(
18+
transport=transport,
19+
fetch_schema_from_transport=False
20+
)
21+
22+
def fetch_user_data(username: str = "yuvrajsinh5252"):
23+
client = create_leetcode_client("csrf_token", "session_id")
24+
variables = {"username": username}
25+
query = gql(
26+
"""
27+
query userProblemsSolved($username: String!) {
28+
allQuestionsCount {
29+
difficulty
30+
count
31+
}
32+
matchedUser(username: $username) {
33+
problemsSolvedBeatsStats {
34+
difficulty
35+
percentage
36+
}
37+
submitStatsGlobal {
38+
acSubmissionNum {
39+
difficulty
40+
count
41+
}
42+
}
43+
}
44+
}
45+
"""
46+
)
47+
48+
try:
49+
result = client.execute(query, variable_values=variables)
50+
return result
51+
except Exception as e:
52+
print(f"Error fetching data: {str(e)}")
53+
return None
54+
55+
def fetch_problem_list(
56+
csrf_token: str,
57+
session_id: str,
58+
categorySlug: str,
59+
limit: int = 20,
60+
skip: int = 0,
61+
filters: dict = {}
62+
):
63+
client = create_leetcode_client(csrf_token, session_id)
64+
variables = {
65+
"categorySlug": categorySlug,
66+
"limit": limit,
67+
"skip": skip,
68+
"filters": filters
69+
}
70+
71+
query = gql(
72+
"""
73+
query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
74+
problemsetQuestionList: questionList(
75+
categorySlug: $categorySlug
76+
limit: $limit
77+
skip: $skip
78+
filters: $filters
79+
) {
80+
total: totalNum
81+
questions: data {
82+
acRate
83+
difficulty
84+
freqBar
85+
frontendQuestionId: questionFrontendId
86+
isFavor
87+
paidOnly: isPaidOnly
88+
status
89+
title
90+
titleSlug
91+
topicTags {
92+
name
93+
id
94+
slug
95+
}
96+
hasSolution
97+
hasVideoSolution
98+
}
99+
}
100+
}
101+
"""
102+
)
103+
104+
try:
105+
result = client.execute(query, variable_values=variables)
106+
return result
107+
except Exception as e:
108+
print(f"Error fetching data: {str(e)}")
109+
return None

0 commit comments

Comments
 (0)