dsa-tracker 0.2.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.
@@ -0,0 +1,6 @@
1
+ """dsa-tracker — log DSA prep solves from a Python notebook or script."""
2
+
3
+ from .client import Tracker, TrackerError
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["Tracker", "TrackerError", "__version__"]
dsa_tracker/client.py ADDED
@@ -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,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,5 @@
1
+ dsa_tracker/__init__.py,sha256=xvCklHMHOAxgvMXPDNHcvYP8_kNoc4APFTJh8R0SUQY,195
2
+ dsa_tracker/client.py,sha256=Qf9QrbYr-Df1dAlVRp2_-3oeI59_YtktNm-GdH8_8-M,6506
3
+ dsa_tracker-0.2.0.dist-info/METADATA,sha256=IczhQ8oBrKoKKz7on1ogNJ8rzN1D278V9Ga_9IWMTJM,4368
4
+ dsa_tracker-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ dsa_tracker-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any