Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
.venv/
.pytest_cache/
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,35 @@
# Rahul-s-LAB
My LAB respository..

A complete, working lab solution implemented as a **Task Tracker CLI**.

## Features
- Add tasks
- List tasks
- Mark tasks as complete
- Delete tasks
- JSON file persistence

## Quickstart
```bash
python -m venv .venv
source .venv/bin/activate
pip install -e . pytest
```

Run commands:
```bash
labtracker add "Write the complete code"
labtracker list
labtracker done 1
labtracker delete 1
```

Use a custom database file:
```bash
labtracker --db ./my_tasks.json add "Sample"
```

## Test
```bash
pytest
```
18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "rahul-lab"
version = "0.1.0"
description = "A complete command-line lab task tracker solution"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []

[project.scripts]
labtracker = "src.main:main"

[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]
60 changes: 60 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

import argparse
from pathlib import Path

from src.tracker import TaskTracker


def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Lab Task Tracker")
parser.add_argument(
"--db",
default=".lab/tasks.json",
help="Path to the tracker JSON file (default: .lab/tasks.json)",
)

subparsers = parser.add_subparsers(dest="command", required=True)

add_parser = subparsers.add_parser("add", help="Add a new task")
add_parser.add_argument("title", help="Task title")

subparsers.add_parser("list", help="List tasks")

done_parser = subparsers.add_parser("done", help="Mark task as completed")
done_parser.add_argument("id", type=int, help="Task id")

delete_parser = subparsers.add_parser("delete", help="Delete a task")
delete_parser.add_argument("id", type=int, help="Task id")

return parser


def main() -> None:
parser = _build_parser()
args = parser.parse_args()

tracker = TaskTracker(Path(args.db))

if args.command == "add":
task = tracker.add_task(args.title)
print(f"Added task #{task.id}: {task.title}")
elif args.command == "list":
tasks = tracker.list_tasks()
if not tasks:
print("No tasks yet.")
return

for task in tasks:
status = "✅" if task.done else "⬜"
print(f"{status} [{task.id}] {task.title}")
elif args.command == "done":
task = tracker.complete_task(args.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Catch TaskTracker validation errors in CLI

TaskTracker raises ValueError for normal user mistakes (e.g., unknown task IDs or blank titles), but this call path is not wrapped in error handling, so common input errors print full Python tracebacks and look like crashes. For example, done on a non-existent ID currently exits with a stack trace instead of a clean CLI error; this should be handled in main() with a user-facing message and non-zero exit.

Useful? React with 👍 / 👎.

print(f"Completed task #{task.id}: {task.title}")
elif args.command == "delete":
tracker.delete_task(args.id)
print(f"Deleted task #{args.id}")


if __name__ == "__main__":
main()
63 changes: 63 additions & 0 deletions src/tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from dataclasses import asdict, dataclass
from pathlib import Path
import json


@dataclass
class Task:
id: int
title: str
done: bool = False


class TaskTracker:
def __init__(self, storage_path: Path) -> None:
self.storage_path = storage_path
self.tasks: list[Task] = []
self._load()

def _load(self) -> None:
if not self.storage_path.exists():
self.tasks = []
return

with self.storage_path.open("r", encoding="utf-8") as f:
payload = json.load(f)

self.tasks = [Task(**item) for item in payload]

def _save(self) -> None:
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
with self.storage_path.open("w", encoding="utf-8") as f:
json.dump([asdict(task) for task in self.tasks], f, indent=2)

def add_task(self, title: str) -> Task:
next_id = 1 if not self.tasks else max(task.id for task in self.tasks) + 1
task = Task(id=next_id, title=title.strip(), done=False)
if not task.title:
raise ValueError("Task title cannot be empty.")
self.tasks.append(task)
self._save()
return task

def list_tasks(self) -> list[Task]:
return self.tasks

def complete_task(self, task_id: int) -> Task:
task = self._find(task_id)
task.done = True
self._save()
return task

def delete_task(self, task_id: int) -> None:
task = self._find(task_id)
self.tasks.remove(task)
self._save()

def _find(self, task_id: int) -> Task:
for task in self.tasks:
if task.id == task_id:
return task
raise ValueError(f"Task with id={task_id} was not found.")
37 changes: 37 additions & 0 deletions tests/test_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from pathlib import Path

import pytest

from src.tracker import TaskTracker


def test_add_and_list_tasks(tmp_path: Path) -> None:
tracker = TaskTracker(tmp_path / "tasks.json")
tracker.add_task("Write code")
tracker.add_task("Build solution")

tasks = tracker.list_tasks()
assert [t.title for t in tasks] == ["Write code", "Build solution"]
assert [t.id for t in tasks] == [1, 2]


def test_complete_and_delete_task(tmp_path: Path) -> None:
tracker = TaskTracker(tmp_path / "tasks.json")
tracker.add_task("Task A")
tracker.complete_task(1)
assert tracker.list_tasks()[0].done is True

tracker.delete_task(1)
assert tracker.list_tasks() == []


def test_raises_for_invalid_task(tmp_path: Path) -> None:
tracker = TaskTracker(tmp_path / "tasks.json")
with pytest.raises(ValueError):
tracker.complete_task(9)


def test_rejects_blank_title(tmp_path: Path) -> None:
tracker = TaskTracker(tmp_path / "tasks.json")
with pytest.raises(ValueError):
tracker.add_task(" ")