dsap-cli 1.1.2__tar.gz → 1.1.4__tar.gz
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_cli-1.1.2 → dsap_cli-1.1.4}/CHANGELOG.md +13 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/PKG-INFO +1 -1
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/_version.py +2 -2
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/database.py +22 -11
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/models.py +4 -1
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/sm2.py +1 -2
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/ui.py +5 -5
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/tests/test_models.py +8 -6
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/.github/workflows/ci.yml +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/.gitignore +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/.pre-commit-config.yaml +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/LICENSE +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/README.md +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/USER_GUIDE.md +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/assets/dsap-cli.gif +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/pyproject.toml +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/__init__.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/__main__.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/cli.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/config.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/data/blind75.yaml +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/data/grind75.yaml +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/data/neetcode150.yaml +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/problem_sets.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/src/dsap/py.typed +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/tests/__init__.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/tests/test_cli.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/tests/test_config.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/tests/test_database.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/tests/test_problem_sets.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/tests/test_sm2.py +0 -0
- {dsap_cli-1.1.2 → dsap_cli-1.1.4}/uv.lock +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DSAP will be documented here.
|
|
4
4
|
|
|
5
|
+
## [1.1.3] - 2026-05-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `dsap list` crashed with `ValidationError: solved_at cannot be before
|
|
10
|
+
first_attempted` for users who reviewed problems before v1.1.0. Root cause:
|
|
11
|
+
v1.1.0 back-filled `first_attempted` with the current timestamp on the next
|
|
12
|
+
review, which landed after the already-stored `solved_at`. Fix: the validator
|
|
13
|
+
now coerces `first_attempted` to `solved_at` instead of raising; a one-time
|
|
14
|
+
migration runs on startup to correct existing rows; and the UPDATE query
|
|
15
|
+
no longer sets `first_attempted` later than `solved_at`.
|
|
16
|
+
|
|
5
17
|
## [1.1.2] - 2026-05-08
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -98,6 +110,7 @@ Initial release.
|
|
|
98
110
|
|
|
99
111
|
---
|
|
100
112
|
|
|
113
|
+
[1.1.3]: https://github.com/juma-paul/dsap-cli/compare/v1.1.2...v1.1.3
|
|
101
114
|
[1.1.2]: https://github.com/juma-paul/dsap-cli/compare/v1.1.1...v1.1.2
|
|
102
115
|
[1.1.1]: https://github.com/juma-paul/dsap-cli/compare/v1.1.0...v1.1.1
|
|
103
116
|
[1.1.0]: https://github.com/juma-paul/dsap-cli/compare/v1.0.2...v1.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dsap-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: CLI tool for practicing data structures and algorithms using spaced repetition.
|
|
5
5
|
Project-URL: Repository, https://github.com/juma-paul/dsap-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/juma-paul/dsap-cli/issues
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 1,
|
|
21
|
+
__version__ = version = '1.1.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 1, 4)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -12,7 +12,7 @@ import os
|
|
|
12
12
|
import sqlite3
|
|
13
13
|
from collections.abc import Iterator
|
|
14
14
|
from contextlib import contextmanager
|
|
15
|
-
from datetime import datetime, timedelta
|
|
15
|
+
from datetime import date, datetime, timedelta
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any
|
|
18
18
|
|
|
@@ -141,6 +141,16 @@ class Database:
|
|
|
141
141
|
ON sessions(date);
|
|
142
142
|
""")
|
|
143
143
|
|
|
144
|
+
# Fix rows where first_attempted was back-filled after solved_at
|
|
145
|
+
# (introduced by v1.1.0 when first_attempted was NULL for old records)
|
|
146
|
+
conn.execute("""
|
|
147
|
+
UPDATE progress
|
|
148
|
+
SET first_attempted = solved_at
|
|
149
|
+
WHERE first_attempted IS NOT NULL
|
|
150
|
+
AND solved_at IS NOT NULL
|
|
151
|
+
AND first_attempted > solved_at
|
|
152
|
+
""")
|
|
153
|
+
|
|
144
154
|
# ==================== Problem Operations ====================
|
|
145
155
|
|
|
146
156
|
def add_problem(self, problem: Problem) -> int:
|
|
@@ -241,9 +251,9 @@ class Database:
|
|
|
241
251
|
params.append(problem_set)
|
|
242
252
|
|
|
243
253
|
if due_only:
|
|
244
|
-
|
|
245
|
-
conditions.append("(pr.next_review <= ? OR pr.next_review IS NULL)")
|
|
246
|
-
params.append(
|
|
254
|
+
today = datetime.now().date().isoformat()
|
|
255
|
+
conditions.append("(DATE(pr.next_review) <= ? OR pr.next_review IS NULL)")
|
|
256
|
+
params.append(today)
|
|
247
257
|
|
|
248
258
|
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
|
|
249
259
|
params.append(limit)
|
|
@@ -472,8 +482,9 @@ class Database:
|
|
|
472
482
|
attempts = attempts + 1,
|
|
473
483
|
solved = CASE WHEN ? >= 3 THEN 1 ELSE solved END,
|
|
474
484
|
first_attempted = CASE
|
|
475
|
-
WHEN first_attempted IS NULL THEN
|
|
476
|
-
|
|
485
|
+
WHEN first_attempted IS NOT NULL THEN first_attempted
|
|
486
|
+
WHEN solved_at IS NOT NULL THEN solved_at
|
|
487
|
+
ELSE ?
|
|
477
488
|
END,
|
|
478
489
|
solved_at = CASE
|
|
479
490
|
WHEN ? >= 3 AND solved = 0 THEN ?
|
|
@@ -566,21 +577,21 @@ class Database:
|
|
|
566
577
|
""").fetchone()
|
|
567
578
|
|
|
568
579
|
# Due counts
|
|
569
|
-
|
|
570
|
-
week_later = (
|
|
580
|
+
today = date.today().isoformat()
|
|
581
|
+
week_later = (date.today() + timedelta(days=7)).isoformat()
|
|
571
582
|
|
|
572
583
|
due_today = conn.execute(
|
|
573
584
|
"""
|
|
574
585
|
SELECT COUNT(*) FROM progress
|
|
575
|
-
WHERE next_review <= ? OR next_review IS NULL
|
|
586
|
+
WHERE DATE(next_review) <= ? OR next_review IS NULL
|
|
576
587
|
""",
|
|
577
|
-
(
|
|
588
|
+
(today,),
|
|
578
589
|
).fetchone()[0]
|
|
579
590
|
|
|
580
591
|
due_week = conn.execute(
|
|
581
592
|
"""
|
|
582
593
|
SELECT COUNT(*) FROM progress
|
|
583
|
-
WHERE next_review <= ?
|
|
594
|
+
WHERE DATE(next_review) <= ? OR next_review IS NULL
|
|
584
595
|
""",
|
|
585
596
|
(week_later,),
|
|
586
597
|
).fetchone()[0]
|
|
@@ -129,7 +129,10 @@ class ProblemProgress(DSAPBaseModel):
|
|
|
129
129
|
and self.first_attempted
|
|
130
130
|
and self.solved_at < self.first_attempted
|
|
131
131
|
):
|
|
132
|
-
|
|
132
|
+
# Coerce: first_attempted cannot be later than solved_at.
|
|
133
|
+
# This can happen with data written by older versions where
|
|
134
|
+
# first_attempted was back-filled after solved_at was set.
|
|
135
|
+
self.first_attempted = self.solved_at
|
|
133
136
|
|
|
134
137
|
if self.solved and self.solved_at is None:
|
|
135
138
|
self.solved_at = datetime.now()
|
|
@@ -252,8 +252,7 @@ class SM2State:
|
|
|
252
252
|
now = datetime.now()
|
|
253
253
|
if self.next_review is None:
|
|
254
254
|
return 0
|
|
255
|
-
|
|
256
|
-
return delta.days
|
|
255
|
+
return (self.next_review.date() - now.date()).days
|
|
257
256
|
|
|
258
257
|
|
|
259
258
|
def calculate_easiness_factor(current_ef: float, quality: int) -> float:
|
|
@@ -8,7 +8,7 @@ Provides beautiful terminal output using Rich library:
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import webbrowser
|
|
11
|
-
from datetime import
|
|
11
|
+
from datetime import date
|
|
12
12
|
|
|
13
13
|
from rich import box
|
|
14
14
|
from rich.console import Console
|
|
@@ -137,8 +137,7 @@ def display_problem(
|
|
|
137
137
|
f" EF: {progress.easiness_factor:.2f} | Interval: {progress.interval} days"
|
|
138
138
|
)
|
|
139
139
|
if progress.next_review:
|
|
140
|
-
|
|
141
|
-
delta = (progress.next_review - now).days
|
|
140
|
+
delta = (progress.next_review.date() - date.today()).days
|
|
142
141
|
if delta < 0:
|
|
143
142
|
due_str = f"Overdue by {abs(delta)} day{'s' if abs(delta) != 1 else ''}"
|
|
144
143
|
elif delta == 0:
|
|
@@ -372,12 +371,13 @@ def display_problem_list(
|
|
|
372
371
|
ef = "-"
|
|
373
372
|
|
|
374
373
|
if progress and progress.next_review:
|
|
375
|
-
|
|
376
|
-
delta = (progress.next_review - now).days
|
|
374
|
+
delta = (progress.next_review.date() - date.today()).days
|
|
377
375
|
if delta < 0:
|
|
378
376
|
due_text = f"[red]{abs(delta)}d late[/red]"
|
|
379
377
|
elif delta == 0:
|
|
380
378
|
due_text = "[yellow]today[/yellow]"
|
|
379
|
+
elif delta == 1:
|
|
380
|
+
due_text = "[dim]tomorrow[/dim]"
|
|
381
381
|
else:
|
|
382
382
|
due_text = f"[dim]in {delta}d[/dim]"
|
|
383
383
|
else:
|
|
@@ -194,15 +194,17 @@ class TestProblemProgress:
|
|
|
194
194
|
make_progress(last_quality=-1)
|
|
195
195
|
|
|
196
196
|
def test_solved_date_validation(self) -> None:
|
|
197
|
+
# When solved_at is before first_attempted, first_attempted is coerced
|
|
198
|
+
# to match solved_at rather than raising — supports legacy data.
|
|
197
199
|
first = datetime.now()
|
|
198
200
|
before_first = first - timedelta(days=1)
|
|
199
201
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
progress = make_progress(
|
|
203
|
+
first_attempted=first,
|
|
204
|
+
solved_at=before_first,
|
|
205
|
+
solved=True,
|
|
206
|
+
)
|
|
207
|
+
assert progress.first_attempted == before_first
|
|
206
208
|
|
|
207
209
|
def test_solved_sets_solved_at(self) -> None:
|
|
208
210
|
progress = make_progress(solved=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|