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.
- qbit_cli-0.1.0/PKG-INFO +103 -0
- qbit_cli-0.1.0/README.md +82 -0
- qbit_cli-0.1.0/pyproject.toml +39 -0
- qbit_cli-0.1.0/setup.cfg +4 -0
- qbit_cli-0.1.0/src/qbit/__init__.py +4 -0
- qbit_cli-0.1.0/src/qbit/cli.py +105 -0
- qbit_cli-0.1.0/src/qbit/config.py +25 -0
- qbit_cli-0.1.0/src/qbit/storage.py +38 -0
- qbit_cli-0.1.0/src/qbit/todo.py +78 -0
- qbit_cli-0.1.0/src/qbit_cli.egg-info/PKG-INFO +103 -0
- qbit_cli-0.1.0/src/qbit_cli.egg-info/SOURCES.txt +15 -0
- qbit_cli-0.1.0/src/qbit_cli.egg-info/dependency_links.txt +1 -0
- qbit_cli-0.1.0/src/qbit_cli.egg-info/entry_points.txt +2 -0
- qbit_cli-0.1.0/src/qbit_cli.egg-info/top_level.txt +1 -0
- qbit_cli-0.1.0/tests/test_cli.py +42 -0
- qbit_cli-0.1.0/tests/test_storage.py +32 -0
- qbit_cli-0.1.0/tests/test_todo.py +43 -0
qbit_cli-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
qbit_cli-0.1.0/README.md
ADDED
|
@@ -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"]
|
qbit_cli-0.1.0/setup.cfg
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())
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qbit
|
|
@@ -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]
|