qbit-cli 0.1.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,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: qbit-cli
3
+ Version: 0.1.0
4
+ Summary: A lightweight CLI todo list manager
5
+ Author: qbit contributors
6
+ License-Expression: MIT
7
+ Keywords: todo,cli,task-manager
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
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 :: Utilities
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # qbit
23
+
24
+ A lightweight CLI todo app for your terminal.
25
+
26
+ ## Features
27
+
28
+ - Add tasks
29
+ - List tasks
30
+ - Mark tasks as done
31
+ - Delete tasks
32
+ - Clear completed tasks
33
+
34
+ ## Installation
35
+
36
+ ### Local editable install
37
+
38
+ ```bash
39
+ pip install -e .
40
+ ```
41
+
42
+ ### Install from built wheel
43
+
44
+ ```bash
45
+ python -m build
46
+ pip install dist/qbit-0.1.0-py3-none-any.whl
47
+ ```
48
+
49
+ ## Release to TestPyPI and PyPI
50
+
51
+ Install Twine:
52
+
53
+ ```bash
54
+ pip install twine build
55
+ ```
56
+
57
+ Build artifacts:
58
+
59
+ ```bash
60
+ python -m build
61
+ ```
62
+
63
+ Upload to TestPyPI:
64
+
65
+ ```bash
66
+ python -m twine upload --repository testpypi dist/*
67
+ ```
68
+
69
+ Test install from TestPyPI:
70
+
71
+ ```bash
72
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple qbit
73
+ ```
74
+
75
+ Upload to PyPI:
76
+
77
+ ```bash
78
+ python -m twine upload dist/*
79
+ ```
80
+
81
+ ## Usage
82
+
83
+ ```bash
84
+ qbit add "Buy milk"
85
+ qbit list
86
+ qbit done 1
87
+ qbit delete 1
88
+ qbit clear
89
+ ```
90
+
91
+ ## Data location
92
+
93
+ qbit stores data in an OS-specific user data directory:
94
+
95
+ - Windows: `%APPDATA%/qbit/todos.json`
96
+ - macOS: `~/Library/Application Support/qbit/todos.json`
97
+ - Linux: `$XDG_DATA_HOME/qbit/todos.json` or `~/.local/share/qbit/todos.json`
98
+
99
+ You can override the file path with:
100
+
101
+ ```bash
102
+ qbit --data-file /path/to/todos.json list
103
+ ```
@@ -0,0 +1,82 @@
1
+ # qbit
2
+
3
+ A lightweight CLI todo app for your terminal.
4
+
5
+ ## Features
6
+
7
+ - Add tasks
8
+ - List tasks
9
+ - Mark tasks as done
10
+ - Delete tasks
11
+ - Clear completed tasks
12
+
13
+ ## Installation
14
+
15
+ ### Local editable install
16
+
17
+ ```bash
18
+ pip install -e .
19
+ ```
20
+
21
+ ### Install from built wheel
22
+
23
+ ```bash
24
+ python -m build
25
+ pip install dist/qbit-0.1.0-py3-none-any.whl
26
+ ```
27
+
28
+ ## Release to TestPyPI and PyPI
29
+
30
+ Install Twine:
31
+
32
+ ```bash
33
+ pip install twine build
34
+ ```
35
+
36
+ Build artifacts:
37
+
38
+ ```bash
39
+ python -m build
40
+ ```
41
+
42
+ Upload to TestPyPI:
43
+
44
+ ```bash
45
+ python -m twine upload --repository testpypi dist/*
46
+ ```
47
+
48
+ Test install from TestPyPI:
49
+
50
+ ```bash
51
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple qbit
52
+ ```
53
+
54
+ Upload to PyPI:
55
+
56
+ ```bash
57
+ python -m twine upload dist/*
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ```bash
63
+ qbit add "Buy milk"
64
+ qbit list
65
+ qbit done 1
66
+ qbit delete 1
67
+ qbit clear
68
+ ```
69
+
70
+ ## Data location
71
+
72
+ qbit stores data in an OS-specific user data directory:
73
+
74
+ - Windows: `%APPDATA%/qbit/todos.json`
75
+ - macOS: `~/Library/Application Support/qbit/todos.json`
76
+ - Linux: `$XDG_DATA_HOME/qbit/todos.json` or `~/.local/share/qbit/todos.json`
77
+
78
+ You can override the file path with:
79
+
80
+ ```bash
81
+ qbit --data-file /path/to/todos.json list
82
+ ```
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qbit-cli"
7
+ version = "0.1.0"
8
+ description = "A lightweight CLI todo list manager"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{ name = "qbit contributors" }]
13
+ keywords = ["todo", "cli", "task-manager"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Utilities",
26
+ ]
27
+
28
+
29
+ [project.scripts]
30
+ qbit = "qbit.cli:main"
31
+
32
+ [tool.setuptools]
33
+ package-dir = {"" = "src"}
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """qbit package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,105 @@
1
+ """Command-line interface for qbit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ from typing import Sequence
8
+
9
+ from .config import default_data_file
10
+ from .storage import TodoStorage
11
+ from .todo import TodoList
12
+
13
+
14
+ def build_parser() -> argparse.ArgumentParser:
15
+ parser = argparse.ArgumentParser(prog="qbit", description="Manage your todos from the CLI")
16
+ parser.add_argument(
17
+ "--data-file",
18
+ type=Path,
19
+ default=None,
20
+ help="Override the default todo data file path.",
21
+ )
22
+
23
+ subparsers = parser.add_subparsers(dest="command", required=True)
24
+
25
+ add_parser = subparsers.add_parser("add", help="Add a new todo")
26
+ add_parser.add_argument("text", help="Todo text")
27
+
28
+ subparsers.add_parser("list", help="List todos")
29
+
30
+ done_parser = subparsers.add_parser("done", help="Mark a todo as completed")
31
+ done_parser.add_argument("id", type=int, help="Todo id")
32
+
33
+ delete_parser = subparsers.add_parser("delete", help="Delete a todo")
34
+ delete_parser.add_argument("id", type=int, help="Todo id")
35
+
36
+ subparsers.add_parser("clear", help="Clear completed todos")
37
+
38
+ return parser
39
+
40
+
41
+ def main(argv: Sequence[str] | None = None) -> int:
42
+ parser = build_parser()
43
+ args = parser.parse_args(argv)
44
+
45
+ data_file = args.data_file if args.data_file else default_data_file()
46
+ storage = TodoStorage(data_file)
47
+
48
+ try:
49
+ todo_list = TodoList(storage.load())
50
+ except ValueError as exc:
51
+ print(f"Error: {exc}")
52
+ return 2
53
+
54
+ if args.command == "add":
55
+ try:
56
+ item = todo_list.add(args.text)
57
+ except ValueError as exc:
58
+ print(f"Error: {exc}")
59
+ return 2
60
+ storage.save(todo_list.items())
61
+ print(f"Added [{item.id}] {item.text}")
62
+ return 0
63
+
64
+ if args.command == "list":
65
+ items = todo_list.items()
66
+ if not items:
67
+ print("No todos yet.")
68
+ return 0
69
+ for item in items:
70
+ status = "x" if item.done else " "
71
+ print(f"[{item.id}] [{status}] {item.text}")
72
+ return 0
73
+
74
+ if args.command == "done":
75
+ try:
76
+ item = todo_list.mark_done(args.id)
77
+ except KeyError as exc:
78
+ print(f"Error: {exc}")
79
+ return 2
80
+ storage.save(todo_list.items())
81
+ print(f"Completed [{item.id}] {item.text}")
82
+ return 0
83
+
84
+ if args.command == "delete":
85
+ try:
86
+ item = todo_list.delete(args.id)
87
+ except KeyError as exc:
88
+ print(f"Error: {exc}")
89
+ return 2
90
+ storage.save(todo_list.items())
91
+ print(f"Deleted [{item.id}] {item.text}")
92
+ return 0
93
+
94
+ if args.command == "clear":
95
+ removed = todo_list.clear_completed()
96
+ storage.save(todo_list.items())
97
+ print(f"Removed {removed} completed todo(s)")
98
+ return 0
99
+
100
+ parser.print_help()
101
+ return 2
102
+
103
+
104
+ if __name__ == "__main__":
105
+ raise SystemExit(main())
@@ -0,0 +1,25 @@
1
+ """Configuration and OS-aware path helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ APP_NAME = "qbit"
10
+ DEFAULT_FILE_NAME = "todos.json"
11
+
12
+
13
+ def default_data_file() -> Path:
14
+ """Return the default todo data file path for the current OS."""
15
+ if sys.platform.startswith("win"):
16
+ appdata = os.environ.get("APPDATA")
17
+ base_dir = Path(appdata) if appdata else Path.home() / "AppData" / "Roaming"
18
+ return base_dir / APP_NAME / DEFAULT_FILE_NAME
19
+
20
+ if sys.platform == "darwin":
21
+ return Path.home() / "Library" / "Application Support" / APP_NAME / DEFAULT_FILE_NAME
22
+
23
+ xdg_data_home = os.environ.get("XDG_DATA_HOME")
24
+ base_dir = Path(xdg_data_home) if xdg_data_home else Path.home() / ".local" / "share"
25
+ return base_dir / APP_NAME / DEFAULT_FILE_NAME
@@ -0,0 +1,38 @@
1
+ """Persistence layer for todo data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from .todo import TodoItem
9
+
10
+
11
+ class TodoStorage:
12
+ """Reads and writes todos from a JSON file."""
13
+
14
+ def __init__(self, data_file: Path) -> None:
15
+ self._data_file = data_file
16
+
17
+ def load(self) -> list[TodoItem]:
18
+ if not self._data_file.exists():
19
+ return []
20
+
21
+ with self._data_file.open("r", encoding="utf-8") as handle:
22
+ payload = json.load(handle)
23
+
24
+ if not isinstance(payload, list):
25
+ raise ValueError("Corrupted data: expected a JSON list.")
26
+
27
+ return [TodoItem.from_dict(item) for item in payload]
28
+
29
+ def save(self, items: list[TodoItem]) -> None:
30
+ self._data_file.parent.mkdir(parents=True, exist_ok=True)
31
+ temp_file = self._data_file.with_suffix(self._data_file.suffix + ".tmp")
32
+
33
+ serialized = [item.to_dict() for item in items]
34
+ with temp_file.open("w", encoding="utf-8") as handle:
35
+ json.dump(serialized, handle, indent=2)
36
+ handle.write("\n")
37
+
38
+ temp_file.replace(self._data_file)
@@ -0,0 +1,78 @@
1
+ """Core todo domain logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, asdict
6
+ from datetime import datetime, timezone
7
+
8
+
9
+ @dataclass
10
+ class TodoItem:
11
+ """A single todo item."""
12
+
13
+ id: int
14
+ text: str
15
+ done: bool = False
16
+ created_at: str = ""
17
+
18
+ def to_dict(self) -> dict:
19
+ return asdict(self)
20
+
21
+ @classmethod
22
+ def from_dict(cls, payload: dict) -> "TodoItem":
23
+ return cls(
24
+ id=int(payload["id"]),
25
+ text=str(payload["text"]),
26
+ done=bool(payload.get("done", False)),
27
+ created_at=str(payload.get("created_at", "")),
28
+ )
29
+
30
+
31
+ class TodoList:
32
+ """Mutable list of todo items with stable integer IDs."""
33
+
34
+ def __init__(self, items: list[TodoItem] | None = None) -> None:
35
+ self._items = items[:] if items else []
36
+
37
+ def items(self) -> list[TodoItem]:
38
+ return self._items[:]
39
+
40
+ def add(self, text: str) -> TodoItem:
41
+ text = text.strip()
42
+ if not text:
43
+ raise ValueError("Todo text cannot be empty.")
44
+
45
+ item = TodoItem(
46
+ id=self._next_id(),
47
+ text=text,
48
+ done=False,
49
+ created_at=datetime.now(timezone.utc).isoformat(),
50
+ )
51
+ self._items.append(item)
52
+ return item
53
+
54
+ def mark_done(self, item_id: int) -> TodoItem:
55
+ item = self._find(item_id)
56
+ item.done = True
57
+ return item
58
+
59
+ def delete(self, item_id: int) -> TodoItem:
60
+ item = self._find(item_id)
61
+ self._items = [existing for existing in self._items if existing.id != item_id]
62
+ return item
63
+
64
+ def clear_completed(self) -> int:
65
+ before = len(self._items)
66
+ self._items = [item for item in self._items if not item.done]
67
+ return before - len(self._items)
68
+
69
+ def _next_id(self) -> int:
70
+ if not self._items:
71
+ return 1
72
+ return max(item.id for item in self._items) + 1
73
+
74
+ def _find(self, item_id: int) -> TodoItem:
75
+ for item in self._items:
76
+ if item.id == item_id:
77
+ return item
78
+ raise KeyError(f"Todo with id {item_id} was not found.")
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: qbit-cli
3
+ Version: 0.1.0
4
+ Summary: A lightweight CLI todo list manager
5
+ Author: qbit contributors
6
+ License-Expression: MIT
7
+ Keywords: todo,cli,task-manager
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
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 :: Utilities
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # qbit
23
+
24
+ A lightweight CLI todo app for your terminal.
25
+
26
+ ## Features
27
+
28
+ - Add tasks
29
+ - List tasks
30
+ - Mark tasks as done
31
+ - Delete tasks
32
+ - Clear completed tasks
33
+
34
+ ## Installation
35
+
36
+ ### Local editable install
37
+
38
+ ```bash
39
+ pip install -e .
40
+ ```
41
+
42
+ ### Install from built wheel
43
+
44
+ ```bash
45
+ python -m build
46
+ pip install dist/qbit-0.1.0-py3-none-any.whl
47
+ ```
48
+
49
+ ## Release to TestPyPI and PyPI
50
+
51
+ Install Twine:
52
+
53
+ ```bash
54
+ pip install twine build
55
+ ```
56
+
57
+ Build artifacts:
58
+
59
+ ```bash
60
+ python -m build
61
+ ```
62
+
63
+ Upload to TestPyPI:
64
+
65
+ ```bash
66
+ python -m twine upload --repository testpypi dist/*
67
+ ```
68
+
69
+ Test install from TestPyPI:
70
+
71
+ ```bash
72
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple qbit
73
+ ```
74
+
75
+ Upload to PyPI:
76
+
77
+ ```bash
78
+ python -m twine upload dist/*
79
+ ```
80
+
81
+ ## Usage
82
+
83
+ ```bash
84
+ qbit add "Buy milk"
85
+ qbit list
86
+ qbit done 1
87
+ qbit delete 1
88
+ qbit clear
89
+ ```
90
+
91
+ ## Data location
92
+
93
+ qbit stores data in an OS-specific user data directory:
94
+
95
+ - Windows: `%APPDATA%/qbit/todos.json`
96
+ - macOS: `~/Library/Application Support/qbit/todos.json`
97
+ - Linux: `$XDG_DATA_HOME/qbit/todos.json` or `~/.local/share/qbit/todos.json`
98
+
99
+ You can override the file path with:
100
+
101
+ ```bash
102
+ qbit --data-file /path/to/todos.json list
103
+ ```
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/qbit/__init__.py
4
+ src/qbit/cli.py
5
+ src/qbit/config.py
6
+ src/qbit/storage.py
7
+ src/qbit/todo.py
8
+ src/qbit_cli.egg-info/PKG-INFO
9
+ src/qbit_cli.egg-info/SOURCES.txt
10
+ src/qbit_cli.egg-info/dependency_links.txt
11
+ src/qbit_cli.egg-info/entry_points.txt
12
+ src/qbit_cli.egg-info/top_level.txt
13
+ tests/test_cli.py
14
+ tests/test_storage.py
15
+ tests/test_todo.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qbit = qbit.cli:main
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+
3
+ from qbit.cli import main
4
+
5
+
6
+ def test_cli_add_then_list(tmp_path, capsys):
7
+ data_file = tmp_path / "todos.json"
8
+
9
+ rc_add = main(["--data-file", str(data_file), "add", "buy milk"])
10
+ out_add = capsys.readouterr().out
11
+
12
+ assert rc_add == 0
13
+ assert "Added [1] buy milk" in out_add
14
+
15
+ rc_list = main(["--data-file", str(data_file), "list"])
16
+ out_list = capsys.readouterr().out
17
+
18
+ assert rc_list == 0
19
+ assert "[1] [ ] buy milk" in out_list
20
+
21
+
22
+ def test_cli_done_missing_id_returns_error(tmp_path, capsys):
23
+ data_file = tmp_path / "todos.json"
24
+
25
+ rc = main(["--data-file", str(data_file), "done", "99"])
26
+ out = capsys.readouterr().out
27
+
28
+ assert rc == 2
29
+ assert "Error:" in out
30
+
31
+
32
+ def test_cli_clear_completed(tmp_path, capsys):
33
+ data_file = Path(tmp_path) / "todos.json"
34
+
35
+ main(["--data-file", str(data_file), "add", "task1"])
36
+ main(["--data-file", str(data_file), "done", "1"])
37
+
38
+ rc = main(["--data-file", str(data_file), "clear"])
39
+ out = capsys.readouterr().out
40
+
41
+ assert rc == 0
42
+ assert "Removed 1 completed todo(s)" in out
@@ -0,0 +1,32 @@
1
+ import json
2
+
3
+ from qbit.storage import TodoStorage
4
+ from qbit.todo import TodoItem
5
+
6
+
7
+ def test_load_returns_empty_when_file_missing(tmp_path):
8
+ storage = TodoStorage(tmp_path / "missing.json")
9
+
10
+ assert storage.load() == []
11
+
12
+
13
+ def test_save_and_load_round_trip(tmp_path):
14
+ file_path = tmp_path / "todos.json"
15
+ storage = TodoStorage(file_path)
16
+
17
+ storage.save([TodoItem(id=1, text="buy milk", done=False, created_at="ts")])
18
+
19
+ loaded = storage.load()
20
+ assert len(loaded) == 1
21
+ assert loaded[0].id == 1
22
+ assert loaded[0].text == "buy milk"
23
+
24
+
25
+ def test_save_writes_valid_json(tmp_path):
26
+ file_path = tmp_path / "todos.json"
27
+ storage = TodoStorage(file_path)
28
+
29
+ storage.save([TodoItem(id=3, text="x", done=True, created_at="now")])
30
+
31
+ parsed = json.loads(file_path.read_text(encoding="utf-8"))
32
+ assert parsed[0]["id"] == 3
@@ -0,0 +1,43 @@
1
+ from qbit.todo import TodoItem, TodoList
2
+
3
+
4
+ def test_add_assigns_incrementing_ids():
5
+ todo_list = TodoList()
6
+
7
+ one = todo_list.add("one")
8
+ two = todo_list.add("two")
9
+
10
+ assert one.id == 1
11
+ assert two.id == 2
12
+
13
+
14
+ def test_mark_done_updates_state():
15
+ todo_list = TodoList([TodoItem(id=7, text="task")])
16
+
17
+ item = todo_list.mark_done(7)
18
+
19
+ assert item.done is True
20
+
21
+
22
+ def test_delete_removes_item():
23
+ todo_list = TodoList([TodoItem(id=1, text="a"), TodoItem(id=2, text="b")])
24
+
25
+ removed = todo_list.delete(1)
26
+
27
+ assert removed.id == 1
28
+ assert [item.id for item in todo_list.items()] == [2]
29
+
30
+
31
+ def test_clear_completed_removes_only_done_items():
32
+ todo_list = TodoList(
33
+ [
34
+ TodoItem(id=1, text="a", done=True),
35
+ TodoItem(id=2, text="b", done=False),
36
+ TodoItem(id=3, text="c", done=True),
37
+ ]
38
+ )
39
+
40
+ removed = todo_list.clear_completed()
41
+
42
+ assert removed == 2
43
+ assert [item.id for item in todo_list.items()] == [2]