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 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,14 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Priority(int, Enum):
5
+ LOW = 1
6
+ NORMAL = 2
7
+ HIGH = 3
8
+
9
+
10
+ class Status(int, Enum):
11
+ TO_DO = 1
12
+ IN_PROGRESS = 2
13
+ REVIEW = 3
14
+ COMPLETED = 4
@@ -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'))