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,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__"]
@@ -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"]