Skip to content

Commit dd0cc88

Browse files
feat: add submit/test command and UI
1 parent 26595f0 commit dd0cc88

File tree

8 files changed

+527
-11
lines changed

8 files changed

+527
-11
lines changed

solution.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from typing import List
2+
3+
class Solution:
4+
def twoSum(self, nums: List[int], target: int) -> List[int]:
5+
n = len(nums)
6+
for i in range(n - 1):
7+
for j in range(i + 1, n):
8+
if nums[i] + nums[j] == target:
9+
return [i, j]
10+
return []

src/commands/show.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
import typer
2+
from rich.console import Console
3+
from src.lib.problem_ui import ProblemDetails
4+
from src.server.auth import Auth
5+
from src.server.solution_manager import SolutionManager
26

3-
def show(problem_id: int):
4-
"""Show details of a specific problem."""
5-
typer.echo(f"Fetching details for problem {problem_id}...")
7+
auth_manager = Auth()
8+
solution_manager = SolutionManager(auth_manager.get_session())
9+
console = Console()
10+
11+
def show(problem: str = typer.Argument(..., help="Problem slug (e.g., 'two-sum')")):
12+
"""Show problem details including description and test cases"""
13+
if not auth_manager.is_authenticated:
14+
typer.echo(typer.style("❌ Please login first using the login command", fg=typer.colors.RED))
15+
raise typer.Exit(1)
16+
17+
data = solution_manager.get_question_data(problem)
18+
if not data.get('data', {}).get('question'):
19+
typer.echo(typer.style(f"❌ Problem '{problem}' not found", fg=typer.colors.RED))
20+
raise typer.Exit(1)
21+
22+
23+
ProblemDetails(data.get('data', {}).get('question')).display()

src/commands/submit.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import typer
2+
from pathlib import Path
3+
from src.server.auth import Auth
4+
from src.server.solution_manager import SolutionManager
5+
6+
auth_manager = Auth()
7+
solution_manager = SolutionManager(auth_manager.get_session())
8+
9+
def submit(
10+
problem: str = typer.Argument(..., help="Problem slug (e.g., 'two-sum')"),
11+
file: Path = typer.Argument(..., help="Path to solution file"),
12+
lang: str = typer.Option("python3", help="Programming language")
13+
):
14+
"""Submit a solution to LeetCode"""
15+
if not auth_manager.is_authenticated:
16+
typer.echo(typer.style("❌ Please login first using the login command", fg=typer.colors.RED))
17+
raise typer.Exit(1)
18+
19+
if not file.exists():
20+
typer.echo(typer.style(f"❌ File not found: {file}", fg=typer.colors.RED))
21+
raise typer.Exit(1)
22+
23+
with open(file, 'r') as f:
24+
code = f.read()
25+
26+
typer.echo(typer.style("📤 Submitting solution...", fg=typer.colors.YELLOW))
27+
result = solution_manager.submit_solution(problem, code, lang)
28+
29+
if result["success"]:
30+
status_color = typer.colors.GREEN if result["status"] == "Accepted" else typer.colors.RED
31+
typer.echo(typer.style(f"\n✨ Status: {result['status']}", fg=status_color))
32+
typer.echo(f"⏱️ Runtime: {result['runtime']}")
33+
typer.echo(f"💾 Memory: {result['memory']}")
34+
typer.echo(f"✅ Passed: {result['passed_testcases']}/{result['total_testcases']} test cases")
35+
else:
36+
typer.echo(typer.style(f"\n❌ Submission failed: {result['error']}", fg=typer.colors.RED))

src/commands/test.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import typer
2+
from pathlib import Path
3+
from src.server.auth import Auth
4+
from src.server.solution_manager import SolutionManager
5+
6+
auth_manager = Auth()
7+
solution_manager = SolutionManager(auth_manager.get_session())
8+
9+
def test(
10+
problem: str = typer.Argument(..., help="Problem slug (e.g., 'two-sum')"),
11+
file: Path = typer.Argument(..., help="Path to solution file"),
12+
lang: str = typer.Option("python3", help="Programming language")
13+
):
14+
"""Test a solution with LeetCode's test cases"""
15+
if not auth_manager.is_authenticated:
16+
typer.echo(typer.style("❌ Please login first using the login command", fg=typer.colors.RED))
17+
raise typer.Exit(1)
18+
19+
if not file.exists():
20+
typer.echo(typer.style(f"❌ File not found: {file}", fg=typer.colors.RED))
21+
raise typer.Exit(1)
22+
23+
with open(file, 'r') as f:
24+
code = f.read()
25+
26+
typer.echo(typer.style("🧪 Testing solution with LeetCode test cases...", fg=typer.colors.YELLOW))
27+
result = solution_manager.test_solution(problem, code, lang)
28+
29+
if result["success"]:
30+
status_color = typer.colors.GREEN if result["status"] == "Accepted" else typer.colors.RED
31+
typer.echo(typer.style(f"\n✨ Status: {result['status']}", fg=status_color))
32+
if "runtime" in result:
33+
typer.echo(f"⏱️ Runtime: {result['runtime']}")
34+
if "memory" in result:
35+
typer.echo(f"💾 Memory: {result['memory']}")
36+
typer.echo("\nTest Case Results:")
37+
typer.echo(f"📥 Input: {result['input']}")
38+
typer.echo(f"📤 Your Output: {result['output']}")
39+
typer.echo(f"✅ Expected: {result['expected']}")
40+
else:
41+
typer.echo(typer.style(f"\n{result['error']}", fg=typer.colors.RED))

src/lib/problem_ui.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from rich.console import Console
2+
from rich.panel import Panel
3+
from rich.markdown import Markdown
4+
from rich.layout import Layout
5+
from rich.box import ROUNDED
6+
from rich.text import Text
7+
from rich.align import Align
8+
from bs4 import BeautifulSoup
9+
import json
10+
from dataclasses import dataclass
11+
from typing import List, Dict, Optional
12+
13+
console = Console()
14+
15+
@dataclass
16+
class FunctionMetadata:
17+
name: str
18+
params: List[Dict[str, str]]
19+
return_type: str
20+
21+
class ProblemDetails:
22+
def __init__(self, problem_data: dict):
23+
self.question_id: int = problem_data['questionId']
24+
self.title: str = problem_data['title']
25+
self.difficulty: str = problem_data['difficulty']
26+
self.content: str = problem_data['content']
27+
self.test_cases: List[str] = problem_data['exampleTestcases'].split('\n')
28+
self.code_snippets: List[Dict] = problem_data['codeSnippets']
29+
30+
# Parse metadata
31+
self.function_metadata: Optional[FunctionMetadata] = None
32+
try:
33+
metadata = json.loads(problem_data['metaData'])
34+
self.function_metadata = FunctionMetadata(
35+
name=metadata.get('name', 'solution'),
36+
params=metadata.get('params', []),
37+
return_type=metadata.get('return', {}).get('type', 'void')
38+
)
39+
except Exception:
40+
pass
41+
42+
self._parse_content()
43+
self.console_width = console.width
44+
45+
def _parse_content(self):
46+
"""Parse the HTML content into different sections"""
47+
soup = BeautifulSoup(self.content, 'html.parser')
48+
49+
for code in soup.find_all('code'):
50+
code.replace_with(soup.new_string(f'`{code.text}`'))
51+
52+
self.description: List[str] = []
53+
self.examples: List[str] = []
54+
self.constraints: List[str] = []
55+
56+
current_section = self.description
57+
58+
for p in soup.find_all('p'):
59+
text = p.text.strip()
60+
if not text:
61+
continue
62+
63+
if 'Example' in text:
64+
current_section = self.examples
65+
current_section.append('\n' + text)
66+
elif 'Constraints:' in text:
67+
current_section = self.constraints
68+
current_section.append('\n' + text)
69+
elif any(marker in text for marker in ['Input:', 'Output:', 'Explanation:']):
70+
current_section.append(' ' + text)
71+
elif current_section == self.examples and not text.startswith('Example'):
72+
current_section.append(' ' + text)
73+
else:
74+
current_section.append(text)
75+
76+
@property
77+
def formatted_description(self) -> str:
78+
"""Get the formatted problem description"""
79+
sections = [
80+
' '.join(self.description),
81+
*self.examples,
82+
*self.constraints
83+
]
84+
return '\n'.join(line for line in sections if line.strip())
85+
86+
@property
87+
def available_languages(self) -> List[str]:
88+
"""Get list of available programming languages"""
89+
return [snippet['lang'] for snippet in self.code_snippets]
90+
91+
@property
92+
def formatted_function_signature(self) -> str:
93+
"""Get the formatted function signature"""
94+
if not self.function_metadata:
95+
return "[red]Error loading function signature[/]"
96+
97+
param_str = ', '.join(
98+
f"{p['name']}: {p['type']}"
99+
for p in self.function_metadata.params
100+
)
101+
102+
return (
103+
"[bold blue]Function Signature:[/]\n"
104+
f"[bold cyan]def[/] [yellow]{self.function_metadata.name}[/](\n"
105+
f" [green]{param_str}[/]\n"
106+
f") -> [green]{self.function_metadata.return_type}[/]"
107+
)
108+
109+
def get_code_snippet(self, language: str) -> Optional[str]:
110+
"""Get code snippet for specific language"""
111+
for snippet in self.code_snippets:
112+
if snippet['lang'].lower() == language.lower():
113+
return snippet['code']
114+
return None
115+
116+
def format_test_case(self, input_str: str, expected_str: str, case_num: int) -> str:
117+
return (
118+
f"[bold blue]Ex {case_num}:[/] "
119+
f"[bold cyan]In:[/] {input_str} → "
120+
f"[bold green]Out:[/] {expected_str}"
121+
)
122+
123+
def create_header(self):
124+
difficulty_colors = {
125+
'Easy': 'bright_green',
126+
'Medium': 'yellow',
127+
'Hard': 'red'
128+
}
129+
difficulty_color = difficulty_colors.get(self.difficulty, 'white')
130+
131+
header = Text()
132+
header.append("🔹 ", style="blue")
133+
header.append(f"{self.question_id}. {self.title}", style="bold cyan")
134+
header.append(" • ", style="dim")
135+
header.append(self.difficulty, style=difficulty_color)
136+
137+
return Align.center(header)
138+
139+
def display(self):
140+
console.clear()
141+
142+
console.print(self.create_header())
143+
console.print()
144+
145+
layout = Layout()
146+
layout.split_row(
147+
Layout(name="left_section"),
148+
Layout(name="sidebar", size=self.console_width // 4)
149+
)
150+
151+
layout["left_section"].split_column(
152+
Layout(name="description", ratio=85),
153+
Layout(name="bottom", ratio=15)
154+
)
155+
156+
# Update description panel
157+
layout["description"].update(Panel(
158+
Markdown(self.formatted_description, justify="left", style="white"),
159+
box=ROUNDED,
160+
title="[bold blue]Description",
161+
border_style="blue",
162+
padding=(1, 2),
163+
expand=True
164+
))
165+
166+
# Update sidebar panel
167+
sidebar_content = (
168+
f"{self.formatted_function_signature}\n\n"
169+
"[bold blue]Available Languages:[/]\n"
170+
f"[green]{self.available_languages}[/]"
171+
)
172+
173+
layout["sidebar"].update(Panel(
174+
sidebar_content,
175+
box=ROUNDED,
176+
border_style="blue",
177+
padding=(1, 2),
178+
expand=True
179+
))
180+
181+
# Update test cases panel
182+
test_cases_content = []
183+
184+
for i in range(0, len(self.test_cases), 2):
185+
if i + 1 < len(self.test_cases):
186+
case_num = i // 2 + 1
187+
test_case = self.format_test_case(
188+
self.test_cases[i],
189+
self.test_cases[i+1],
190+
case_num
191+
)
192+
test_cases_content.append(test_case)
193+
194+
layout["bottom"].update(Panel(
195+
"\n\n".join(test_cases_content),
196+
box=ROUNDED,
197+
title="[bold blue]Sample Test Cases",
198+
border_style="blue",
199+
padding=(1, 2),
200+
))
201+
202+
console.print(layout)

src/lib/ui.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,10 @@ def create_user_stats_table(data):
3434
return table
3535

3636
def display_problem_stats(data):
37-
"""Display problem statistics in a formatted way."""
37+
problems_table = create_problems_table(data)
38+
console.print(problems_table)
3839

39-
problems_table = create_problems_table(data)
40-
console.print(problems_table)
41-
42-
user_stats = create_user_stats_table(data)
43-
if user_stats:
44-
console.print("\n")
45-
console.print(user_stats)
40+
user_stats = create_user_stats_table(data)
41+
if user_stats:
42+
console.print("\n")
43+
console.print(user_stats)

src/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import typer
22
from src import show, details
33
from src.commands.login import login
4+
from src.commands.submit import submit
5+
from src.commands.test import test
46

57
app = typer.Typer()
68

79
app.command(name="show")(show)
810
app.command(name="user-details")(details)
911
app.command(name="login")(login)
12+
app.command(name="submit")(submit)
13+
app.command(name="test")(test)
1014

1115
if __name__ == "__main__":
1216
app()

0 commit comments

Comments
 (0)