dsa-tracker 0.2.0__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.
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Root-level safety net for the dsa-tracker monorepo. Each entry applies
|
|
2
|
+
# to every subtree (backend, frontend, colab_helper). Subdirectories MAY
|
|
3
|
+
# add their own .gitignore for project-local extras, but anything common
|
|
4
|
+
# belongs here so we never have to chase the same rule into two files.
|
|
5
|
+
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
# Secrets / local-only state — must never reach a remote
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
.env
|
|
10
|
+
.env.*
|
|
11
|
+
!.env.example
|
|
12
|
+
!.env.*.example
|
|
13
|
+
|
|
14
|
+
# Auth scratch files used by manual API testing
|
|
15
|
+
cookies.txt
|
|
16
|
+
*_cookies.txt
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Databases (local dev only — production uses Postgres on a managed host)
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
*.db
|
|
22
|
+
*.db-journal
|
|
23
|
+
*.sqlite
|
|
24
|
+
*.sqlite3
|
|
25
|
+
*.sqlite3-journal
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Python
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
__pycache__/
|
|
31
|
+
*.py[cod]
|
|
32
|
+
*$py.class
|
|
33
|
+
*.so
|
|
34
|
+
|
|
35
|
+
# Virtual environments
|
|
36
|
+
.venv/
|
|
37
|
+
venv/
|
|
38
|
+
env/
|
|
39
|
+
ENV/
|
|
40
|
+
|
|
41
|
+
# Packaging / build
|
|
42
|
+
*.egg-info/
|
|
43
|
+
*.egg
|
|
44
|
+
.eggs/
|
|
45
|
+
build/
|
|
46
|
+
dist/
|
|
47
|
+
sdist/
|
|
48
|
+
wheels/
|
|
49
|
+
*.whl
|
|
50
|
+
pip-log.txt
|
|
51
|
+
pip-delete-this-directory.txt
|
|
52
|
+
|
|
53
|
+
# Test / lint / type-check caches
|
|
54
|
+
.pytest_cache/
|
|
55
|
+
.ruff_cache/
|
|
56
|
+
.mypy_cache/
|
|
57
|
+
.pyright/
|
|
58
|
+
.pyre/
|
|
59
|
+
.tox/
|
|
60
|
+
.nox/
|
|
61
|
+
|
|
62
|
+
# Coverage
|
|
63
|
+
.coverage
|
|
64
|
+
.coverage.*
|
|
65
|
+
coverage.xml
|
|
66
|
+
htmlcov/
|
|
67
|
+
.cache/
|
|
68
|
+
.hypothesis/
|
|
69
|
+
|
|
70
|
+
# Notebooks (colab_helper has examples; checkpoints are local)
|
|
71
|
+
.ipynb_checkpoints/
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Node / JS / TypeScript
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
node_modules/
|
|
77
|
+
.npm/
|
|
78
|
+
.yarn/
|
|
79
|
+
.pnpm-store/
|
|
80
|
+
.pnp.*
|
|
81
|
+
|
|
82
|
+
# Build output
|
|
83
|
+
dist/
|
|
84
|
+
build/
|
|
85
|
+
out/
|
|
86
|
+
|
|
87
|
+
# Vite / TS / bundler caches
|
|
88
|
+
.vite/
|
|
89
|
+
.turbo/
|
|
90
|
+
.parcel-cache/
|
|
91
|
+
*.tsbuildinfo
|
|
92
|
+
|
|
93
|
+
# JS coverage
|
|
94
|
+
coverage/
|
|
95
|
+
|
|
96
|
+
# Debug logs
|
|
97
|
+
npm-debug.log*
|
|
98
|
+
yarn-debug.log*
|
|
99
|
+
yarn-error.log*
|
|
100
|
+
pnpm-debug.log*
|
|
101
|
+
lerna-debug.log*
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Logs
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
*.log
|
|
107
|
+
logs/
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Editors / IDEs
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
.vscode/
|
|
113
|
+
!.vscode/extensions.json
|
|
114
|
+
.idea/
|
|
115
|
+
*.iml
|
|
116
|
+
.fleet/
|
|
117
|
+
*.swp
|
|
118
|
+
*.swo
|
|
119
|
+
*~
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# OS junk
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
.DS_Store
|
|
125
|
+
.AppleDouble
|
|
126
|
+
.LSOverride
|
|
127
|
+
Thumbs.db
|
|
128
|
+
ehthumbs.db
|
|
129
|
+
Desktop.ini
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Misc local scratch
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
.tmp/
|
|
135
|
+
tmp/
|
|
136
|
+
*.local
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# Whitelists — undo overly broad rules from the user's global gitignore
|
|
140
|
+
# (their global has `*.ini`, which would otherwise drop alembic.ini).
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
!*.ini
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dsa-tracker
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Log DSA prep solves from a Colab/Jupyter notebook to your DSA Tracker instance.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yourusername/dsa-tracker
|
|
6
|
+
Project-URL: Issues, https://github.com/yourusername/dsa-tracker/issues
|
|
7
|
+
Author-email: DSA Tracker <noreply@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: colab,dsa,interview-prep,leetcode,spaced-repetition
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Education
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# dsa-tracker
|
|
25
|
+
|
|
26
|
+
Tiny Python client for [DSA Tracker](https://github.com/yourusername/dsa-tracker). Log a solve from a Colab/Jupyter notebook in one call — same SM-2 scheduling and GitHub auto-push you get from the web app.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install dsa-tracker
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Get an API key
|
|
35
|
+
|
|
36
|
+
In your DSA Tracker dashboard → **Settings → API keys → Generate new key**. The plaintext value is shown exactly once — copy it and treat it like a password. You can name keys per device/notebook so revoking one doesn't break the others.
|
|
37
|
+
|
|
38
|
+
## Use it
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from dsa_tracker import Tracker
|
|
42
|
+
|
|
43
|
+
tracker = Tracker(
|
|
44
|
+
api_url="https://your-dsa-tracker.com",
|
|
45
|
+
api_key="dsa_xxxxxxxxxxxxxxxxxxxxxx",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Solve the problem like you always do
|
|
49
|
+
def two_sum(nums, target):
|
|
50
|
+
seen = {}
|
|
51
|
+
for i, n in enumerate(nums):
|
|
52
|
+
if target - n in seen:
|
|
53
|
+
return [seen[target - n], i]
|
|
54
|
+
seen[n] = i
|
|
55
|
+
|
|
56
|
+
# Log it without leaving the notebook
|
|
57
|
+
import inspect
|
|
58
|
+
|
|
59
|
+
result = tracker.log(
|
|
60
|
+
title="Two Sum",
|
|
61
|
+
confidence=4, # 1 (Forgot) → 5 (Easy)
|
|
62
|
+
code=inspect.getsource(two_sum),
|
|
63
|
+
language="python",
|
|
64
|
+
difficulty="easy",
|
|
65
|
+
topics=["Array", "HashMap"],
|
|
66
|
+
pattern="Hash Lookup",
|
|
67
|
+
time_complexity="O(n)",
|
|
68
|
+
space_complexity="O(n)",
|
|
69
|
+
playlist="leetcode-75", # optional: push to this playlist's GitHub config
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
print(f"→ next review: {result['next_review_at']} · status: {result['status']}")
|
|
73
|
+
if result.get('git_push_queued'):
|
|
74
|
+
print("→ pushed to GitHub via playlist 'leetcode-75'")
|
|
75
|
+
elif result.get('git_push_skip_reason'):
|
|
76
|
+
print(f"→ no GitHub push: {result['git_push_skip_reason']}")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## What happens on the server
|
|
80
|
+
|
|
81
|
+
1. `Two Sum` is looked up by slug. Created if new, otherwise reused.
|
|
82
|
+
2. A new `Solution` row captures your code, language, complexities.
|
|
83
|
+
3. A new `Review` row runs through SM-2 with your confidence rating.
|
|
84
|
+
4. `problem.next_review_at` and `problem.status` are updated.
|
|
85
|
+
5. If you have GitHub auto-push enabled, the solution is committed to your repo in the background.
|
|
86
|
+
|
|
87
|
+
The return value tells you exactly what happened:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
{
|
|
91
|
+
'problem_id': 42,
|
|
92
|
+
'solution_id': 87,
|
|
93
|
+
'review_id': 105,
|
|
94
|
+
'created': True, # False if you've logged this title before
|
|
95
|
+
'next_review_at': '2026-05-27',
|
|
96
|
+
'status': 'learning',
|
|
97
|
+
'reps': 1,
|
|
98
|
+
'interval_days': 1,
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## All parameters
|
|
103
|
+
|
|
104
|
+
| Required | Optional |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `title`, `confidence` | `code`, `language`, `difficulty`, `topics`, `pattern`, `platform`, `problem_url`, `approach`, `time_complexity`, `space_complexity`, `notes` |
|
|
107
|
+
|
|
108
|
+
Defaults: `language="python"`, `difficulty="medium"`, `approach="optimal"`. Anything you omit just isn't sent.
|
|
109
|
+
|
|
110
|
+
## Errors
|
|
111
|
+
|
|
112
|
+
The client raises `TrackerError` on any non-2xx response with the server's reason attached:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from dsa_tracker import TrackerError
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
tracker.log(title="X", confidence=4)
|
|
119
|
+
except TrackerError as e:
|
|
120
|
+
print(f"Couldn't log: {e}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Common cases: `401` (bad/revoked API key), `422` (missing required field), connection timeout.
|
|
124
|
+
|
|
125
|
+
## Context manager
|
|
126
|
+
|
|
127
|
+
For scripts that make many calls, use it as a context manager to clean up the underlying connection pool:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
with Tracker(api_url=..., api_key=...) as tracker:
|
|
131
|
+
for problem in queue:
|
|
132
|
+
tracker.log(title=problem.name, confidence=problem.rating, code=problem.code)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# dsa-tracker
|
|
2
|
+
|
|
3
|
+
Tiny Python client for [DSA Tracker](https://github.com/yourusername/dsa-tracker). Log a solve from a Colab/Jupyter notebook in one call — same SM-2 scheduling and GitHub auto-push you get from the web app.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install dsa-tracker
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Get an API key
|
|
12
|
+
|
|
13
|
+
In your DSA Tracker dashboard → **Settings → API keys → Generate new key**. The plaintext value is shown exactly once — copy it and treat it like a password. You can name keys per device/notebook so revoking one doesn't break the others.
|
|
14
|
+
|
|
15
|
+
## Use it
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from dsa_tracker import Tracker
|
|
19
|
+
|
|
20
|
+
tracker = Tracker(
|
|
21
|
+
api_url="https://your-dsa-tracker.com",
|
|
22
|
+
api_key="dsa_xxxxxxxxxxxxxxxxxxxxxx",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Solve the problem like you always do
|
|
26
|
+
def two_sum(nums, target):
|
|
27
|
+
seen = {}
|
|
28
|
+
for i, n in enumerate(nums):
|
|
29
|
+
if target - n in seen:
|
|
30
|
+
return [seen[target - n], i]
|
|
31
|
+
seen[n] = i
|
|
32
|
+
|
|
33
|
+
# Log it without leaving the notebook
|
|
34
|
+
import inspect
|
|
35
|
+
|
|
36
|
+
result = tracker.log(
|
|
37
|
+
title="Two Sum",
|
|
38
|
+
confidence=4, # 1 (Forgot) → 5 (Easy)
|
|
39
|
+
code=inspect.getsource(two_sum),
|
|
40
|
+
language="python",
|
|
41
|
+
difficulty="easy",
|
|
42
|
+
topics=["Array", "HashMap"],
|
|
43
|
+
pattern="Hash Lookup",
|
|
44
|
+
time_complexity="O(n)",
|
|
45
|
+
space_complexity="O(n)",
|
|
46
|
+
playlist="leetcode-75", # optional: push to this playlist's GitHub config
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
print(f"→ next review: {result['next_review_at']} · status: {result['status']}")
|
|
50
|
+
if result.get('git_push_queued'):
|
|
51
|
+
print("→ pushed to GitHub via playlist 'leetcode-75'")
|
|
52
|
+
elif result.get('git_push_skip_reason'):
|
|
53
|
+
print(f"→ no GitHub push: {result['git_push_skip_reason']}")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## What happens on the server
|
|
57
|
+
|
|
58
|
+
1. `Two Sum` is looked up by slug. Created if new, otherwise reused.
|
|
59
|
+
2. A new `Solution` row captures your code, language, complexities.
|
|
60
|
+
3. A new `Review` row runs through SM-2 with your confidence rating.
|
|
61
|
+
4. `problem.next_review_at` and `problem.status` are updated.
|
|
62
|
+
5. If you have GitHub auto-push enabled, the solution is committed to your repo in the background.
|
|
63
|
+
|
|
64
|
+
The return value tells you exactly what happened:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
{
|
|
68
|
+
'problem_id': 42,
|
|
69
|
+
'solution_id': 87,
|
|
70
|
+
'review_id': 105,
|
|
71
|
+
'created': True, # False if you've logged this title before
|
|
72
|
+
'next_review_at': '2026-05-27',
|
|
73
|
+
'status': 'learning',
|
|
74
|
+
'reps': 1,
|
|
75
|
+
'interval_days': 1,
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## All parameters
|
|
80
|
+
|
|
81
|
+
| Required | Optional |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `title`, `confidence` | `code`, `language`, `difficulty`, `topics`, `pattern`, `platform`, `problem_url`, `approach`, `time_complexity`, `space_complexity`, `notes` |
|
|
84
|
+
|
|
85
|
+
Defaults: `language="python"`, `difficulty="medium"`, `approach="optimal"`. Anything you omit just isn't sent.
|
|
86
|
+
|
|
87
|
+
## Errors
|
|
88
|
+
|
|
89
|
+
The client raises `TrackerError` on any non-2xx response with the server's reason attached:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from dsa_tracker import TrackerError
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
tracker.log(title="X", confidence=4)
|
|
96
|
+
except TrackerError as e:
|
|
97
|
+
print(f"Couldn't log: {e}")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Common cases: `401` (bad/revoked API key), `422` (missing required field), connection timeout.
|
|
101
|
+
|
|
102
|
+
## Context manager
|
|
103
|
+
|
|
104
|
+
For scripts that make many calls, use it as a context manager to clean up the underlying connection pool:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
with Tracker(api_url=..., api_key=...) as tracker:
|
|
108
|
+
for problem in queue:
|
|
109
|
+
tracker.log(title=problem.name, confidence=problem.rating, code=problem.code)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Python client for the DSA Tracker `/api/log` endpoint.
|
|
2
|
+
|
|
3
|
+
Designed for use from a Colab/Jupyter notebook or any Python script. The
|
|
4
|
+
common pattern:
|
|
5
|
+
|
|
6
|
+
from dsa_tracker import Tracker
|
|
7
|
+
|
|
8
|
+
tracker = Tracker(
|
|
9
|
+
api_url="https://dsa-tracker.yourdomain.com",
|
|
10
|
+
api_key="dsa_xxx", # generated in Settings → API keys
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def two_sum(nums, target):
|
|
14
|
+
seen = {}
|
|
15
|
+
for i, n in enumerate(nums):
|
|
16
|
+
if target - n in seen:
|
|
17
|
+
return [seen[target - n], i]
|
|
18
|
+
seen[n] = i
|
|
19
|
+
|
|
20
|
+
import inspect
|
|
21
|
+
tracker.log(
|
|
22
|
+
title="Two Sum",
|
|
23
|
+
code=inspect.getsource(two_sum),
|
|
24
|
+
confidence=4,
|
|
25
|
+
difficulty="easy",
|
|
26
|
+
topics=["Array", "HashMap"],
|
|
27
|
+
time_complexity="O(n)",
|
|
28
|
+
space_complexity="O(n)",
|
|
29
|
+
playlist="leetcode-75", # optional: pushes to that playlist's Git config
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
The optional `playlist` slug pushes the solve to GitHub using that
|
|
33
|
+
playlist's per-enrollment Git config (folder template, .md sections,
|
|
34
|
+
branch). Slug must match an enrolled playlist with Git enabled —
|
|
35
|
+
otherwise the push is silently skipped and the response includes a
|
|
36
|
+
`git_push_skip_reason` so you can debug.
|
|
37
|
+
|
|
38
|
+
Returns a dict with the resulting problem/solution/review IDs plus the new
|
|
39
|
+
schedule state — useful for printing back to the notebook.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from typing import Any, Iterable
|
|
45
|
+
|
|
46
|
+
import httpx
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TrackerError(RuntimeError):
|
|
50
|
+
"""Raised when the server rejects a log call (auth, validation, etc.)."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
VALID_DIFFICULTY = {"easy", "medium", "hard"}
|
|
54
|
+
VALID_APPROACH = {"brute", "better", "optimal"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Tracker:
|
|
58
|
+
"""One instance per (api_url, api_key) pair. Cheap to construct.
|
|
59
|
+
|
|
60
|
+
Use as a context manager to close the underlying httpx client when done,
|
|
61
|
+
or call .close() explicitly. Leaving it open across many calls is fine —
|
|
62
|
+
the client uses connection pooling.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, api_url: str, api_key: str, timeout: float = 15.0):
|
|
66
|
+
if not api_url:
|
|
67
|
+
raise ValueError("api_url is required")
|
|
68
|
+
if not api_key:
|
|
69
|
+
raise ValueError("api_key is required")
|
|
70
|
+
self.api_url = api_url.rstrip("/")
|
|
71
|
+
self._client = httpx.Client(
|
|
72
|
+
timeout=timeout,
|
|
73
|
+
headers={
|
|
74
|
+
"Authorization": f"Bearer {api_key}",
|
|
75
|
+
"User-Agent": "dsa-tracker-python/0.2.0",
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def __enter__(self) -> "Tracker":
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, *exc: Any) -> None:
|
|
83
|
+
self.close()
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
self._client.close()
|
|
87
|
+
|
|
88
|
+
def log(
|
|
89
|
+
self,
|
|
90
|
+
title: str,
|
|
91
|
+
confidence: int,
|
|
92
|
+
*,
|
|
93
|
+
code: str | None = None,
|
|
94
|
+
language: str | None = "python",
|
|
95
|
+
difficulty: str = "medium",
|
|
96
|
+
topics: Iterable[str] | None = None,
|
|
97
|
+
pattern: str | None = None,
|
|
98
|
+
platform: str | None = None,
|
|
99
|
+
problem_url: str | None = None,
|
|
100
|
+
approach: str | None = "optimal",
|
|
101
|
+
time_complexity: str | None = None,
|
|
102
|
+
space_complexity: str | None = None,
|
|
103
|
+
notes: str | None = None,
|
|
104
|
+
playlist: str | None = None,
|
|
105
|
+
) -> dict:
|
|
106
|
+
"""Log a solve. Creates or updates the problem by slug; always adds
|
|
107
|
+
a new Review and (if `code` is provided) a new Solution.
|
|
108
|
+
|
|
109
|
+
Required:
|
|
110
|
+
title — human-readable problem name
|
|
111
|
+
confidence — 1 (Forgot) to 5 (Easy)
|
|
112
|
+
|
|
113
|
+
Optional:
|
|
114
|
+
playlist — slug of an enrolled playlist (e.g. "leetcode-75",
|
|
115
|
+
"neetcode-150", or your custom playlist's slug).
|
|
116
|
+
When set AND that playlist has Git enabled, this
|
|
117
|
+
solve is auto-pushed to GitHub using that playlist's
|
|
118
|
+
folder + .md/.code config. Otherwise the response
|
|
119
|
+
includes a `git_push_skip_reason` explaining why.
|
|
120
|
+
|
|
121
|
+
Returns a dict like:
|
|
122
|
+
{
|
|
123
|
+
"problem_id": 42,
|
|
124
|
+
"solution_id": 87,
|
|
125
|
+
"review_id": 105,
|
|
126
|
+
"created": True,
|
|
127
|
+
"next_review_at": "2026-05-27",
|
|
128
|
+
"status": "learning",
|
|
129
|
+
"reps": 1,
|
|
130
|
+
"interval_days": 1,
|
|
131
|
+
"git_push_queued": True,
|
|
132
|
+
"git_push_skip_reason": None,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Raises TrackerError on any non-2xx response.
|
|
136
|
+
"""
|
|
137
|
+
if not 1 <= confidence <= 5:
|
|
138
|
+
raise ValueError("confidence must be 1-5")
|
|
139
|
+
if difficulty not in VALID_DIFFICULTY:
|
|
140
|
+
raise ValueError(f"difficulty must be one of {sorted(VALID_DIFFICULTY)}")
|
|
141
|
+
if approach is not None and approach not in VALID_APPROACH:
|
|
142
|
+
raise ValueError(f"approach must be one of {sorted(VALID_APPROACH)} or None")
|
|
143
|
+
|
|
144
|
+
payload: dict[str, Any] = {
|
|
145
|
+
"title": title,
|
|
146
|
+
"confidence": confidence,
|
|
147
|
+
"difficulty": difficulty,
|
|
148
|
+
}
|
|
149
|
+
# Drop None so server defaults apply
|
|
150
|
+
for k, v in (
|
|
151
|
+
("code", code),
|
|
152
|
+
("language", language),
|
|
153
|
+
("topics", list(topics) if topics else None),
|
|
154
|
+
("pattern", pattern),
|
|
155
|
+
("platform", platform),
|
|
156
|
+
("problem_url", problem_url),
|
|
157
|
+
("approach", approach),
|
|
158
|
+
("time_complexity", time_complexity),
|
|
159
|
+
("space_complexity", space_complexity),
|
|
160
|
+
("notes", notes),
|
|
161
|
+
("playlist", playlist),
|
|
162
|
+
):
|
|
163
|
+
if v is not None:
|
|
164
|
+
payload[k] = v
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
res = self._client.post(f"{self.api_url}/api/log", json=payload)
|
|
168
|
+
except httpx.RequestError as e:
|
|
169
|
+
raise TrackerError(f"could not reach DSA Tracker at {self.api_url}: {e}") from e
|
|
170
|
+
|
|
171
|
+
if 200 <= res.status_code < 300:
|
|
172
|
+
return res.json()
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
body = res.json()
|
|
176
|
+
detail = body.get("detail") or body
|
|
177
|
+
except ValueError:
|
|
178
|
+
detail = res.text[:300]
|
|
179
|
+
raise TrackerError(f"server rejected log ({res.status_code}): {detail}")
|
|
180
|
+
|
|
181
|
+
def ping(self) -> bool:
|
|
182
|
+
"""Verify api_url + api_key by hitting /health and a key-scoped check.
|
|
183
|
+
|
|
184
|
+
Returns True on success. Raises TrackerError on auth or network errors.
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
r = self._client.get(f"{self.api_url}/health")
|
|
188
|
+
except httpx.RequestError as e:
|
|
189
|
+
raise TrackerError(f"could not reach {self.api_url}: {e}") from e
|
|
190
|
+
if r.status_code != 200:
|
|
191
|
+
raise TrackerError(f"server at {self.api_url} returned {r.status_code}")
|
|
192
|
+
return True
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Example: log a problem from a Python script (mirror of the Colab cell).
|
|
2
|
+
|
|
3
|
+
Run as: python colab_demo.py
|
|
4
|
+
|
|
5
|
+
Set DSA_TRACKER_URL and DSA_TRACKER_KEY in the environment first.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import inspect
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from dsa_tracker import Tracker, TrackerError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def two_sum(nums: list[int], target: int) -> list[int] | None:
|
|
16
|
+
"""Classic hash-map one-pass solution."""
|
|
17
|
+
seen: dict[int, int] = {}
|
|
18
|
+
for i, n in enumerate(nums):
|
|
19
|
+
complement = target - n
|
|
20
|
+
if complement in seen:
|
|
21
|
+
return [seen[complement], i]
|
|
22
|
+
seen[n] = i
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
api_url = os.environ.get("DSA_TRACKER_URL", "http://localhost:8000")
|
|
28
|
+
api_key = os.environ.get("DSA_TRACKER_KEY")
|
|
29
|
+
if not api_key:
|
|
30
|
+
print(
|
|
31
|
+
"ERROR: set DSA_TRACKER_KEY (generate one in Settings → API keys).",
|
|
32
|
+
file=sys.stderr,
|
|
33
|
+
)
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
# Quick sanity check
|
|
37
|
+
assert two_sum([2, 7, 11, 15], 9) == [0, 1]
|
|
38
|
+
|
|
39
|
+
with Tracker(api_url=api_url, api_key=api_key) as tracker:
|
|
40
|
+
result = tracker.log(
|
|
41
|
+
title="Two Sum",
|
|
42
|
+
confidence=4,
|
|
43
|
+
code=inspect.getsource(two_sum),
|
|
44
|
+
language="python",
|
|
45
|
+
difficulty="easy",
|
|
46
|
+
topics=["Array", "HashMap"],
|
|
47
|
+
pattern="Hash Lookup",
|
|
48
|
+
time_complexity="O(n)",
|
|
49
|
+
space_complexity="O(n)",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
print(f"✅ logged — problem #{result['problem_id']}")
|
|
53
|
+
print(f" status: {result['status']}")
|
|
54
|
+
print(f" reps: {result['reps']}")
|
|
55
|
+
print(f" next review: {result['next_review_at']}")
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
try:
|
|
61
|
+
sys.exit(main())
|
|
62
|
+
except TrackerError as e:
|
|
63
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
64
|
+
sys.exit(2)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dsa-tracker"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Log DSA prep solves from a Colab/Jupyter notebook to your DSA Tracker instance."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "DSA Tracker", email = "noreply@example.com" }]
|
|
9
|
+
keywords = ["dsa", "leetcode", "spaced-repetition", "colab", "interview-prep"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.9",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Education",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["httpx>=0.27"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/yourusername/dsa-tracker"
|
|
26
|
+
Issues = "https://github.com/yourusername/dsa-tracker/issues"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["dsa_tracker"]
|