dsap-cli 0.1.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.
- dsap/__init__.py +21 -0
- dsap/__main__.py +9 -0
- dsap/cli.py +618 -0
- dsap/config.py +158 -0
- dsap/data/blind75.yaml +405 -0
- dsap/data/grind75.yaml +401 -0
- dsap/data/neetcode150.yaml +796 -0
- dsap/database.py +774 -0
- dsap/models.py +225 -0
- dsap/problem_sets.py +195 -0
- dsap/py.typed +0 -0
- dsap/sm2.py +521 -0
- dsap/ui.py +445 -0
- dsap_cli-0.1.0.dist-info/METADATA +287 -0
- dsap_cli-0.1.0.dist-info/RECORD +18 -0
- dsap_cli-0.1.0.dist-info/WHEEL +4 -0
- dsap_cli-0.1.0.dist-info/entry_points.txt +2 -0
- dsap_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
dsap/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""DSAP - DSA Practice with Spaced Repetition.
|
|
2
|
+
|
|
3
|
+
A CLI tool for mastering Data Structures and Algorithms using the
|
|
4
|
+
scientifically-proven SM-2 spaced repetition algorithm.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
dsap review Start a review session with due problems
|
|
8
|
+
dsap next Get next recommended problem
|
|
9
|
+
dsap list List problems with filters
|
|
10
|
+
dsap stats Show your statistics
|
|
11
|
+
dsap load Load a curated problem set
|
|
12
|
+
dsap add Add a custom problem
|
|
13
|
+
dsap config Manage settings
|
|
14
|
+
|
|
15
|
+
Quick Start:
|
|
16
|
+
$ dsap load blind75
|
|
17
|
+
$ dsap review
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__author__ = "Juma R. Paul"
|
dsap/__main__.py
ADDED
dsap/cli.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""DSAP CLI - DSA Practice with Spaced Repetition.
|
|
2
|
+
|
|
3
|
+
Main command-line interface using Click.
|
|
4
|
+
|
|
5
|
+
Commands:
|
|
6
|
+
dsap review - Start a review session with due problems
|
|
7
|
+
dsap next - Get next recommended problem
|
|
8
|
+
dsap list - List problems with filters
|
|
9
|
+
dsap stats - Show statistics and progress
|
|
10
|
+
dsap load - Load a curated problem set
|
|
11
|
+
dsap add - Add a custom problem
|
|
12
|
+
dsap config - Manage configuration
|
|
13
|
+
dsap reset - Reset problems and/or progress
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from dsap import __version__
|
|
20
|
+
from dsap.config import get_config
|
|
21
|
+
from dsap.database import Database
|
|
22
|
+
from dsap.models import Difficulty, Problem
|
|
23
|
+
from dsap.problem_sets import list_bundled_sets, load_problem_set
|
|
24
|
+
from dsap.sm2 import SM2State, process_review
|
|
25
|
+
from dsap.ui import (
|
|
26
|
+
console,
|
|
27
|
+
display_error,
|
|
28
|
+
display_info,
|
|
29
|
+
display_problem,
|
|
30
|
+
display_problem_list,
|
|
31
|
+
display_review_feedback,
|
|
32
|
+
display_session_summary,
|
|
33
|
+
display_stats,
|
|
34
|
+
display_success,
|
|
35
|
+
display_warning,
|
|
36
|
+
display_welcome,
|
|
37
|
+
prompt_open_browser,
|
|
38
|
+
prompt_quality_rating,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def normalize_set_name(name: str | None) -> str | None:
|
|
43
|
+
"""Normalize problem set names to their canonical form."""
|
|
44
|
+
if name is None:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
name_lower = name.lower().replace(" ", "").replace("_", "")
|
|
48
|
+
|
|
49
|
+
mapping = {
|
|
50
|
+
"blind75": "Blind 75",
|
|
51
|
+
"neetcode150": "NeetCode 150",
|
|
52
|
+
"grind75": "Grind 75",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return mapping.get(name_lower, name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@click.group(invoke_without_command=True)
|
|
59
|
+
@click.version_option(version=__version__, prog_name="dsap")
|
|
60
|
+
@click.pass_context
|
|
61
|
+
def cli(ctx: click.Context) -> None:
|
|
62
|
+
r"""DSAP - DSA Practice with Spaced Repetition.
|
|
63
|
+
|
|
64
|
+
Master data structures and algorithms with scientifically-proven
|
|
65
|
+
spaced repetition (SM-2 algorithm).
|
|
66
|
+
|
|
67
|
+
\b
|
|
68
|
+
Quick Start:
|
|
69
|
+
dsap load blind75 Load the Blind 75 problems
|
|
70
|
+
dsap review Start practicing
|
|
71
|
+
dsap stats Check your progress
|
|
72
|
+
|
|
73
|
+
\b
|
|
74
|
+
Examples:
|
|
75
|
+
dsap review --limit 5 Review up to 5 problems
|
|
76
|
+
dsap next --difficulty Easy Get an easy problem
|
|
77
|
+
dsap list --due Show problems due today
|
|
78
|
+
"""
|
|
79
|
+
if ctx.invoked_subcommand is None:
|
|
80
|
+
display_welcome()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@cli.command()
|
|
84
|
+
@click.option(
|
|
85
|
+
"--limit", "-n", default=10, help="Maximum problems to review (default: 10)"
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--difficulty",
|
|
89
|
+
"-d",
|
|
90
|
+
type=click.Choice(["Easy", "Medium", "Hard"], case_sensitive=False),
|
|
91
|
+
help="Filter by difficulty",
|
|
92
|
+
)
|
|
93
|
+
@click.option("--category", "-c", help="Filter by category")
|
|
94
|
+
@click.option(
|
|
95
|
+
"--set",
|
|
96
|
+
"-s",
|
|
97
|
+
"problem_set",
|
|
98
|
+
help="Filter by problem set (e.g., blind75, neetcode150)",
|
|
99
|
+
)
|
|
100
|
+
def review(limit: int, difficulty: str, category: str, problem_set: str) -> None:
|
|
101
|
+
r"""Start a review session with due problems.
|
|
102
|
+
|
|
103
|
+
Reviews problems that are due based on SM-2 scheduling.
|
|
104
|
+
After solving each problem, rate your recall quality (0-5).
|
|
105
|
+
|
|
106
|
+
\b
|
|
107
|
+
Example:
|
|
108
|
+
dsap review Review up to 10 due problems
|
|
109
|
+
dsap review -n 5 Review up to 5 problems
|
|
110
|
+
dsap review -d Medium Review only medium difficulty
|
|
111
|
+
dsap review -s blind75 Review only Blind 75 problems
|
|
112
|
+
"""
|
|
113
|
+
db = Database()
|
|
114
|
+
config = get_config()
|
|
115
|
+
|
|
116
|
+
# Use config's preferred_set if not specified
|
|
117
|
+
if not problem_set:
|
|
118
|
+
problem_set = config.get("preferred_set")
|
|
119
|
+
|
|
120
|
+
# Normalize the set name
|
|
121
|
+
problem_set = normalize_set_name(problem_set)
|
|
122
|
+
|
|
123
|
+
# Get due problems
|
|
124
|
+
due_problems = db.get_due_problems(
|
|
125
|
+
limit=limit,
|
|
126
|
+
difficulty=difficulty,
|
|
127
|
+
category=category,
|
|
128
|
+
problem_set=problem_set,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not due_problems:
|
|
132
|
+
# Check if there are new problems to start
|
|
133
|
+
new_problems = db.get_new_problems(limit=limit, problem_set=problem_set)
|
|
134
|
+
if new_problems:
|
|
135
|
+
console.print(
|
|
136
|
+
f"[green]No problems due![/green] "
|
|
137
|
+
f"You have {len(new_problems)} new problems to start."
|
|
138
|
+
)
|
|
139
|
+
console.print("Run [bold]dsap next[/bold] to get a new problem.")
|
|
140
|
+
else:
|
|
141
|
+
console.print(
|
|
142
|
+
"[green]No problems due for review![/green] "
|
|
143
|
+
"Great job keeping up with your practice."
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
console.print(
|
|
148
|
+
f"[bold]Starting review session: {len(due_problems)} problems due[/bold]"
|
|
149
|
+
)
|
|
150
|
+
console.print()
|
|
151
|
+
|
|
152
|
+
qualities: list[int] = []
|
|
153
|
+
reviewed = 0
|
|
154
|
+
|
|
155
|
+
for i, (problem, progress) in enumerate(due_problems, 1):
|
|
156
|
+
# Display problem
|
|
157
|
+
display_problem(
|
|
158
|
+
problem,
|
|
159
|
+
progress=progress,
|
|
160
|
+
index=i,
|
|
161
|
+
total=len(due_problems),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Prompt to open browser
|
|
165
|
+
auto_open = config.get("auto_open_browser")
|
|
166
|
+
prompt_open_browser(str(problem.url), auto_open=auto_open)
|
|
167
|
+
|
|
168
|
+
# Get quality rating
|
|
169
|
+
quality = prompt_quality_rating()
|
|
170
|
+
|
|
171
|
+
if quality is None:
|
|
172
|
+
# User wants to quit
|
|
173
|
+
console.print()
|
|
174
|
+
display_warning("Session paused. Progress saved.")
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
# Update SM-2 state
|
|
178
|
+
current_state = SM2State(
|
|
179
|
+
easiness_factor=progress.easiness_factor,
|
|
180
|
+
interval=progress.interval,
|
|
181
|
+
repetitions=progress.repetitions,
|
|
182
|
+
)
|
|
183
|
+
new_state = process_review(current_state, quality)
|
|
184
|
+
|
|
185
|
+
# Save to database
|
|
186
|
+
db.update_progress(problem.id, new_state, quality)
|
|
187
|
+
|
|
188
|
+
# Show feedback
|
|
189
|
+
display_review_feedback(quality, new_state.interval)
|
|
190
|
+
|
|
191
|
+
qualities.append(quality)
|
|
192
|
+
reviewed += 1
|
|
193
|
+
|
|
194
|
+
# Show session summary
|
|
195
|
+
display_session_summary(reviewed, len(due_problems), qualities)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@cli.command()
|
|
199
|
+
@click.option(
|
|
200
|
+
"--difficulty",
|
|
201
|
+
"-d",
|
|
202
|
+
type=click.Choice(["Easy", "Medium", "Hard"], case_sensitive=False),
|
|
203
|
+
help="Filter by difficulty",
|
|
204
|
+
)
|
|
205
|
+
@click.option("--category", "-c", help="Filter by category")
|
|
206
|
+
@click.option(
|
|
207
|
+
"--set",
|
|
208
|
+
"-s",
|
|
209
|
+
"problem_set",
|
|
210
|
+
help="Filter by problem set (e.g., blind75, neetcode150)",
|
|
211
|
+
)
|
|
212
|
+
@click.option(
|
|
213
|
+
"--new-only", "-n", is_flag=True, help="Only show problems never attempted"
|
|
214
|
+
)
|
|
215
|
+
def next(difficulty: str, category: str, problem_set: str, new_only: bool) -> None:
|
|
216
|
+
r"""Get the next recommended problem to practice.
|
|
217
|
+
|
|
218
|
+
Recommends based on:
|
|
219
|
+
1. Problems due for review (highest priority)
|
|
220
|
+
2. New problems you haven't tried yet
|
|
221
|
+
3. Problems with low easiness factors (harder for you)
|
|
222
|
+
|
|
223
|
+
\b
|
|
224
|
+
Example:
|
|
225
|
+
dsap next Get next recommended problem
|
|
226
|
+
dsap next -d Easy Get an easy problem
|
|
227
|
+
dsap next --new-only Get a new problem only
|
|
228
|
+
dsap next -s blind75 Get from Blind 75 only
|
|
229
|
+
"""
|
|
230
|
+
db = Database()
|
|
231
|
+
config = get_config()
|
|
232
|
+
|
|
233
|
+
# Use config's preferred_set if not specified
|
|
234
|
+
if not problem_set:
|
|
235
|
+
problem_set = config.get("preferred_set")
|
|
236
|
+
|
|
237
|
+
# Normalize the set name
|
|
238
|
+
problem_set = normalize_set_name(problem_set)
|
|
239
|
+
|
|
240
|
+
result = db.get_next_recommendation(
|
|
241
|
+
difficulty=difficulty,
|
|
242
|
+
category=category,
|
|
243
|
+
problem_set=problem_set,
|
|
244
|
+
new_only=new_only,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if not result:
|
|
248
|
+
if difficulty or category:
|
|
249
|
+
display_warning("No problems found matching your filters.")
|
|
250
|
+
else:
|
|
251
|
+
display_info("No problems available. Try loading a problem set:")
|
|
252
|
+
console.print(" dsap load blind75")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
problem, progress = result
|
|
256
|
+
|
|
257
|
+
# Ensure progress record exists
|
|
258
|
+
if progress is None:
|
|
259
|
+
db.ensure_progress_exists(problem.id)
|
|
260
|
+
|
|
261
|
+
# Display the problem
|
|
262
|
+
display_problem(
|
|
263
|
+
problem,
|
|
264
|
+
progress=progress,
|
|
265
|
+
show_hints=config.get("show_hints"),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Prompt to open and optionally rate
|
|
269
|
+
auto_open = config.get("auto_open_browser")
|
|
270
|
+
opened = prompt_open_browser(str(problem.url), auto_open=auto_open)
|
|
271
|
+
|
|
272
|
+
if opened:
|
|
273
|
+
console.print()
|
|
274
|
+
rate_now = click.confirm("Rate this problem now?", default=True)
|
|
275
|
+
|
|
276
|
+
if rate_now:
|
|
277
|
+
quality = prompt_quality_rating()
|
|
278
|
+
|
|
279
|
+
if quality is not None:
|
|
280
|
+
current_state = SM2State(
|
|
281
|
+
easiness_factor=progress.easiness_factor if progress else 2.5,
|
|
282
|
+
interval=progress.interval if progress else 0,
|
|
283
|
+
repetitions=progress.repetitions if progress else 0,
|
|
284
|
+
)
|
|
285
|
+
new_state = process_review(current_state, quality)
|
|
286
|
+
db.update_progress(problem.id, new_state, quality)
|
|
287
|
+
display_review_feedback(quality, new_state.interval)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@cli.command("list")
|
|
291
|
+
@click.option(
|
|
292
|
+
"--difficulty",
|
|
293
|
+
"-d",
|
|
294
|
+
type=click.Choice(["Easy", "Medium", "Hard"], case_sensitive=False),
|
|
295
|
+
help="Filter by difficulty",
|
|
296
|
+
)
|
|
297
|
+
@click.option("--category", "-c", help="Filter by category")
|
|
298
|
+
@click.option("--set", "problem_set", help="Filter by problem set (e.g., blind75)")
|
|
299
|
+
@click.option("--due", is_flag=True, help="Show only problems due for review")
|
|
300
|
+
@click.option(
|
|
301
|
+
"--limit", "-n", default=200, help="Maximum problems to show (default: 200)"
|
|
302
|
+
)
|
|
303
|
+
def list_problems(
|
|
304
|
+
difficulty: str,
|
|
305
|
+
category: str,
|
|
306
|
+
problem_set: str,
|
|
307
|
+
due: bool,
|
|
308
|
+
limit: int,
|
|
309
|
+
) -> None:
|
|
310
|
+
r"""List all problems with optional filters.
|
|
311
|
+
|
|
312
|
+
\b
|
|
313
|
+
Example:
|
|
314
|
+
dsap list List all problems
|
|
315
|
+
dsap list --due Show only due problems
|
|
316
|
+
dsap list -d Hard Show only hard problems
|
|
317
|
+
dsap list --set blind75 Show Blind 75 problems
|
|
318
|
+
"""
|
|
319
|
+
db = Database()
|
|
320
|
+
|
|
321
|
+
# Normalize set name
|
|
322
|
+
problem_set = normalize_set_name(problem_set)
|
|
323
|
+
|
|
324
|
+
problems = db.get_problems(
|
|
325
|
+
difficulty=difficulty,
|
|
326
|
+
category=category,
|
|
327
|
+
problem_set=problem_set,
|
|
328
|
+
due_only=due,
|
|
329
|
+
limit=limit,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if not problems:
|
|
333
|
+
if difficulty or category or problem_set or due:
|
|
334
|
+
display_warning("No problems found matching your filters.")
|
|
335
|
+
else:
|
|
336
|
+
display_info("No problems loaded. Try:")
|
|
337
|
+
console.print(" dsap load blind75")
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
display_problem_list(problems)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@cli.command()
|
|
344
|
+
def stats() -> None:
|
|
345
|
+
"""Show your practice statistics and progress.
|
|
346
|
+
|
|
347
|
+
Displays:
|
|
348
|
+
- Total and solved problems
|
|
349
|
+
- Problems due today and this week
|
|
350
|
+
- Current and best streaks
|
|
351
|
+
- Difficulty breakdown with progress bars
|
|
352
|
+
"""
|
|
353
|
+
db = Database()
|
|
354
|
+
statistics = db.get_statistics()
|
|
355
|
+
|
|
356
|
+
if statistics.total_problems == 0:
|
|
357
|
+
display_info("No problems loaded yet. Try:")
|
|
358
|
+
console.print(" dsap load blind75")
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
display_stats(statistics)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@cli.command()
|
|
365
|
+
@click.argument("source", required=False)
|
|
366
|
+
@click.option("--list", "show_list", is_flag=True, help="List available problem sets")
|
|
367
|
+
def load(source: str, show_list: bool) -> None:
|
|
368
|
+
r"""Load a curated problem set.
|
|
369
|
+
|
|
370
|
+
\b
|
|
371
|
+
Available sets:
|
|
372
|
+
blind75 - Blind 75 essential problems
|
|
373
|
+
neetcode150 - NeetCode 150 extended set
|
|
374
|
+
grind75 - Grind 75 flexible plan
|
|
375
|
+
|
|
376
|
+
\b
|
|
377
|
+
Example:
|
|
378
|
+
dsap load blind75 Load Blind 75
|
|
379
|
+
dsap load --list Show available sets
|
|
380
|
+
dsap load ./custom.yaml Load custom YAML file
|
|
381
|
+
"""
|
|
382
|
+
if show_list:
|
|
383
|
+
console.print("[bold]Available Problem Sets:[/bold]")
|
|
384
|
+
console.print()
|
|
385
|
+
for name, description in list_bundled_sets().items():
|
|
386
|
+
console.print(f" [cyan]{name}[/cyan] - {description}")
|
|
387
|
+
console.print()
|
|
388
|
+
console.print("Usage: dsap load <set_name>")
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
if not source:
|
|
392
|
+
display_error("Please specify a problem set or use --list")
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
with console.status(f"Loading {source}..."):
|
|
397
|
+
problems = load_problem_set(source)
|
|
398
|
+
|
|
399
|
+
db = Database()
|
|
400
|
+
added = db.add_problems(problems)
|
|
401
|
+
|
|
402
|
+
display_success(
|
|
403
|
+
f"Loaded {added} new problems from '{source}' "
|
|
404
|
+
f"({len(problems)} total in set)"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if added < len(problems):
|
|
408
|
+
display_info(
|
|
409
|
+
f"{len(problems) - added} problems were already in your database"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
except FileNotFoundError as e:
|
|
413
|
+
display_error(str(e))
|
|
414
|
+
except ValueError as e:
|
|
415
|
+
display_error(str(e))
|
|
416
|
+
except yaml.YAMLError as e:
|
|
417
|
+
display_error(f"Failed to parse YAML file: {e}")
|
|
418
|
+
except OSError as e:
|
|
419
|
+
display_error(f"Failed to read file: {e}")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@cli.command()
|
|
423
|
+
@click.argument("title")
|
|
424
|
+
@click.argument("url")
|
|
425
|
+
@click.option(
|
|
426
|
+
"--difficulty",
|
|
427
|
+
"-d",
|
|
428
|
+
required=True,
|
|
429
|
+
type=click.Choice(["Easy", "Medium", "Hard"], case_sensitive=False),
|
|
430
|
+
help="Problem difficulty",
|
|
431
|
+
)
|
|
432
|
+
@click.option(
|
|
433
|
+
"--category",
|
|
434
|
+
"-c",
|
|
435
|
+
required=True,
|
|
436
|
+
help="Problem category (e.g., 'Arrays', 'Dynamic Programming')",
|
|
437
|
+
)
|
|
438
|
+
@click.option("--description", help="Brief problem description")
|
|
439
|
+
@click.option("--tags", help="Comma-separated tags")
|
|
440
|
+
def add(
|
|
441
|
+
title: str,
|
|
442
|
+
url: str,
|
|
443
|
+
difficulty: str,
|
|
444
|
+
category: str,
|
|
445
|
+
description: str,
|
|
446
|
+
tags: str,
|
|
447
|
+
) -> None:
|
|
448
|
+
r"""Add a custom problem.
|
|
449
|
+
|
|
450
|
+
\b
|
|
451
|
+
Example:
|
|
452
|
+
dsap add "Two Sum" "https://leetcode.com/problems/two-sum" \\
|
|
453
|
+
-d Easy -c "Arrays & Hashing"
|
|
454
|
+
"""
|
|
455
|
+
tag_list = [t.strip() for t in tags.split(",")] if tags else []
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
problem = Problem(
|
|
459
|
+
title=title,
|
|
460
|
+
url=url,
|
|
461
|
+
difficulty=Difficulty.from_string(difficulty),
|
|
462
|
+
category=category,
|
|
463
|
+
description=description or "",
|
|
464
|
+
tags=tag_list,
|
|
465
|
+
problem_set="custom",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
db = Database()
|
|
469
|
+
problem_id = db.add_problem(problem)
|
|
470
|
+
|
|
471
|
+
display_success(f"Added '{title}' with ID {problem_id}")
|
|
472
|
+
|
|
473
|
+
except (ValueError, TypeError) as e:
|
|
474
|
+
display_error(f"Failed to add problem: {e}")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@cli.command()
|
|
478
|
+
@click.argument("key", required=False)
|
|
479
|
+
@click.argument("value", required=False)
|
|
480
|
+
@click.option(
|
|
481
|
+
"--list", "show_list", is_flag=True, help="Show all configuration options"
|
|
482
|
+
)
|
|
483
|
+
@click.option("--reset", is_flag=True, help="Reset configuration to defaults")
|
|
484
|
+
def config(key: str, value: str, show_list: bool, reset: bool) -> None:
|
|
485
|
+
r"""View or set configuration options.
|
|
486
|
+
|
|
487
|
+
\b
|
|
488
|
+
Options:
|
|
489
|
+
daily_goal Number of problems per day (default: 5)
|
|
490
|
+
preferred_difficulty Preferred difficulty (Easy/Medium/Hard/None)
|
|
491
|
+
preferred_set Default problem set (blind75/neetcode150/grind75/None)
|
|
492
|
+
show_hints Show hints for problems (true/false)
|
|
493
|
+
auto_open_browser Auto-open problems in browser (true/false)
|
|
494
|
+
|
|
495
|
+
\b
|
|
496
|
+
Example:
|
|
497
|
+
dsap config --list Show all settings
|
|
498
|
+
dsap config daily_goal View daily goal
|
|
499
|
+
dsap config daily_goal 10 Set daily goal to 10
|
|
500
|
+
dsap config preferred_set blind75 Focus on Blind 75
|
|
501
|
+
dsap config --reset Reset to defaults
|
|
502
|
+
"""
|
|
503
|
+
cfg = get_config()
|
|
504
|
+
|
|
505
|
+
if reset:
|
|
506
|
+
cfg.reset()
|
|
507
|
+
display_success("Configuration reset to defaults")
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
if show_list:
|
|
511
|
+
console.print("[bold]Configuration:[/bold]")
|
|
512
|
+
console.print()
|
|
513
|
+
for k, v in cfg.all().items():
|
|
514
|
+
console.print(f" {k}: [cyan]{v}[/cyan]")
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
if key and value:
|
|
518
|
+
try:
|
|
519
|
+
cfg.set(key, value)
|
|
520
|
+
display_success(f"Set {key} = {value}")
|
|
521
|
+
except ValueError as e:
|
|
522
|
+
display_error(str(e))
|
|
523
|
+
elif key:
|
|
524
|
+
val = cfg.get(key)
|
|
525
|
+
if val is not None:
|
|
526
|
+
console.print(f"{key}: [cyan]{val}[/cyan]")
|
|
527
|
+
else:
|
|
528
|
+
display_error(f"Unknown configuration key: {key}")
|
|
529
|
+
else:
|
|
530
|
+
# Show help
|
|
531
|
+
console.print("Usage: dsap config [key] [value]")
|
|
532
|
+
console.print(" dsap config --list")
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@cli.command()
|
|
536
|
+
@click.option(
|
|
537
|
+
"--problems",
|
|
538
|
+
"-p",
|
|
539
|
+
is_flag=True,
|
|
540
|
+
help="Delete all problems (keeps progress history)",
|
|
541
|
+
)
|
|
542
|
+
@click.option(
|
|
543
|
+
"--progress", "-r", is_flag=True, help="Reset progress only (keeps problems)"
|
|
544
|
+
)
|
|
545
|
+
@click.option(
|
|
546
|
+
"--set", "-s", "problem_set", help="Only reset specific problem set (e.g., blind75)"
|
|
547
|
+
)
|
|
548
|
+
@click.option(
|
|
549
|
+
"--all",
|
|
550
|
+
"-a",
|
|
551
|
+
"reset_all",
|
|
552
|
+
is_flag=True,
|
|
553
|
+
help="Reset everything (problems and progress)",
|
|
554
|
+
)
|
|
555
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
556
|
+
def reset(
|
|
557
|
+
problems: bool,
|
|
558
|
+
progress: bool,
|
|
559
|
+
problem_set: str,
|
|
560
|
+
reset_all: bool,
|
|
561
|
+
yes: bool,
|
|
562
|
+
) -> None:
|
|
563
|
+
r"""Reset problems and/or progress.
|
|
564
|
+
|
|
565
|
+
\b
|
|
566
|
+
Example:
|
|
567
|
+
dsap reset --all Reset everything
|
|
568
|
+
dsap reset --problems Delete all problems
|
|
569
|
+
dsap reset --progress Reset progress only
|
|
570
|
+
dsap reset -s blind75 --all Reset only Blind 75
|
|
571
|
+
"""
|
|
572
|
+
if not (problems or progress or reset_all):
|
|
573
|
+
display_error("Please specify what to reset: --problems, --progress, or --all")
|
|
574
|
+
console.print("\nRun [bold]dsap reset --help[/bold] for options.")
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
db = Database()
|
|
578
|
+
|
|
579
|
+
# Normalize set name
|
|
580
|
+
problem_set = normalize_set_name(problem_set)
|
|
581
|
+
|
|
582
|
+
# Build description of what will be reset
|
|
583
|
+
scope = f"'{problem_set}'" if problem_set else "all"
|
|
584
|
+
actions = []
|
|
585
|
+
if reset_all or problems:
|
|
586
|
+
actions.append("delete problems")
|
|
587
|
+
if reset_all or progress:
|
|
588
|
+
actions.append("reset progress")
|
|
589
|
+
|
|
590
|
+
action_str = " and ".join(actions)
|
|
591
|
+
|
|
592
|
+
# Confirm
|
|
593
|
+
if not yes:
|
|
594
|
+
confirmed = click.confirm(
|
|
595
|
+
f"This will {action_str} for {scope} problems. Continue?",
|
|
596
|
+
default=False,
|
|
597
|
+
)
|
|
598
|
+
if not confirmed:
|
|
599
|
+
display_info("Reset cancelled.")
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
# Perform reset
|
|
603
|
+
if reset_all or progress:
|
|
604
|
+
count = db.reset_progress(problem_set)
|
|
605
|
+
display_success(f"Reset progress for {count} problems")
|
|
606
|
+
|
|
607
|
+
if reset_all or problems:
|
|
608
|
+
count = db.delete_all_problems(problem_set)
|
|
609
|
+
display_success(f"Deleted {count} problems")
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def main() -> None:
|
|
613
|
+
"""Entry point for the CLI."""
|
|
614
|
+
cli()
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
if __name__ == "__main__":
|
|
618
|
+
main()
|