Skip to content

Commit 61180b5

Browse files
committed
Migrate to Cleo. Add Readme
1 parent a821cc5 commit 61180b5

13 files changed

Lines changed: 251 additions & 94 deletions

File tree

README.md

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,66 @@
11
# aiogram-cli
22

3-
Command line interface
3+
Command line interface for developers
4+
5+
Works only with [aiogram](https://github.com/aiogram/aiogram) 3.0+ (Is under development)
6+
7+
Here is only bootstrap for CLI interface with extensions based on [pkg_resources](https://setuptools.readthedocs.io/en/latest/pkg_resources.html)
8+
9+
## Installation
10+
11+
### From PyPi
12+
`pip install --extra-index-url https://dev-docs.aiogram.dev/simple --pre aiogram-cli`
13+
14+
### Poetry
15+
16+
Add this block to `pyproject.toml` file:
17+
```toml
18+
[[tool.poetry.source]]
19+
name = "aiogram-dev"
20+
url = "https://dev-docs.aiogram.dev/simple"
21+
secondary = true
22+
```
23+
24+
And then run: `poetry add -D aiogram-cli`
25+
26+
## Extensions
27+
28+
- `aiogram_cli_generator` (WIP) - Project files generator based on pre-defined cookiecutter templates
29+
- `aiogram_cli_executor` (WIP) - Executor for bots
30+
- ...
31+
32+
## Usage
33+
34+
Just run in terminal `aiogram-cli` (Or alias - `aiogram`) and see what you can do with it.
35+
36+
## Example
37+
38+
![main interface](assets/cli.png)
39+
40+
![commands](assets/commands.png)
41+
42+
43+
## Writing extensions
44+
45+
Any **aiogram-cli** extension package should provide an entry point like this:
46+
```
47+
[aiogram_cli.plugins]
48+
my_extension = my_package.module:MyCommand
49+
```
50+
51+
Or with poetry like this:
52+
```toml
53+
[tool.poetry.plugins."aiogram_cli.plugins"]
54+
"builtin-about" = "aiogram_cli.commands.about:AboutCommand"
55+
"builtin-plugins" = "aiogram_cli.commands.plugins:PluginsListCommand"
56+
```
57+
58+
This application is based on [cleo](https://cleo.readthedocs.io/en/latest/) framework and that mean all plugins should be one of:
59+
1. subclass of `cleo.Command`
60+
1. instance of `cleo.Command`
61+
1. sequence of subclasses or instances of `cleo.Command`
62+
1. callable which accepts `app: cleo.Application` and returns any of 1-3 formats
63+
64+
Examples:
65+
[aiogram_cli.commands.about](aiogram_cli/commands/about.py)
66+
[aiogram_cli.commands.plugins](aiogram_cli/commands/plugins.py)

aiogram_cli/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from aiogram_cli.main import main
2+
3+
if __name__ == "__main__":
4+
main()

aiogram_cli/commands/about.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Optional
2+
3+
import aiogram
4+
from cleo import Command
5+
6+
import aiogram_cli
7+
8+
9+
class AboutCommand(Command):
10+
name = "about"
11+
description = "Get application info"
12+
help = description
13+
14+
def handle(self) -> Optional[int]:
15+
self.line(f"aiogram-cli: <comment>v{aiogram_cli.__version__}</comment>")
16+
self.line(f"aiogram: <comment>v{aiogram.__version__}</comment>")
17+
18+
return 0

aiogram_cli/commands/builtin.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

aiogram_cli/commands/plugins.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
1-
import typer
1+
from typing import Optional
22

3-
from aiogram_cli.loader import load_plugins_list
3+
from cleo import Command
4+
from pkg_resources import EntryPoint
45

6+
from aiogram_cli.loader import ExtensionsLoader
57

6-
def plugins() -> None:
7-
"""
8-
Get plugins list
9-
"""
10-
for index, entry_point in enumerate(load_plugins_list(), start=1):
11-
typer.echo(f" {index}. {entry_point}")
8+
9+
class PluginsListCommand(Command):
10+
name = "plugins"
11+
description = "Get installed plugins list"
12+
help = description
13+
hidden = True
14+
15+
def handle(self) -> Optional[int]:
16+
loader = ExtensionsLoader()
17+
for index, entry_point in enumerate(loader.iter_entry_points(), start=1):
18+
line = f"{index:>3}. {self.format_entry_point(entry_point)}"
19+
self.line(line)
20+
21+
return 0
22+
23+
@classmethod
24+
def format_entry_point(cls, entry_point: EntryPoint) -> str:
25+
line = f"<info>{entry_point.name}</info> from <info>{entry_point.module_name}</info>"
26+
if entry_point.attrs:
27+
line += " by <info>" + ".".join(entry_point.attrs) + "</info>"
28+
if entry_point.extras:
29+
line += (
30+
" with extras [" + ", ".join(f"<info>{s}</info>" for s in entry_point.extras) + "]"
31+
)
32+
return line

aiogram_cli/commands/version.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

aiogram_cli/loader.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,44 @@
1-
from typing import Callable, Generator
1+
from typing import Any, Callable, Generator, Sequence, Type, Union
22

3-
import typer
3+
from cleo import Application, Command
44
from pkg_resources import EntryPoint, iter_entry_points
5-
from typer import Typer
65

6+
CommandType = Union[Command, Type[Command]]
7+
CommandLoaderType = Callable[[Application], Union[CommandType, Sequence[CommandType]]]
8+
CommandExtension = Union[CommandType, CommandLoaderType]
79

8-
def load_plugins_list() -> Generator[EntryPoint, None, None]:
9-
yield from iter_entry_points(group="aiogram_cli.plugins", name=None)
10+
EXTENSIONS_GROUP = "aiogram_cli.plugins"
1011

1112

12-
def resolve_entry_point(app: Typer, entry_point: EntryPoint) -> None:
13-
plugin_loader: Callable[[Typer], None] = entry_point.resolve()
14-
plugin_loader(app)
13+
class ExtensionsLoader:
14+
def __init__(self, group: str = EXTENSIONS_GROUP) -> None:
15+
self.group = group
1516

17+
def iter_entry_points(self) -> Generator[EntryPoint, None, None]:
18+
yield from iter_entry_points(group=self.group, name=None)
1619

17-
def setup_plugins(app: Typer) -> None:
18-
for entry_point in load_plugins_list():
19-
try:
20-
resolve_entry_point(app=app, entry_point=entry_point)
21-
except Exception:
22-
typer.echo("Failed to load plugin {plugin}", err=True)
23-
raise
20+
def resolve_entry_points(self) -> Generator[Any, None, None]:
21+
for entry_point in self.iter_entry_points():
22+
yield entry_point.resolve()
23+
24+
def _load_plugin(self, plugin: Any, app: Application) -> Generator[Command, None, None]:
25+
if isinstance(plugin, Command):
26+
yield plugin
27+
elif issubclass(plugin, Command):
28+
yield plugin()
29+
elif isinstance(plugin, Sequence):
30+
for item in plugin:
31+
yield from self._load_plugin(item, app=app)
32+
elif callable(plugin):
33+
plugin = plugin(app)
34+
yield from self._load_plugin(plugin, app=app)
35+
else:
36+
raise TypeError(f"{plugin} is not Command or factory")
37+
38+
def _resolve_plugins(self, app: Application) -> Generator[Command, None, None]:
39+
for plugin in self.resolve_entry_points():
40+
yield from self._load_plugin(plugin, app=app)
41+
42+
def setup(self, app: Application) -> None:
43+
for command in self._resolve_plugins(app):
44+
app.add(command)

aiogram_cli/main.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
from typing import Any
22

3-
import typer
3+
from cleo import Application
44

5-
from aiogram_cli.loader import setup_plugins
5+
from aiogram_cli import __version__
6+
from aiogram_cli.loader import ExtensionsLoader
7+
8+
9+
def get_application() -> Application:
10+
app = Application("aiogram-cli", __version__)
11+
12+
loader = ExtensionsLoader()
13+
loader.setup(app=app)
14+
15+
return app
616

717

818
def main() -> Any:
9-
app = typer.Typer()
10-
setup_plugins(app)
11-
return app()
19+
app = get_application()
20+
return app.run()

assets/cli.png

62.8 KB
Loading

assets/commands.png

22.7 KB
Loading

0 commit comments

Comments
 (0)