Skip to content

Commit 2448777

Browse files
committed
Refactor and optimize commands
1 parent 8f9c7cf commit 2448777

12 files changed

Lines changed: 228 additions & 99 deletions

File tree

aiogram_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.0.0"
1+
__version__ = "1.0.1"

aiogram_cli/develop/_dispatcher.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import inspect
2+
from typing import Any
3+
4+
from aiogram import Bot, Dispatcher
5+
6+
from aiogram_cli.develop._resolver import logger, resolve_import
7+
8+
9+
async def do_skip_updates(bot: Bot):
10+
logger.warning("Skipping updates")
11+
await bot.delete_webhook(drop_pending_updates=True)
12+
13+
14+
def resolve_dispatcher(*, target: str) -> Dispatcher:
15+
return resolve_import(target=target, possible_names={"dp", "dispatcher"})
16+
17+
18+
async def prepare_dispatcher(*, dispatcher: Any, skip_updates: bool) -> Dispatcher:
19+
if callable(dispatcher):
20+
dispatcher = dispatcher()
21+
22+
if inspect.isawaitable(dispatcher):
23+
dispatcher = await dispatcher
24+
25+
if not isinstance(dispatcher, Dispatcher):
26+
msg = f"Dispatcher must be instance of aiogram.Dispatcher, got: {type(dispatcher)}"
27+
raise ValueError(msg)
28+
29+
if skip_updates:
30+
dispatcher.startup.register(do_skip_updates)
31+
32+
return dispatcher
33+
34+
35+
def simplified_prepare_dispatcher(*, dispatcher: Any, skip_updates: bool) -> Dispatcher:
36+
if callable(dispatcher):
37+
dispatcher = dispatcher()
38+
39+
if not isinstance(dispatcher, Dispatcher):
40+
msg = f"Dispatcher must be instance of aiogram.Dispatcher, got: {type(dispatcher)}"
41+
raise ValueError(msg)
42+
43+
if skip_updates:
44+
dispatcher.startup.register(do_skip_updates)
45+
46+
return dispatcher

aiogram_cli/develop/_polling.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import asyncio
22
import sys
33
from logging import basicConfig
4+
from typing import Any
45

56
import aiogram
67
import click
7-
from aiogram import Bot, Dispatcher
8+
from aiogram import Bot
89
from aiogram.enums import ParseMode
910

10-
from aiogram_cli.develop._resolver import LoadError, resolve_dispatcher
11+
from aiogram_cli.develop._resolver import LoadError
12+
from aiogram_cli.develop._dispatcher import prepare_dispatcher, resolve_dispatcher
1113

1214

1315
def start_polling(
@@ -29,26 +31,29 @@ def start_polling(
2931

3032
click.echo("Loading application...")
3133
try:
32-
dispatcher = resolve_dispatcher(target=target, skip_updates=skip_updates)
34+
dispatcher = resolve_dispatcher(target=target)
3335
except LoadError as e:
3436
click.echo(str(e), err=True)
3537
sys.exit(2)
3638

39+
# TODO: Add possibility to specify bot instance
3740
bot = Bot(
3841
token=token,
3942
parse_mode=parse_mode,
4043
disable_web_page_preview=disable_web_page_preview,
4144
protect_content=protect_content,
4245
)
43-
click.echo("Start polling")
44-
asyncio.run(_polling(bot=bot, dispatcher=dispatcher))
46+
47+
asyncio.run(_polling(bot=bot, dispatcher=dispatcher, skip_updates=skip_updates))
4548
return 0
4649

4750

4851
async def _polling(
4952
*,
50-
dispatcher: Dispatcher,
53+
dispatcher: Any,
5154
bot: Bot,
55+
skip_updates: bool = False,
5256
):
57+
dispatcher = await prepare_dispatcher(dispatcher=dispatcher, skip_updates=skip_updates)
5358
async with bot.context() as bot:
5459
await dispatcher.start_polling(bot, close_bot_session=False)

aiogram_cli/develop/_resolver.py

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import importlib
22
import logging
3-
4-
from aiogram import Bot, Dispatcher
3+
from typing import Any
54

65
logger = logging.getLogger(__name__)
76

@@ -10,42 +9,24 @@ class LoadError(Exception):
109
pass
1110

1211

13-
def resolve_dispatcher(*, target: str, skip_updates: bool) -> Dispatcher:
12+
def resolve_import(*, target: str, possible_names: set[str] | None = None) -> Any:
1413
module_name, _, target_name = target.partition(":")
15-
1614
try:
1715
module = importlib.import_module(module_name)
18-
except ImportError:
19-
msg = f"Can't import module: {module_name!r}"
20-
raise LoadError(msg) from None
21-
16+
except ImportError as e:
17+
msg = f"Can't import module: {module_name!r} ({type(e).__name__}: {e})"
18+
raise LoadError(msg) from e
2219
if not target_name:
23-
for name in ["dp", "dispatcher"]:
20+
for name in possible_names:
2421
if hasattr(module, name):
2522
target_name = name
2623
break
2724
else:
28-
msg = f"Can't find dispatcher in module: {module_name!r}"
25+
msg = f"Can't find target name in module: {module_name!r} (checked names: {possible_names})"
2926
raise LoadError(msg)
3027

31-
dispatcher = getattr(module, target_name, None)
32-
if not dispatcher:
33-
msg = f"Can't find dispatcher in module: {module_name!r} by name: {target_name!r}"
34-
raise LoadError(msg)
35-
36-
if callable(dispatcher):
37-
dispatcher = dispatcher()
38-
39-
if not isinstance(dispatcher, Dispatcher):
40-
msg = f"Dispatcher must be instance of aiogram.Dispatcher, got: {type(dispatcher)}"
41-
raise LoadError(msg)
42-
43-
if skip_updates:
44-
dispatcher.startup.register(_skip_updates)
45-
46-
return dispatcher
47-
48-
49-
async def _skip_updates(bot: Bot):
50-
logger.warning("Skipping updates")
51-
await bot.delete_webhook(drop_pending_updates=True)
28+
try:
29+
return getattr(module, target_name, None)
30+
except AttributeError as e:
31+
msg = f"Can't find target name in module: {module_name!r} by name: {target_name!r}"
32+
raise LoadError(msg) from e

aiogram_cli/develop/_webhook.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
1010
from aiohttp.web import Application, run_app
1111

12-
from aiogram_cli.develop._resolver import LoadError, resolve_dispatcher
12+
from aiogram_cli.develop._resolver import LoadError
13+
from aiogram_cli.develop._dispatcher import resolve_dispatcher, simplified_prepare_dispatcher
1314

1415
logger = logging.getLogger(__name__)
1516

@@ -38,11 +39,12 @@ def start_webhook(
3839

3940
click.echo("Loading application...")
4041
try:
41-
dispatcher = resolve_dispatcher(target=target, skip_updates=skip_updates)
42+
dispatcher = resolve_dispatcher(target=target)
4243
except LoadError as e:
4344
click.echo(str(e), err=True)
4445
sys.exit(2)
4546

47+
dispatcher = simplified_prepare_dispatcher(dispatcher=dispatcher, skip_updates=skip_updates)
4648
bot = Bot(
4749
token=token,
4850
parse_mode=parse_mode,

aiogram_cli/develop/plugin.py

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
1-
from logging import getLevelNamesMapping
1+
from contextlib import suppress
22
from os import getenv
33
from pathlib import Path
44

55
import click
6+
from aiogram import Bot
67
from aiogram.enums import ParseMode
7-
from click import Choice
8+
from click import Choice, style
89
from watchfiles import run_process
910
from watchfiles.main import FileChange
1011

1112
from aiogram_cli.develop._polling import start_polling
1213
from aiogram_cli.develop._webhook import start_webhook
14+
from aiogram_cli.wraps import async_command
15+
16+
LOGGING_LEVELS = [
17+
"CRITICAL",
18+
"FATAL",
19+
"ERROR",
20+
"WARN",
21+
"WARNING",
22+
"INFO",
23+
"DEBUG",
24+
"NOTSET",
25+
]
1326

1427

1528
@click.group("run")
@@ -53,7 +66,7 @@ def develop_runner():
5366
@click.option(
5467
"--log-level",
5568
"-l",
56-
type=Choice(list(getLevelNamesMapping().keys())),
69+
type=Choice(LOGGING_LEVELS, case_sensitive=False),
5770
help="Logging level",
5871
)
5972
@click.option(
@@ -103,15 +116,16 @@ def command_polling(
103116
"log_level": log_level,
104117
"log_format": log_format,
105118
}
106-
if reload:
107-
run_process(
108-
*reload_path,
109-
target=start_polling,
110-
kwargs=kwargs,
111-
callback=_changes_detected,
112-
)
113-
else:
114-
return start_polling(**kwargs)
119+
with suppress(KeyboardInterrupt):
120+
if reload:
121+
run_process(
122+
*reload_path,
123+
target=start_polling,
124+
kwargs=kwargs,
125+
callback=_changes_detected,
126+
)
127+
else:
128+
return start_polling(**kwargs)
115129

116130

117131
@develop_runner.command("webhook")
@@ -183,7 +197,7 @@ def command_polling(
183197
@click.option(
184198
"--log-level",
185199
"-l",
186-
type=Choice(list(getLevelNamesMapping().keys())),
200+
type=Choice(LOGGING_LEVELS, case_sensitive=False),
187201
help="Logging level",
188202
)
189203
@click.option(
@@ -247,17 +261,64 @@ def command_webhook(
247261
"log_level": log_level,
248262
"log_format": log_format,
249263
}
250-
if reload:
251-
run_process(
252-
*reload_path,
253-
target=start_webhook,
254-
kwargs=kwargs,
255-
callback=_changes_detected,
256-
)
257-
else:
258-
return start_webhook(**kwargs)
264+
with suppress(KeyboardInterrupt):
265+
if reload:
266+
run_process(
267+
*reload_path,
268+
target=start_webhook,
269+
kwargs=kwargs,
270+
callback=_changes_detected,
271+
)
272+
else:
273+
return start_webhook(**kwargs)
259274

260275

261276
def _changes_detected(changes: set[FileChange]):
262277
files = {path for change, path in changes}
263278
click.echo(f"Changes detected: {files}")
279+
280+
281+
@develop_runner.command("info")
282+
@click.option(
283+
"--token",
284+
"-t",
285+
help="Bot token",
286+
required=True,
287+
default=getenv("TELEGRAM_TOKEN"),
288+
)
289+
@async_command
290+
async def command_info(token: str):
291+
async with Bot(token=token).context() as bot:
292+
me = await bot.get_me()
293+
webhook_info = await bot.get_webhook_info()
294+
295+
click.echo(style("Bot info:", fg="green", bold=True))
296+
click.echo(" " + style("ID: ", bold=True, fg="green") + str(me.id))
297+
click.echo(" " + style("Username: ", bold=True, fg="green") + f"@{me.username} (https://t.me/{me.username})")
298+
if me.is_premium:
299+
click.echo(" " + style("Premium: ", bold=True, fg="green") + "Yes")
300+
if webhook_info.url:
301+
click.echo(
302+
style("Webhook info:", fg="green", bold=True),
303+
)
304+
click.echo(" " + style("URL: ", bold=True, fg="green") + webhook_info.url)
305+
if webhook_info.allowed_updates:
306+
click.echo(" " + style("Allowed updates: ", bold=True, fg="green") + ", ".join(webhook_info.allowed_updates))
307+
if webhook_info.ip_address:
308+
click.echo(" " + style("IP address: ", bold=True, fg="green") + webhook_info.ip_address)
309+
if webhook_info.has_custom_certificate:
310+
click.echo(" " + style("Has custom certificate: ", bold=True, fg="green") + "Yes")
311+
if webhook_info.pending_update_count:
312+
click.echo(" " + style("Pending updates: ", bold=True, fg="green") + str(webhook_info.pending_update_count))
313+
if webhook_info.last_error_date:
314+
click.echo(" " + style("Last error date: ", bold=True, fg="green") + str(webhook_info.last_error_date))
315+
if webhook_info.last_error_message:
316+
click.echo(" " + style("Last error message: ", bold=True, fg="green") + webhook_info.last_error_message)
317+
if webhook_info.max_connections:
318+
click.echo(" " + style("Max connections: ", bold=True, fg="green") + str(webhook_info.max_connections))
319+
if webhook_info.last_synchronization_error_date:
320+
click.echo(
321+
" "
322+
+ style("Last synchronization error date: ", bold=True, fg="green")
323+
+ str(webhook_info.last_synchronization_error_date)
324+
)

aiogram_cli/main.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from collections import defaultdict
2+
from importlib.metadata import EntryPoint
3+
4+
import aiogram
15
import click
26
from click_plugins import with_plugins
3-
from pkg_resources import iter_entry_points
47

58
from aiogram_cli import __version__
9+
from aiogram_cli.resolver import iter_entry_points
610

711

812
@with_plugins(iter_entry_points("aiogram_cli.plugins"))
@@ -12,3 +16,44 @@
1216
@click.version_option(version=__version__, prog_name="aiogram-cli", message="%(version)s")
1317
def cli():
1418
pass
19+
20+
21+
@cli.command("version")
22+
def command_version():
23+
"""Get an application version"""
24+
click.echo(f"aiogram-cli: v{__version__}")
25+
click.echo(f"aiogram: v{aiogram.__version__}")
26+
27+
28+
@cli.command("plugins")
29+
def command_plugins():
30+
"""Get a list of available plugins"""
31+
entrypoint: EntryPoint
32+
broken_libs = defaultdict(set)
33+
for index, entrypoint in enumerate(iter_entry_points("aiogram_cli.plugins"), start=1):
34+
click.echo(f"{index:3}.", nl=False)
35+
36+
dist = entrypoint.dist
37+
try:
38+
plugin = entrypoint.load()
39+
except Exception as e:
40+
broken_libs[dist.name].add(entrypoint.name)
41+
click.secho(f" {entrypoint.name}", fg="red", err=True, nl=False)
42+
click.echo(f" (by {dist.name} v{dist.version})", err=True, nl=False)
43+
click.echo(f" is unusable in due to {type(e).__name__}: {e}")
44+
continue
45+
46+
description = plugin.__doc__.split("\n")[0] if plugin.__doc__ else ""
47+
48+
click.secho(f" {entrypoint.name}", fg="green", nl=False)
49+
click.echo(f" (by {dist.name} v{dist.version})", nl=False)
50+
if description:
51+
click.echo(f": {description}")
52+
else:
53+
click.echo()
54+
55+
if broken_libs:
56+
click.echo()
57+
click.secho("Broken libraries:", fg="red", err=True)
58+
for lib, plugins in broken_libs.items():
59+
click.echo(f" {lib} ({', '.join(plugins)})")

0 commit comments

Comments
 (0)