qbit-cli 0.1.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.
qbit/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """qbit package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
qbit/cli.py ADDED
@@ -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())
qbit/config.py ADDED
@@ -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
qbit/storage.py ADDED
@@ -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)
qbit/todo.py ADDED
@@ -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,10 @@
1
+ qbit/__init__.py,sha256=OfgglIGcAFrxJZG8INOUhqAEMD_kFrrJDJJIDgbiiy4,73
2
+ qbit/cli.py,sha256=AasEpN2AOFIk9zBKrosbHrQtLLVBNe9dyxxOSum0mEI,3062
3
+ qbit/config.py,sha256=AaQNEm-ZEIv8FaXQ4f9EWBgwtKm560omaVg6s5x1dJY,856
4
+ qbit/storage.py,sha256=w4fimloSxX0VsXMPaMHNN0NCRHFqVhGZnOUlWjfgGjw,1169
5
+ qbit/todo.py,sha256=Dd9auyuWAbLuuMpGEMM1TgXPMaCyHaZrQoDUYhZAgoY,2209
6
+ qbit_cli-0.1.0.dist-info/METADATA,sha256=MocPgXGml-ZfVf9iP1WGSmOOtleOLFTuTeRZ287QEVs,2048
7
+ qbit_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ qbit_cli-0.1.0.dist-info/entry_points.txt,sha256=sIjiORpO06Jn_Mk5JxT_EFy0I6z2qCP1d-eJFKpjX7g,39
9
+ qbit_cli-0.1.0.dist-info/top_level.txt,sha256=uOAbec6EXWbkbDq98NXUpTZfYs-7d-BM943WGSvTY-8,5
10
+ qbit_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qbit = qbit.cli:main
@@ -0,0 +1 @@
1
+ qbit