kboard 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.
- kboard-0.1.0/LICENSE +21 -0
- kboard-0.1.0/PKG-INFO +110 -0
- kboard-0.1.0/README.md +64 -0
- kboard-0.1.0/pyproject.toml +40 -0
- kboard-0.1.0/src/kboard/__init__.py +0 -0
- kboard-0.1.0/src/kboard/app.py +16 -0
- kboard-0.1.0/src/kboard/commands/__init__.py +0 -0
- kboard-0.1.0/src/kboard/commands/backlog.py +25 -0
- kboard-0.1.0/src/kboard/commands/board.py +118 -0
- kboard-0.1.0/src/kboard/commands/configure.py +17 -0
- kboard-0.1.0/src/kboard/commands/task.py +130 -0
- kboard-0.1.0/src/kboard/config.py +25 -0
- kboard-0.1.0/src/kboard/enums.py +14 -0
- kboard-0.1.0/src/kboard/models.py +107 -0
- kboard-0.1.0/src/kboard/utils.py +19 -0
kboard-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Óscar Miranda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kboard-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kboard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Console-based Kanban task manager created in Python.
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Óscar Miranda
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Keywords: kanban,board,project,management,cli
|
|
28
|
+
Author: Óscar Miranda
|
|
29
|
+
Author-email: oscarmiranda3615@gmail.com
|
|
30
|
+
Requires-Python: >=3.14.2
|
|
31
|
+
Classifier: Environment :: Console
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: Intended Audience :: Information Technology
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Topic :: Software Development
|
|
38
|
+
Classifier: Topic :: Utilities
|
|
39
|
+
Requires-Dist: rich (>=14.3.2,<15.0.0)
|
|
40
|
+
Requires-Dist: sqlalchemy (>=2.0.46,<3.0.0)
|
|
41
|
+
Requires-Dist: typer (>=0.21.1,<0.22.0)
|
|
42
|
+
Project-URL: Homepage, https://github.com/OscarM3615/kboard/
|
|
43
|
+
Project-URL: Repository, https://github.com/OscarM3615/kboard/
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# kboard
|
|
47
|
+
|
|
48
|
+
Console-based Kanban task manager created in Python.
|
|
49
|
+
|
|
50
|
+
Create and manage tasks visually in your terminal using handy commands.
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- CLI based Kanban board.
|
|
55
|
+
- Easy setup.
|
|
56
|
+
- Simple commands.
|
|
57
|
+
- Structured database file.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
Install using pip running the following command in the terminal:
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
If you installed the library, you can use the CLI as a system command:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
kb COMMAND [ARGS] ...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Examples
|
|
72
|
+
|
|
73
|
+
Here are some examples of the commands available:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
# List the existing boards.
|
|
77
|
+
kb board ls
|
|
78
|
+
|
|
79
|
+
# Create a new board.
|
|
80
|
+
kb board add "Board name"
|
|
81
|
+
|
|
82
|
+
# Add a task to the backlog.
|
|
83
|
+
kb task add "Task title"
|
|
84
|
+
|
|
85
|
+
# Add a task to a board with high priority.
|
|
86
|
+
kb task add --board 1 --priority 3 "Important task"
|
|
87
|
+
|
|
88
|
+
# Move a task
|
|
89
|
+
kb task mv 2
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
Thank you for considering contributing to my project! Any pull requests are
|
|
95
|
+
welcome and greatly appreciated. If you encounter any issues while using
|
|
96
|
+
the project, please feel free to post them on the issue tracker.
|
|
97
|
+
|
|
98
|
+
To contribute to the project, please follow these steps:
|
|
99
|
+
|
|
100
|
+
1. Fork the repository.
|
|
101
|
+
2. Add a new feature or bug fix.
|
|
102
|
+
3. Commit them using descriptive messages, using
|
|
103
|
+
[conventional commits](https://www.conventionalcommits.org/) is recommended.
|
|
104
|
+
4. Submit a pull request.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
|
|
109
|
+
for more details.
|
|
110
|
+
|
kboard-0.1.0/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# kboard
|
|
2
|
+
|
|
3
|
+
Console-based Kanban task manager created in Python.
|
|
4
|
+
|
|
5
|
+
Create and manage tasks visually in your terminal using handy commands.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- CLI based Kanban board.
|
|
10
|
+
- Easy setup.
|
|
11
|
+
- Simple commands.
|
|
12
|
+
- Structured database file.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install using pip running the following command in the terminal:
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
If you installed the library, you can use the CLI as a system command:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
kb COMMAND [ARGS] ...
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Examples
|
|
27
|
+
|
|
28
|
+
Here are some examples of the commands available:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
# List the existing boards.
|
|
32
|
+
kb board ls
|
|
33
|
+
|
|
34
|
+
# Create a new board.
|
|
35
|
+
kb board add "Board name"
|
|
36
|
+
|
|
37
|
+
# Add a task to the backlog.
|
|
38
|
+
kb task add "Task title"
|
|
39
|
+
|
|
40
|
+
# Add a task to a board with high priority.
|
|
41
|
+
kb task add --board 1 --priority 3 "Important task"
|
|
42
|
+
|
|
43
|
+
# Move a task
|
|
44
|
+
kb task mv 2
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Contributing
|
|
48
|
+
|
|
49
|
+
Thank you for considering contributing to my project! Any pull requests are
|
|
50
|
+
welcome and greatly appreciated. If you encounter any issues while using
|
|
51
|
+
the project, please feel free to post them on the issue tracker.
|
|
52
|
+
|
|
53
|
+
To contribute to the project, please follow these steps:
|
|
54
|
+
|
|
55
|
+
1. Fork the repository.
|
|
56
|
+
2. Add a new feature or bug fix.
|
|
57
|
+
3. Commit them using descriptive messages, using
|
|
58
|
+
[conventional commits](https://www.conventionalcommits.org/) is recommended.
|
|
59
|
+
4. Submit a pull request.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
|
|
64
|
+
for more details.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kboard"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Console-based Kanban task manager created in Python."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Óscar Miranda", email = "oscarmiranda3615@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
license = {file = "LICENSE"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
keywords = ["kanban", "board", "project", "management", "cli"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Intended Audience :: Information Technology",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
"Topic :: Utilities",
|
|
20
|
+
]
|
|
21
|
+
requires-python = ">=3.14.2"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"typer (>=0.21.1,<0.22.0)",
|
|
24
|
+
"rich (>=14.3.2,<15.0.0)",
|
|
25
|
+
"sqlalchemy (>=2.0.46,<3.0.0)"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
homepage = "https://github.com/OscarM3615/kboard/"
|
|
30
|
+
repository = "https://github.com/OscarM3615/kboard/"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
kb = 'kboard.app:app'
|
|
34
|
+
|
|
35
|
+
[tool.poetry]
|
|
36
|
+
packages = [{include = "kboard", from = "src"}]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
40
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from .commands import backlog, board, configure, task
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(no_args_is_help=True,
|
|
7
|
+
context_settings={'help_option_names': ['-h', '--help']})
|
|
8
|
+
|
|
9
|
+
app.add_typer(configure.app)
|
|
10
|
+
app.add_typer(backlog.app)
|
|
11
|
+
app.add_typer(board.app)
|
|
12
|
+
app.add_typer(task.app)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == '__main__':
|
|
16
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from rich import print
|
|
2
|
+
from sqlalchemy import select
|
|
3
|
+
from sqlalchemy.orm import Session
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from ..config import engine
|
|
7
|
+
from ..models import Board, Task
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
app = typer.Typer()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def backlog():
|
|
15
|
+
"""Display tasks from backlog.
|
|
16
|
+
|
|
17
|
+
The backlog is composed of tasks with no assigned board.
|
|
18
|
+
"""
|
|
19
|
+
with Session(engine) as session:
|
|
20
|
+
tasks = session.execute(
|
|
21
|
+
select(Task).where(Task.board_id == None)).scalars().all()
|
|
22
|
+
|
|
23
|
+
board = Board(name='Backlog', tasks=tasks)
|
|
24
|
+
|
|
25
|
+
print(board)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich import print
|
|
5
|
+
from rich.console import Group
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from sqlalchemy import delete, select
|
|
8
|
+
from sqlalchemy.orm import Session
|
|
9
|
+
|
|
10
|
+
from ..config import engine
|
|
11
|
+
from ..models import Board, Status, Task
|
|
12
|
+
from ..utils import success, error
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(name='board', help='Manage boards.', no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command()
|
|
19
|
+
def ls():
|
|
20
|
+
"""List existing boards.
|
|
21
|
+
"""
|
|
22
|
+
with Session(engine) as session:
|
|
23
|
+
boards = session.execute(select(Board)).scalars().all()
|
|
24
|
+
|
|
25
|
+
titles = [b.inline() for b in boards]
|
|
26
|
+
|
|
27
|
+
print(Panel(Group(*titles), title='Boards', title_align='left',
|
|
28
|
+
border_style='blue'))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def add(name: Annotated[str, typer.Argument(help='Board name.')]):
|
|
33
|
+
"""Create a new board.
|
|
34
|
+
"""
|
|
35
|
+
board = Board(name=name)
|
|
36
|
+
|
|
37
|
+
with Session(engine) as session:
|
|
38
|
+
session.add(board)
|
|
39
|
+
session.commit()
|
|
40
|
+
|
|
41
|
+
success(f'Created board "{board.name}" ({board.id}).')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command(help='Rename a board.')
|
|
45
|
+
def rename(id: Annotated[int, typer.Argument(help='Board ID.')],
|
|
46
|
+
name: Annotated[str, typer.Argument(help='New name.')]):
|
|
47
|
+
"""Rename a board.
|
|
48
|
+
"""
|
|
49
|
+
with Session(engine) as session:
|
|
50
|
+
board = session.get(Board, id)
|
|
51
|
+
|
|
52
|
+
if not board:
|
|
53
|
+
return error('Board not found.')
|
|
54
|
+
|
|
55
|
+
board.name = name
|
|
56
|
+
session.commit()
|
|
57
|
+
|
|
58
|
+
success(f'Renamed board to "{board.name}".')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command()
|
|
62
|
+
def rm(id: Annotated[int, typer.Argument(help='Board ID.')],
|
|
63
|
+
force: Annotated[bool, typer.Option(
|
|
64
|
+
'--force', '-f',
|
|
65
|
+
prompt='Are you sure you want to delete the board?',
|
|
66
|
+
help='Force deletion without confirmation.')] = False):
|
|
67
|
+
"""Delete existing board.
|
|
68
|
+
|
|
69
|
+
If --force is not used, will ask for confirmation.
|
|
70
|
+
"""
|
|
71
|
+
if force:
|
|
72
|
+
with Session(engine) as session:
|
|
73
|
+
board = session.get(Board, id)
|
|
74
|
+
|
|
75
|
+
if not board:
|
|
76
|
+
return error('Board not found.')
|
|
77
|
+
|
|
78
|
+
session.delete(board)
|
|
79
|
+
session.commit()
|
|
80
|
+
|
|
81
|
+
success(f'Deleted board "{board.name}".')
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def show(id: Annotated[int, typer.Argument(help='Board ID.')]):
|
|
86
|
+
"""Display board and its tasks.
|
|
87
|
+
"""
|
|
88
|
+
with Session(engine) as session:
|
|
89
|
+
board = session.get(Board, id)
|
|
90
|
+
|
|
91
|
+
if not board:
|
|
92
|
+
return error('Board not found.')
|
|
93
|
+
|
|
94
|
+
print(board)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.command()
|
|
98
|
+
def clean(id: Annotated[int, typer.Argument(help='Board ID.')],
|
|
99
|
+
force: Annotated[bool, typer.Option(
|
|
100
|
+
'--force', '-f',
|
|
101
|
+
prompt='Are you sure you want to delete completed tasks?',
|
|
102
|
+
help='Force deletion without confirmation.', )] = False):
|
|
103
|
+
"""Delete completed tasks from a board.
|
|
104
|
+
|
|
105
|
+
If --force is not used, will ask for confirmation.
|
|
106
|
+
"""
|
|
107
|
+
if force:
|
|
108
|
+
with Session(engine) as session:
|
|
109
|
+
board = session.get(Board, id)
|
|
110
|
+
|
|
111
|
+
if not board:
|
|
112
|
+
return error('Board not found.')
|
|
113
|
+
|
|
114
|
+
session.execute(delete(Task).where(Task.board_id == board.id,
|
|
115
|
+
Task.status == Status.COMPLETED))
|
|
116
|
+
session.commit()
|
|
117
|
+
|
|
118
|
+
success('Deleted completed tasks.')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ..config import engine
|
|
4
|
+
from ..models import Base
|
|
5
|
+
from ..utils import success
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
app = typer.Typer()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command()
|
|
12
|
+
def configure():
|
|
13
|
+
"""Create and initialise data file.
|
|
14
|
+
"""
|
|
15
|
+
Base.metadata.create_all(engine)
|
|
16
|
+
|
|
17
|
+
success('Data file created successfully.')
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from ..config import STATUS_NAMES, engine
|
|
7
|
+
from ..models import Board, Priority, Status, Task
|
|
8
|
+
from ..utils import error, success
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(name='task', help='Manage tasks.', no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def add(title: Annotated[str, typer.Argument(help='Task title.')],
|
|
16
|
+
priority: Annotated[Priority, typer.Option(
|
|
17
|
+
'--priority', '-p', help='Task priority.')] = Priority.NORMAL,
|
|
18
|
+
tag: Annotated[str | None, typer.Option(
|
|
19
|
+
'--tag', '-t', help='Task custom tag.',
|
|
20
|
+
)] = None,
|
|
21
|
+
board_id: Annotated[int | None, typer.Option(
|
|
22
|
+
'--board', '-b', help='Board ID to assign to the task.')] = None):
|
|
23
|
+
"""Add a new task.
|
|
24
|
+
|
|
25
|
+
The task can be preassigned to a board using the --board option.
|
|
26
|
+
"""
|
|
27
|
+
with Session(engine) as session:
|
|
28
|
+
if board_id:
|
|
29
|
+
board = session.get(Board, board_id)
|
|
30
|
+
|
|
31
|
+
if not board:
|
|
32
|
+
return error('Board not found.')
|
|
33
|
+
else:
|
|
34
|
+
board = None
|
|
35
|
+
|
|
36
|
+
task = Task(title=title, priority=priority, tag=tag, board=board)
|
|
37
|
+
|
|
38
|
+
session.add(task)
|
|
39
|
+
session.commit()
|
|
40
|
+
|
|
41
|
+
success(f'Created task with ID {task.id}.')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def edit(id: Annotated[int, typer.Argument(help='Task ID.')],
|
|
46
|
+
title: Annotated[str | None, typer.Option(
|
|
47
|
+
'--title', help='New task title.')] = None,
|
|
48
|
+
priority: Annotated[Priority | None, typer.Option(
|
|
49
|
+
'--priority', '-p', help='New task priority.')] = None,
|
|
50
|
+
tag: Annotated[str | None, typer.Option(
|
|
51
|
+
'--tag', '-t', help='Task custom tag.',
|
|
52
|
+
)] = None,
|
|
53
|
+
board_id: Annotated[int | None, typer.Option(
|
|
54
|
+
'-b', '--board', help='New board ID (use -1 to unasign).')] = None):
|
|
55
|
+
"""Edit existing task attributes.
|
|
56
|
+
|
|
57
|
+
All parameters and options from the `add` command are optional here.
|
|
58
|
+
"""
|
|
59
|
+
with Session(engine) as session:
|
|
60
|
+
task = session.get(Task, id)
|
|
61
|
+
|
|
62
|
+
if not task:
|
|
63
|
+
return error('Task not found.')
|
|
64
|
+
|
|
65
|
+
new_attrs = {'title': title, 'priority': priority, 'tag': tag}
|
|
66
|
+
task.update(**new_attrs)
|
|
67
|
+
|
|
68
|
+
if board_id is not None:
|
|
69
|
+
if board_id != -1:
|
|
70
|
+
board = session.get(Board, board_id)
|
|
71
|
+
|
|
72
|
+
if not board:
|
|
73
|
+
return error('Board not found.')
|
|
74
|
+
else:
|
|
75
|
+
board = None
|
|
76
|
+
task.board = board
|
|
77
|
+
|
|
78
|
+
session.commit()
|
|
79
|
+
|
|
80
|
+
success(f'Updated task "{task.title}" attributes.')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command()
|
|
84
|
+
def mv(id: Annotated[int, typer.Argument(help='Task ID.')],
|
|
85
|
+
steps: Annotated[int, typer.Option(
|
|
86
|
+
'--steps', '-s', help='Number of steps to move.')] = 1):
|
|
87
|
+
"""Move a task from its current status.
|
|
88
|
+
|
|
89
|
+
To customise the direction or number of steps, use the --steps option.
|
|
90
|
+
|
|
91
|
+
To move a task backwards the steps must be negative.
|
|
92
|
+
"""
|
|
93
|
+
with Session(engine) as session:
|
|
94
|
+
task = session.get(Task, id)
|
|
95
|
+
|
|
96
|
+
if not task:
|
|
97
|
+
return error('Task not found.')
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
task.status = Status(task.status + steps)
|
|
101
|
+
except ValueError:
|
|
102
|
+
return error(f'Unable to move {steps} step(s).')
|
|
103
|
+
|
|
104
|
+
session.commit()
|
|
105
|
+
|
|
106
|
+
success(
|
|
107
|
+
f'Moved task "{task.title}" to [cyan]{STATUS_NAMES[task.status]}[/].')
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command()
|
|
111
|
+
def rm(id: Annotated[int, typer.Argument(help='Task ID.')],
|
|
112
|
+
force: Annotated[bool, typer.Option(
|
|
113
|
+
'--force', '-f',
|
|
114
|
+
prompt='Are you sure you want to delete the task?',
|
|
115
|
+
help='Force deletion without confirmation.')] = False):
|
|
116
|
+
"""Delete existing task.
|
|
117
|
+
|
|
118
|
+
If --force is not used, will ask for confirmation.
|
|
119
|
+
"""
|
|
120
|
+
if force:
|
|
121
|
+
with Session(engine) as session:
|
|
122
|
+
task = session.get(Task, id)
|
|
123
|
+
|
|
124
|
+
if not task:
|
|
125
|
+
return error('Task not found.')
|
|
126
|
+
|
|
127
|
+
session.delete(task)
|
|
128
|
+
session.commit()
|
|
129
|
+
|
|
130
|
+
success(f'Deleted task with ID {task.id}.')
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
|
|
6
|
+
from .enums import Priority, Status
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DB_PATH = Path(os.environ.get('KBOARD_HOME', Path.home())) / 'kboard.db'
|
|
10
|
+
|
|
11
|
+
STATUS_NAMES: dict[Status, str] = {
|
|
12
|
+
Status.TO_DO: 'To do',
|
|
13
|
+
Status.IN_PROGRESS: 'In progress',
|
|
14
|
+
Status.REVIEW: 'Review',
|
|
15
|
+
Status.COMPLETED: 'Completed',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
STATUS_COLOURS: dict[Status, str] = {
|
|
19
|
+
Status.TO_DO: 'white',
|
|
20
|
+
Status.IN_PROGRESS: 'blue',
|
|
21
|
+
Status.REVIEW: 'magenta',
|
|
22
|
+
Status.COMPLETED: 'green',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
engine = create_engine(f'sqlite:///{DB_PATH}')
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import overload
|
|
3
|
+
from rich import box
|
|
4
|
+
from rich.console import Group, RenderableType
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from sqlalchemy import ForeignKey
|
|
8
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
9
|
+
|
|
10
|
+
from .config import STATUS_COLOURS, STATUS_NAMES
|
|
11
|
+
from .enums import Priority, Status
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Base(DeclarativeBase):
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Board(Base):
|
|
19
|
+
__tablename__ = 'boards'
|
|
20
|
+
|
|
21
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
22
|
+
name: Mapped[str]
|
|
23
|
+
|
|
24
|
+
tasks: Mapped[list['Task']] = relationship(back_populates='board',
|
|
25
|
+
cascade='all, delete',
|
|
26
|
+
passive_deletes=True)
|
|
27
|
+
|
|
28
|
+
def __rich__(self) -> RenderableType:
|
|
29
|
+
"""Display board as a Kanban table.
|
|
30
|
+
|
|
31
|
+
:return: rich renderable object
|
|
32
|
+
"""
|
|
33
|
+
statuses = defaultdict(list)
|
|
34
|
+
|
|
35
|
+
for task in self.tasks:
|
|
36
|
+
statuses[task.status].append(task)
|
|
37
|
+
|
|
38
|
+
table = Table(title=self.name, box=box.DOUBLE, expand=True)
|
|
39
|
+
|
|
40
|
+
for s in Status:
|
|
41
|
+
table.add_column(
|
|
42
|
+
f'[{STATUS_COLOURS[s]}]{STATUS_NAMES[s]}[/] ({len(statuses[s])})',
|
|
43
|
+
ratio=1
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
table.add_row(*[Group(*statuses[s]) for s in Status])
|
|
47
|
+
|
|
48
|
+
return table
|
|
49
|
+
|
|
50
|
+
def inline(self) -> RenderableType:
|
|
51
|
+
return f'\\[[cyan]{self.id}[/]] {self.name}'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Task(Base):
|
|
55
|
+
__tablename__ = 'tasks'
|
|
56
|
+
|
|
57
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
58
|
+
title: Mapped[str]
|
|
59
|
+
priority: Mapped[Priority]
|
|
60
|
+
tag: Mapped[str] = mapped_column(default='')
|
|
61
|
+
status: Mapped[Status] = mapped_column(default=Status.TO_DO)
|
|
62
|
+
board_id: Mapped[int | None] = mapped_column(
|
|
63
|
+
ForeignKey('boards.id', ondelete='CASCADE'))
|
|
64
|
+
|
|
65
|
+
board: Mapped[Board | None] = relationship(back_populates='tasks')
|
|
66
|
+
|
|
67
|
+
def __rich__(self) -> RenderableType:
|
|
68
|
+
"""Display task as a Kanban card.
|
|
69
|
+
|
|
70
|
+
:return: rich renderable object
|
|
71
|
+
"""
|
|
72
|
+
content = self.title
|
|
73
|
+
|
|
74
|
+
if self.priority == Priority.LOW:
|
|
75
|
+
content = f'[bright_black]{content}[/]'
|
|
76
|
+
elif self.priority == Priority.HIGH:
|
|
77
|
+
content = f'[yellow]\\[!][/] {content}'
|
|
78
|
+
|
|
79
|
+
if self.tag:
|
|
80
|
+
content += f' ([cyan]{self.tag}[/])'
|
|
81
|
+
|
|
82
|
+
return Panel(content, title=str(self.id), title_align='left',
|
|
83
|
+
border_style=STATUS_COLOURS[self.status])
|
|
84
|
+
|
|
85
|
+
@overload
|
|
86
|
+
def update(self, *, title: str | None = None,
|
|
87
|
+
priority: Priority | None = None, tag: str | None = None,
|
|
88
|
+
status: Status | None = None) -> None:
|
|
89
|
+
"""Update multiple instance attributes in a single call.
|
|
90
|
+
|
|
91
|
+
To skip a field leave is as ``None``.
|
|
92
|
+
|
|
93
|
+
:param title: new title
|
|
94
|
+
:param priority: new priority
|
|
95
|
+
:param tag: new tag
|
|
96
|
+
:param status: new status
|
|
97
|
+
"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
@overload
|
|
101
|
+
def update(self, **kwargs) -> None:
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def update(self, **kwargs) -> None:
|
|
105
|
+
for attr, value in kwargs.items():
|
|
106
|
+
if value is not None:
|
|
107
|
+
setattr(self, attr, value)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from rich import print
|
|
2
|
+
from rich.panel import Panel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def success(message: str) -> None:
|
|
6
|
+
"""Display a success message.
|
|
7
|
+
|
|
8
|
+
:param message: message to display
|
|
9
|
+
"""
|
|
10
|
+
print(Panel(message, title='Success', title_align='left',
|
|
11
|
+
border_style='green'))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def error(message: str) -> None:
|
|
15
|
+
"""Display an error message.
|
|
16
|
+
|
|
17
|
+
:param message: Message to display.
|
|
18
|
+
"""
|
|
19
|
+
print(Panel(message, title='Error', title_align='left', border_style='red'))
|