aiogram-cmds 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,69 @@
1
+ """
2
+ aiogram_cmds - Command builder & scope manager for aiogram bots.
3
+
4
+ This package provides both simple and full customizable modes for managing
5
+ Telegram bot commands with i18n support, profiles, and multiple scope types.
6
+
7
+ Simple Mode:
8
+ from aiogram_cmds import CommandScopeManager, load_settings, build_translator_from_i18n
9
+
10
+ settings = load_settings()
11
+ translator = build_translator_from_i18n(i18n)
12
+ manager = CommandScopeManager(bot, settings=settings, translator=translator)
13
+ await manager.setup_all()
14
+
15
+ Full Customizable Mode:
16
+ from aiogram_cmds import CmdsConfig, CommandDef, ProfileDef, ScopeDef, MenuButtonDef
17
+
18
+ config = CmdsConfig(
19
+ commands={"start": CommandDef(i18n_key="start.desc")},
20
+ profiles={"guest": ProfileDef(include=["start"])},
21
+ scopes=[ScopeDef(scope="all_private_chats", profile="guest")],
22
+ )
23
+ manager = CommandScopeManager(bot, config=config, profile_resolver=my_resolver)
24
+
25
+ Auto-Setup (Recommended):
26
+ from aiogram_cmds.auto_setup import setup_commands_auto
27
+
28
+ async def on_startup():
29
+ command_manager = await setup_commands_auto(bot)
30
+ """
31
+
32
+ from .apply import apply_config
33
+ from .auto_setup import setup_commands_auto
34
+ from .builder import build_bot_commands
35
+ from .customize import CmdsConfig, CommandDef, MenuButtonDef, ProfileDef, ScopeDef
36
+ from .dynamic import set_user_profile
37
+ from .manager import CommandScopeManager
38
+ from .policy import CommandPolicy, Flags, ProfileResolver, default_policy
39
+ from .settings import CmdsSettings, load_settings
40
+ from .translator import Translator, build_translator_from_i18n, noop_translator
41
+
42
+ __version__ = "0.1.0"
43
+ __author__ = "aiogram_cmds contributors"
44
+ __description__ = "Command builder & scope manager for aiogram bots"
45
+
46
+ __all__ = [
47
+ # Simple / classic
48
+ "build_bot_commands",
49
+ "CommandScopeManager",
50
+ "Flags",
51
+ "CommandPolicy",
52
+ "default_policy",
53
+ "Translator",
54
+ "build_translator_from_i18n",
55
+ "noop_translator",
56
+ "CmdsSettings",
57
+ "load_settings",
58
+ # Full customizable
59
+ "CmdsConfig",
60
+ "CommandDef",
61
+ "ProfileDef",
62
+ "ScopeDef",
63
+ "MenuButtonDef",
64
+ "apply_config",
65
+ "set_user_profile",
66
+ "ProfileResolver",
67
+ # Auto-setup (convenience)
68
+ "setup_commands_auto",
69
+ ]
aiogram_cmds/apply.py ADDED
@@ -0,0 +1,88 @@
1
+ """
2
+ Apply full configurations to Telegram bot.
3
+ """
4
+
5
+ from collections.abc import Iterable
6
+
7
+ from aiogram import Bot
8
+ from aiogram.types import (
9
+ BotCommandScopeAllChatAdministrators,
10
+ BotCommandScopeAllGroupChats,
11
+ BotCommandScopeAllPrivateChats,
12
+ BotCommandScopeChat,
13
+ BotCommandScopeChatAdministrators,
14
+ BotCommandScopeChatMember,
15
+ BotCommandScopeDefault,
16
+ MenuButtonCommands,
17
+ MenuButtonDefault,
18
+ )
19
+
20
+ from .builders_configured import build_from_config
21
+ from .customize import CmdsConfig, ScopeDef
22
+ from .registry import CommandRegistry
23
+ from .translator import Translator
24
+
25
+
26
+ def _scope_obj(s: ScopeDef):
27
+ if s.scope == "default":
28
+ return BotCommandScopeDefault()
29
+ if s.scope == "all_private_chats":
30
+ return BotCommandScopeAllPrivateChats()
31
+ if s.scope == "all_group_chats":
32
+ return BotCommandScopeAllGroupChats()
33
+ if s.scope == "all_chat_admins":
34
+ return BotCommandScopeAllChatAdministrators()
35
+ if s.scope == "chat":
36
+ return BotCommandScopeChat(chat_id=s.chat_id)
37
+ if s.scope == "chat_admins":
38
+ return BotCommandScopeChatAdministrators(chat_id=s.chat_id)
39
+ if s.scope == "chat_member":
40
+ return BotCommandScopeChatMember(chat_id=s.chat_id, user_id=s.user_id)
41
+ raise ValueError(f"Unknown scope: {s.scope}")
42
+
43
+
44
+ def _sorted_scopes(scopes: Iterable[ScopeDef]) -> list[ScopeDef]:
45
+ rank = {
46
+ "chat_member": 0,
47
+ "chat_admins": 1,
48
+ "chat": 2,
49
+ "all_chat_admins": 3,
50
+ "all_group_chats": 4,
51
+ "all_private_chats": 5,
52
+ "default": 6,
53
+ }
54
+ return sorted(scopes, key=lambda s: rank[s.scope])
55
+
56
+
57
+ async def apply_config(
58
+ bot: Bot,
59
+ cfg: CmdsConfig,
60
+ *,
61
+ translator: Translator | None = None,
62
+ ) -> None:
63
+ """
64
+ Apply all configured scopes + menu button in order of specificity.
65
+ """
66
+ reg = CommandRegistry(cfg)
67
+ scopes = _sorted_scopes(cfg.scopes)
68
+ langs = cfg.languages or ["en"]
69
+
70
+ for s in scopes:
71
+ scope_obj = _scope_obj(s)
72
+ target_langs = s.languages or langs
73
+ names = list(reg.resolve_profile_commands(s.profile))
74
+ for lang in target_langs:
75
+ cmds = build_from_config(
76
+ reg,
77
+ names,
78
+ lang=lang,
79
+ translator=translator,
80
+ key_prefix=cfg.i18n_key_prefix,
81
+ )
82
+ await bot.set_my_commands(cmds, scope=scope_obj, language_code=lang)
83
+
84
+ # menu button (global default)
85
+ if cfg.menu_button.mode == "commands":
86
+ await bot.set_chat_menu_button(menu_button=MenuButtonCommands())
87
+ else:
88
+ await bot.set_chat_menu_button(menu_button=MenuButtonDefault())
@@ -0,0 +1,161 @@
1
+ """
2
+ Auto-setup utilities for aiogram_cmds.
3
+
4
+ This module provides convenient functions to automatically set up command management
5
+ with minimal configuration, making it easy to integrate into __main__.py.
6
+ """
7
+
8
+ import logging
9
+
10
+ from aiogram import Bot
11
+
12
+ from .customize import CmdsConfig, CommandDef, MenuButtonDef, ProfileDef, ScopeDef
13
+ from .manager import CommandScopeManager
14
+ from .policy import Flags, ProfileResolver
15
+ from .translator import build_translator_from_i18n
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ async def setup_commands_auto(
21
+ bot: Bot,
22
+ *,
23
+ languages: list[str] | None = None,
24
+ i18n_instance=None,
25
+ config: CmdsConfig | None = None,
26
+ profile_resolver: ProfileResolver | None = None,
27
+ ) -> CommandScopeManager:
28
+ """
29
+ Automatically set up command management with minimal configuration.
30
+
31
+ This function provides a simple way to set up commands in __main__.py
32
+ with sensible defaults and automatic i18n integration.
33
+
34
+ Args:
35
+ bot: Telegram Bot instance
36
+ languages: List of supported languages (default: ["en"])
37
+ i18n_instance: aiogram i18n instance (optional, will try to import from common locations)
38
+ config: Custom CmdsConfig (optional, will create default if not provided)
39
+ profile_resolver: Custom ProfileResolver (optional, will create default if not provided)
40
+
41
+ Returns:
42
+ CommandScopeManager instance ready to use
43
+
44
+ Example:
45
+ # In __main__.py
46
+ from aiogram_cmds.auto_setup import setup_commands_auto
47
+
48
+ async def on_startup(dp, app_context=None):
49
+ command_manager = await setup_commands_auto(bot)
50
+ # Commands are automatically set up!
51
+ """
52
+ # Default languages
53
+ if languages is None:
54
+ languages = ["en"]
55
+
56
+ # Try to get i18n instance if not provided
57
+ if i18n_instance is None:
58
+ i18n_instance = _try_import_i18n()
59
+
60
+ # Build translator
61
+ translator = build_translator_from_i18n(i18n_instance) if i18n_instance else None
62
+
63
+ # Create default config if not provided
64
+ if config is None:
65
+ config = create_default_config(languages)
66
+
67
+ # Create default profile resolver if not provided
68
+ if profile_resolver is None:
69
+ profile_resolver = create_default_profile_resolver()
70
+
71
+ # Create and setup command manager
72
+ manager = CommandScopeManager(
73
+ bot,
74
+ config=config,
75
+ translator=translator,
76
+ profile_resolver=profile_resolver,
77
+ )
78
+
79
+ # Setup all command scopes and menu button
80
+ await manager.setup_all()
81
+
82
+ logger.info(
83
+ f"✅ Command management set up with {len(config.commands)} commands and {len(config.profiles)} profiles"
84
+ )
85
+
86
+ return manager
87
+
88
+
89
+ def _try_import_i18n():
90
+ """Try to import i18n from common locations."""
91
+ # Common aiogram i18n import patterns
92
+ import_patterns = [
93
+ "bot.core.i18n.i18n",
94
+ "bot.i18n.i18n",
95
+ "i18n.i18n",
96
+ "bot.core.i18n",
97
+ "bot.i18n",
98
+ "i18n",
99
+ ]
100
+
101
+ for pattern in import_patterns:
102
+ try:
103
+ module_parts = pattern.split(".")
104
+ module = __import__(pattern, fromlist=[module_parts[-1]])
105
+ # Try to get the i18n instance
106
+ if hasattr(module, "i18n"):
107
+ return module.i18n
108
+ elif hasattr(module, "gettext"):
109
+ return module
110
+ except ImportError:
111
+ continue
112
+
113
+ logger.debug(
114
+ "Could not find i18n instance, commands will use fallback descriptions"
115
+ )
116
+ return None
117
+
118
+
119
+ def create_default_config(languages: list[str]) -> CmdsConfig:
120
+ """
121
+ Create a default configuration with basic commands.
122
+
123
+ This provides a sensible starting point for most bots.
124
+ """
125
+ return CmdsConfig(
126
+ languages=languages,
127
+ fallback_language=languages[0] if languages else "en",
128
+ i18n_key_prefix="cmd",
129
+ # Basic commands that most bots need
130
+ commands={
131
+ "start": CommandDef(i18n_key="start.desc"),
132
+ "help": CommandDef(i18n_key="help.desc"),
133
+ "cancel": CommandDef(i18n_key="cancel.desc"),
134
+ },
135
+ # Simple profile structure
136
+ profiles={
137
+ "guest": ProfileDef(include=["start", "help"]),
138
+ "user": ProfileDef(include=["start", "help", "cancel"]),
139
+ },
140
+ # Default scope for all private chats
141
+ scopes=[
142
+ ScopeDef(scope="all_private_chats", profile="guest"),
143
+ ],
144
+ # Menu button
145
+ menu_button=MenuButtonDef(mode="commands"),
146
+ )
147
+
148
+
149
+ def create_default_profile_resolver() -> ProfileResolver:
150
+ """
151
+ Create a default profile resolver that maps user flags to profiles.
152
+
153
+ This provides a simple guest/user distinction based on registration status.
154
+ """
155
+
156
+ def default_resolver(flags: Flags) -> str:
157
+ if flags.is_during_registration or not flags.is_registered:
158
+ return "guest"
159
+ return "user"
160
+
161
+ return default_resolver
@@ -0,0 +1,77 @@
1
+ """
2
+ Command building - construct BotCommand objects from names and i18n/inlined descriptions.
3
+
4
+ - Validates command names per Telegram constraints.
5
+ - Resolves descriptions via translator or falls back to Title Case.
6
+ - Enforces Telegram length limits (name: 1..32, description: 1..256).
7
+ """
8
+
9
+ import logging
10
+ import re
11
+
12
+ from aiogram.types import BotCommand
13
+
14
+ from .translator import Translator
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Telegram /command rules (lowercase letters, digits, underscores; must start with a letter)
19
+ _CMD_RE = re.compile(r"^[a-z][a-z0-9_]{0,31}$")
20
+
21
+
22
+ def validate_name(name: str) -> str:
23
+ """Coerce a human/provided name into a safe Telegram command name."""
24
+ n = (name or "").strip().lower().replace(" ", "_")
25
+ if not _CMD_RE.match(n):
26
+ logger.debug("Invalid command name '%s', coercing to safe form", name)
27
+ # drop leading non-letters
28
+ while n and not n[0].isalpha():
29
+ n = n[1:]
30
+ # remove invalid chars
31
+ n = re.sub(r"[^a-z0-9_]", "", n)
32
+ # clamp length
33
+ n = n[:32] if n else "cmd"
34
+ if not _CMD_RE.match(n):
35
+ n = "cmd"
36
+ return n
37
+
38
+
39
+ def build_bot_commands(
40
+ command_names: list[str],
41
+ *,
42
+ lang: str,
43
+ translator: Translator | None = None,
44
+ key_prefix: str = "cmd",
45
+ ) -> list[BotCommand]:
46
+ """
47
+ Build BotCommand objects from command names using i18n descriptions.
48
+
49
+ i18n key pattern by default: {key_prefix}.{name}.desc
50
+ """
51
+ cmds: list[BotCommand] = []
52
+
53
+ for raw in command_names:
54
+ name = validate_name(raw)
55
+
56
+ # Try to get description from i18n
57
+ desc = None
58
+ if translator:
59
+ key = f"{key_prefix}.{name}.desc"
60
+ try:
61
+ desc = translator(key, locale=lang)
62
+ except Exception: # pragma: no cover
63
+ logger.debug(
64
+ "Translator failed for key %s (lang=%s)", key, lang, exc_info=True
65
+ )
66
+
67
+ # Fallback if missing translation
68
+ if not desc:
69
+ desc = name.replace("_", " ").title()
70
+ logger.debug(
71
+ "Missing i18n for %s.%s, fallback to '%s'", key_prefix, name, desc
72
+ )
73
+
74
+ cmds.append(BotCommand(command=name[:32], description=desc[:256]))
75
+
76
+ logger.debug("Built %d commands for language '%s'", len(cmds), lang)
77
+ return cmds
@@ -0,0 +1,69 @@
1
+ """
2
+ Command builders for config-driven mode.
3
+ """
4
+
5
+ import logging
6
+
7
+ from aiogram.types import BotCommand
8
+
9
+ from .builder import validate_name
10
+ from .registry import CommandRegistry
11
+ from .translator import Translator
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def build_from_config(
17
+ reg: CommandRegistry,
18
+ names: list[str],
19
+ *,
20
+ lang: str,
21
+ translator: Translator | None,
22
+ key_prefix: str,
23
+ ) -> list[BotCommand]:
24
+ """
25
+ Build commands using CmdsConfig registry:
26
+ - inline descriptions win
27
+ - else use command.i18n_key (relative to prefix unless dotted)
28
+ - else fallback to {prefix}.{name}.desc
29
+ - else Title Case fallback
30
+ """
31
+ items: list[BotCommand] = []
32
+ for raw in names:
33
+ name = validate_name(raw)
34
+ desc = None
35
+ c = reg.get_command(name)
36
+
37
+ # 1) inline descriptions
38
+ if c and c.descriptions:
39
+ desc = c.descriptions.get(lang)
40
+
41
+ # 2) specified i18n key
42
+ if not desc and c and c.i18n_key and translator:
43
+ i18n_key = c.i18n_key if "." in c.i18n_key else f"{key_prefix}.{c.i18n_key}"
44
+ try:
45
+ desc = translator(i18n_key, locale=lang)
46
+ except Exception:
47
+ logger.debug(
48
+ "Translator failed for key %s (lang=%s)",
49
+ i18n_key,
50
+ lang,
51
+ exc_info=True,
52
+ )
53
+
54
+ # 3) default i18n key {prefix}.{name}.desc
55
+ if not desc and translator:
56
+ key = f"{key_prefix}.{name}.desc"
57
+ try:
58
+ desc = translator(key, locale=lang)
59
+ except Exception:
60
+ logger.debug(
61
+ "Translator failed for key %s (lang=%s)", key, lang, exc_info=True
62
+ )
63
+
64
+ # 4) fallback
65
+ if not desc:
66
+ desc = name.replace("_", " ").title()
67
+
68
+ items.append(BotCommand(command=name[:32], description=desc[:256]))
69
+ return items
@@ -0,0 +1,67 @@
1
+ """
2
+ Full customizable configuration models for aiogram_cmds.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ ScopeType = Literal[
12
+ "default",
13
+ "all_private_chats",
14
+ "all_group_chats",
15
+ "all_chat_admins",
16
+ "chat",
17
+ "chat_admins",
18
+ "chat_member",
19
+ ]
20
+
21
+
22
+ class CommandDef(BaseModel):
23
+ model_config = ConfigDict(extra="ignore")
24
+ # Either use i18n_key OR inline descriptions per language
25
+ i18n_key: str | None = None
26
+ descriptions: dict[str, str] = Field(default_factory=dict)
27
+ tags: list[str] = Field(default_factory=list)
28
+
29
+
30
+ class ProfileDef(BaseModel):
31
+ model_config = ConfigDict(extra="ignore")
32
+ include: list[str] = Field(default_factory=list)
33
+ exclude: list[str] = Field(default_factory=list)
34
+
35
+
36
+ class ScopeDef(BaseModel):
37
+ model_config = ConfigDict(extra="ignore")
38
+ scope: ScopeType
39
+ profile: str
40
+ chat_id: int | None = None
41
+ user_id: int | None = None
42
+ languages: list[str] = Field(default_factory=list) # empty = use global languages
43
+
44
+
45
+ class MenuButtonDef(BaseModel):
46
+ model_config = ConfigDict(extra="ignore")
47
+ mode: Literal["commands", "default"] = "commands"
48
+
49
+
50
+ class CmdsConfig(BaseModel):
51
+ """
52
+ Full customizable configuration:
53
+ - languages, fallback language, i18n key prefix
54
+ - commands registry (with i18n_key or inline descriptions)
55
+ - profiles: named sets of commands (include/exclude)
56
+ - scopes: where to apply which profile
57
+ - menu button config
58
+ """
59
+
60
+ model_config = ConfigDict(extra="ignore")
61
+ languages: list[str] = ["en"]
62
+ fallback_language: str = "en"
63
+ i18n_key_prefix: str = "cmd"
64
+ commands: dict[str, CommandDef] = Field(default_factory=dict)
65
+ profiles: dict[str, ProfileDef] = Field(default_factory=dict)
66
+ scopes: list[ScopeDef] = Field(default_factory=list)
67
+ menu_button: MenuButtonDef = Field(default_factory=MenuButtonDef)
@@ -0,0 +1,39 @@
1
+ """
2
+ Dynamic command management for runtime profile changes.
3
+ """
4
+
5
+ from aiogram import Bot
6
+ from aiogram.types import BotCommandScopeChat, BotCommandScopeChatMember
7
+
8
+ from .builders_configured import build_from_config
9
+ from .customize import CmdsConfig
10
+ from .registry import CommandRegistry
11
+ from .translator import Translator
12
+
13
+
14
+ async def set_user_profile(
15
+ bot: Bot,
16
+ cfg: CmdsConfig,
17
+ *,
18
+ translator: Translator | None,
19
+ user_id: int,
20
+ chat_id: int | None,
21
+ profile: str,
22
+ language: str,
23
+ ) -> None:
24
+ """
25
+ Dynamically apply a profile to a user (optionally in a specific chat).
26
+ """
27
+ reg = CommandRegistry(cfg)
28
+ names = list(reg.resolve_profile_commands(profile))
29
+ cmds = build_from_config(
30
+ reg, names, lang=language, translator=translator, key_prefix=cfg.i18n_key_prefix
31
+ )
32
+
33
+ scope = (
34
+ BotCommandScopeChat(chat_id=user_id)
35
+ if chat_id is None
36
+ else BotCommandScopeChatMember(chat_id=chat_id, user_id=user_id)
37
+ )
38
+
39
+ await bot.set_my_commands(cmds, scope=scope, language_code=language)
@@ -0,0 +1,148 @@
1
+ """
2
+ High-level manager that supports:
3
+ 1) Simple mode (CmdsSettings + CommandPolicy -> list of command names)
4
+ 2) Full customizable mode (CmdsConfig + ProfileResolver -> profiles & scopes)
5
+
6
+ Use whichever you provide. If both are provided, config mode is used for setup_all(),
7
+ and update_user_commands() will prefer the profile resolver if present; otherwise falls
8
+ back to policy-based names.
9
+ """
10
+
11
+ import logging
12
+ from typing import TYPE_CHECKING, Optional
13
+
14
+ from aiogram import Bot
15
+
16
+ from .apply import apply_config
17
+ from .dynamic import set_user_profile
18
+ from .policy import CommandPolicy, Flags, ProfileResolver, default_policy
19
+ from .setters import (
20
+ set_user_commands_by_flags,
21
+ setup_all_command_scopes,
22
+ )
23
+ from .settings import CmdsSettings
24
+ from .translator import Translator
25
+
26
+ if TYPE_CHECKING:
27
+ from .customize import CmdsConfig
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class CommandScopeManager:
33
+ def __init__(
34
+ self,
35
+ bot: Bot,
36
+ *,
37
+ settings: CmdsSettings | None = None,
38
+ config: Optional["CmdsConfig"] = None,
39
+ translator: Translator | None = None,
40
+ policy: CommandPolicy | None = None,
41
+ profile_resolver: ProfileResolver | None = None,
42
+ ) -> None:
43
+ self.bot = bot
44
+ self.settings = settings or CmdsSettings()
45
+ self.config = config
46
+ self.translator = translator
47
+ self.policy = policy or default_policy
48
+ self.profile_resolver = profile_resolver
49
+
50
+ async def update_user_commands(
51
+ self,
52
+ user_id: int,
53
+ is_registered: bool,
54
+ has_vehicle: bool,
55
+ is_during_registration: bool = False,
56
+ user_language: str = "en",
57
+ chat_id: int | None = None,
58
+ ) -> None:
59
+ flags = Flags(
60
+ is_registered=is_registered,
61
+ has_vehicle=has_vehicle,
62
+ is_during_registration=is_during_registration,
63
+ )
64
+
65
+ try:
66
+ if self.config and self.profile_resolver:
67
+ profile = self.profile_resolver(flags)
68
+ await set_user_profile(
69
+ self.bot,
70
+ self.config,
71
+ translator=self.translator,
72
+ user_id=user_id,
73
+ chat_id=chat_id,
74
+ profile=profile,
75
+ language=user_language,
76
+ )
77
+ else:
78
+ # Classic mode: compute names via policy
79
+ await set_user_commands_by_flags(
80
+ self.bot,
81
+ user_id,
82
+ is_registered=is_registered,
83
+ has_vehicle=has_vehicle,
84
+ is_during_registration=is_during_registration,
85
+ user_language=user_language,
86
+ policy=self.policy,
87
+ translator=self.translator,
88
+ key_prefix=self.settings.i18n_key_prefix,
89
+ )
90
+ except Exception as e:
91
+ logger.error(
92
+ "Failed to update user commands for user %d: %s",
93
+ user_id,
94
+ e,
95
+ exc_info=True,
96
+ )
97
+
98
+ # Human-friendly log
99
+ if is_during_registration:
100
+ status = "registration"
101
+ elif is_registered and has_vehicle:
102
+ status = "onboarded"
103
+ elif is_registered and not has_vehicle:
104
+ status = "registered_no_vehicle"
105
+ else:
106
+ status = "guest"
107
+
108
+ logger.info(
109
+ "Updated commands for user %s: %s (registered=%s, vehicle=%s, during_registration=%s)",
110
+ user_id,
111
+ status,
112
+ is_registered,
113
+ has_vehicle,
114
+ is_during_registration,
115
+ )
116
+
117
+ async def clear_user_commands(self, user_id: int) -> None:
118
+ from aiogram.types import BotCommandScopeChat
119
+
120
+ try:
121
+ await self.bot.delete_my_commands(
122
+ scope=BotCommandScopeChat(chat_id=user_id)
123
+ )
124
+ logger.info("Cleared commands for user %s", user_id)
125
+ except Exception as e:
126
+ logger.error(
127
+ "Failed to clear commands for user %d: %s", user_id, e, exc_info=True
128
+ )
129
+
130
+ async def setup_all(self) -> None:
131
+ """
132
+ Setup all command scopes + menu button.
133
+ - If a full CmdsConfig is provided: apply_config (full customizable mode)
134
+ - Else: setup_all_command_scopes (classic mode)
135
+ """
136
+ try:
137
+ if self.config:
138
+ await apply_config(self.bot, self.config, translator=self.translator)
139
+ else:
140
+ await setup_all_command_scopes(
141
+ self.bot,
142
+ languages=tuple(self.settings.languages),
143
+ translator=self.translator,
144
+ key_prefix=self.settings.i18n_key_prefix,
145
+ menu_button=self.settings.menu_button,
146
+ )
147
+ except Exception as e:
148
+ logger.error("Failed to setup all commands: %s", e, exc_info=True)
aiogram_cmds/policy.py ADDED
@@ -0,0 +1,38 @@
1
+ """
2
+ Policies & flags:
3
+ - CommandPolicy: returns a list of command names from user Flags.
4
+ - ProfileResolver: returns a profile name (for config-driven mode).
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Protocol
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Flags:
13
+ """User flags that determine which commands are available."""
14
+
15
+ is_registered: bool
16
+ has_vehicle: bool
17
+ is_during_registration: bool = False
18
+
19
+
20
+ class CommandPolicy(Protocol):
21
+ """Protocol for command policy functions."""
22
+
23
+ def __call__(self, flags: Flags) -> list[str]: ...
24
+
25
+
26
+ class ProfileResolver(Protocol):
27
+ """Resolve a profile name for a given user Flags (for CmdsConfig mode)."""
28
+
29
+ def __call__(self, flags: Flags) -> str: ...
30
+
31
+
32
+ def default_policy(flags: Flags) -> list[str]:
33
+ """
34
+ Minimal, project-agnostic command policy.
35
+ """
36
+ if flags.is_during_registration or not flags.is_registered:
37
+ return ["start", "cancel"]
38
+ return ["start", "help", "cancel", "profile", "settings"]
@@ -0,0 +1,21 @@
1
+ """
2
+ Command registry for managing profiles and commands in CmdsConfig.
3
+ """
4
+
5
+ from .customize import CmdsConfig, CommandDef, ProfileDef
6
+
7
+
8
+ class CommandRegistry:
9
+ def __init__(self, cfg: CmdsConfig):
10
+ self.cfg = cfg
11
+
12
+ def resolve_profile_commands(self, profile: str) -> list[str]:
13
+ p: ProfileDef | None = self.cfg.profiles.get(profile)
14
+ if not p:
15
+ return []
16
+ keep = [n for n in p.include if n in self.cfg.commands]
17
+ exclude = set(p.exclude)
18
+ return [n for n in keep if n not in exclude]
19
+
20
+ def get_command(self, name: str) -> CommandDef | None:
21
+ return self.cfg.commands.get(name)
@@ -0,0 +1,92 @@
1
+ """
2
+ Simple (classic) setters for per-user and global scopes.
3
+ """
4
+
5
+ import logging
6
+ from collections.abc import Iterable
7
+
8
+ from aiogram import Bot
9
+ from aiogram.types import (
10
+ BotCommandScopeAllPrivateChats,
11
+ BotCommandScopeChat,
12
+ BotCommandScopeDefault,
13
+ MenuButtonCommands,
14
+ MenuButtonDefault,
15
+ )
16
+
17
+ from .builder import build_bot_commands
18
+ from .policy import CommandPolicy, Flags, default_policy
19
+ from .translator import Translator
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ async def set_user_commands_by_flags(
25
+ bot: Bot,
26
+ user_id: int,
27
+ *,
28
+ is_registered: bool,
29
+ has_vehicle: bool,
30
+ is_during_registration: bool = False,
31
+ user_language: str = "en",
32
+ policy: CommandPolicy = default_policy,
33
+ translator: Translator | None = None,
34
+ key_prefix: str = "cmd",
35
+ ) -> None:
36
+ names = policy(
37
+ Flags(
38
+ is_registered=is_registered,
39
+ has_vehicle=has_vehicle,
40
+ is_during_registration=is_during_registration,
41
+ )
42
+ )
43
+ commands = build_bot_commands(
44
+ names, lang=user_language, translator=translator, key_prefix=key_prefix
45
+ )
46
+ # Per-chat override (user's private chat)
47
+ await bot.set_my_commands(
48
+ commands=commands,
49
+ scope=BotCommandScopeChat(chat_id=user_id),
50
+ language_code=user_language,
51
+ )
52
+
53
+
54
+ async def clear_user_commands(bot: Bot, user_id: int) -> None:
55
+ # Delete per-chat override (falls back to broader scopes)
56
+ await bot.delete_my_commands(scope=BotCommandScopeChat(chat_id=user_id))
57
+
58
+
59
+ async def setup_all_command_scopes(
60
+ bot: Bot,
61
+ *,
62
+ languages: tuple[str, ...],
63
+ translator: Translator | None = None,
64
+ key_prefix: str = "cmd",
65
+ menu_button: str = "commands",
66
+ default_commands: Iterable[str] | None = None,
67
+ ) -> None:
68
+ """
69
+ Configure default/global command sets and the menu button for classic mode.
70
+ """
71
+ base_names = list(default_commands or ["start", "help"])
72
+
73
+ # default scope without language as a fallback
74
+ fallback_cmds = build_bot_commands(
75
+ base_names, lang=languages[0], translator=translator, key_prefix=key_prefix
76
+ )
77
+ await bot.set_my_commands(fallback_cmds, scope=BotCommandScopeDefault())
78
+
79
+ # language-specific sets for private chats (most common case)
80
+ for lang in languages:
81
+ cmds = build_bot_commands(
82
+ base_names, lang=lang, translator=translator, key_prefix=key_prefix
83
+ )
84
+ await bot.set_my_commands(
85
+ cmds, scope=BotCommandScopeAllPrivateChats(), language_code=lang
86
+ )
87
+
88
+ # Menu button setup
89
+ if menu_button == "commands":
90
+ await bot.set_chat_menu_button(menu_button=MenuButtonCommands())
91
+ else:
92
+ await bot.set_chat_menu_button(menu_button=MenuButtonDefault())
@@ -0,0 +1,54 @@
1
+ """
2
+ Standalone settings for aiogram_cmds package.
3
+
4
+ This module provides settings dataclass and configuration loading
5
+ from pyproject.toml, making the library configurable without hardcoded values.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from pathlib import Path
12
+
13
+ import tomllib
14
+ from pydantic import BaseModel, ConfigDict, field_validator
15
+
16
+
17
+ class CmdsSettings(BaseModel):
18
+ """Configuration settings for command management."""
19
+
20
+ model_config = ConfigDict(extra="ignore", frozen=True)
21
+ languages: tuple[str, ...] = ("en",)
22
+ fallback_language: str = "en"
23
+ i18n_key_prefix: str = "cmd"
24
+ profile: str = "default"
25
+ menu_button: str = "commands" # "commands" | "default"
26
+
27
+ @field_validator("languages", mode="before")
28
+ @classmethod
29
+ def _ensure_nonempty(cls, v):
30
+ if v is None:
31
+ return ("en",)
32
+ if isinstance(v, str):
33
+ return (v,)
34
+ if isinstance(v, (list, tuple)):
35
+ return tuple(v) if v else ("en",)
36
+ return ("en",)
37
+
38
+
39
+ def load_settings(pyproject_path: Path | None = None) -> CmdsSettings:
40
+ """
41
+ Load settings from [tool.aiogram_cmds] in pyproject.toml (if present).
42
+ """
43
+ p = pyproject_path or Path("pyproject.toml")
44
+ if not p.exists():
45
+ return CmdsSettings()
46
+ try:
47
+ data = tomllib.loads(p.read_text(encoding="utf-8"))
48
+ cfg = (data.get("tool") or {}).get("aiogram_cmds") or {}
49
+ return CmdsSettings.model_validate(cfg)
50
+ except Exception as ex: # pragma: no cover
51
+ logging.getLogger(__name__).warning(
52
+ "aiogram_cmds: failed to load settings: %s", ex
53
+ )
54
+ return CmdsSettings()
@@ -0,0 +1,33 @@
1
+ """
2
+ Translator protocol and adapters for i18n integration.
3
+ """
4
+
5
+ from typing import Protocol
6
+
7
+
8
+ class Translator(Protocol):
9
+ """Protocol for translation functions."""
10
+
11
+ def __call__(self, key: str, *, locale: str) -> str | None: ...
12
+
13
+
14
+ def build_translator_from_i18n(i18n_obj) -> Translator:
15
+ """
16
+ Build translator adapter from an aiogram i18n instance exposing gettext(key, locale=?).
17
+ """
18
+
19
+ def _t(key: str, *, locale: str) -> str | None:
20
+ try:
21
+ value = i18n_obj.gettext(key, locale=locale)
22
+ if not value or value == key:
23
+ return None
24
+ return value
25
+ except Exception:
26
+ return None
27
+
28
+ return _t
29
+
30
+
31
+ def noop_translator(key: str, *, locale: str) -> str | None:
32
+ """No-op translator that always returns None."""
33
+ return None
@@ -0,0 +1,3 @@
1
+ """Version information for aiogram-cmds."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,282 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiogram-cmds
3
+ Version: 0.1.0
4
+ Summary: Command management library for aiogram v3
5
+ Project-URL: Homepage, https://github.com/ArmanAvanesyan/aiogram-cmds
6
+ Project-URL: Repository, https://github.com/ArmanAvanesyan/aiogram-cmds
7
+ Project-URL: Issues, https://github.com/ArmanAvanesyan/aiogram-cmds/issues
8
+ Author: aiogram-cmds contributors
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications :: Chat
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: aiogram>=3.0.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: bandit>=1.7.0; extra == 'dev'
27
+ Requires-Dist: black>=23.0.0; extra == 'dev'
28
+ Requires-Dist: build>=1.0.0; extra == 'dev'
29
+ Requires-Dist: isort>=5.12.0; extra == 'dev'
30
+ Requires-Dist: pip-audit>=2.0.0; extra == 'dev'
31
+ Requires-Dist: pyright>=1.1.0; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
33
+ Requires-Dist: pytest-benchmark>=4.0.0; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
35
+ Requires-Dist: pytest-mock>=3.10.0; extra == 'dev'
36
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
37
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
38
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
39
+ Provides-Extra: redis
40
+ Requires-Dist: redis>=4.0.0; extra == 'redis'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # aiogram-cmds
44
+
45
+ [![CI](https://github.com/ArmanAvanesyan/aiogram-cmds/workflows/CI/badge.svg)](https://github.com/ArmanAvanesyan/aiogram-cmds/actions)
46
+ [![PyPI](https://img.shields.io/pypi/v/aiogram-cmds.svg)](https://pypi.org/project/aiogram-cmds/)
47
+ [![Python versions](https://img.shields.io/pypi/pyversions/aiogram-cmds.svg)](https://pypi.org/project/aiogram-cmds/)
48
+ [![License](https://img.shields.io/pypi/l/aiogram-cmds.svg)](https://pypi.org/project/aiogram-cmds/)
49
+
50
+ **Command management library for aiogram v3** - Manage Telegram bot commands with i18n support, user profiles, and dynamic scopes.
51
+
52
+ ## ✨ Features
53
+
54
+ * **Simple & Advanced Modes**: Start simple, scale to complex configurations
55
+ * **i18n Integration**: Built-in support for aiogram i18n with fallback handling
56
+ * **User Profiles**: Dynamic command sets based on user status (guest, registered, etc.)
57
+ * **Multiple Scopes**: Support for all Telegram command scopes (private, group, admin, etc.)
58
+ * **Auto-Setup**: One-call setup with sensible defaults
59
+ * **Type-Safe**: Full type hints and strict type checking
60
+ * **Production Ready**: Comprehensive testing, CI/CD, and documentation
61
+
62
+ ## 📦 Installation
63
+
64
+ ```bash
65
+ # Basic installation
66
+ pip install aiogram-cmds
67
+
68
+ # With Redis support (for multi-worker deployments)
69
+ pip install aiogram-cmds[redis]
70
+ ```
71
+
72
+ ## ⚡ Quick Start
73
+
74
+ ### Auto-Setup (Recommended)
75
+
76
+ ```python
77
+ from aiogram import Bot, Dispatcher
78
+ from aiogram_cmds.auto_setup import setup_commands_auto
79
+
80
+ async def on_startup():
81
+ # Automatically set up commands with sensible defaults
82
+ command_manager = await setup_commands_auto(bot)
83
+ # Commands are ready to use!
84
+
85
+ # Your handlers
86
+ @dp.message(Command("start"))
87
+ async def handle_start(message: Message):
88
+ await message.answer("Welcome! Use /help to see available commands.")
89
+ ```
90
+
91
+ ### Simple Mode
92
+
93
+ ```python
94
+ from aiogram import Bot, Dispatcher
95
+ from aiogram_cmds import CommandScopeManager, load_settings, build_translator_from_i18n
96
+
97
+ # Load settings from pyproject.toml or use defaults
98
+ settings = load_settings()
99
+ translator = build_translator_from_i18n(i18n) # Your aiogram i18n instance
100
+
101
+ # Create manager
102
+ manager = CommandScopeManager(bot, settings=settings, translator=translator)
103
+
104
+ # Set up all command scopes
105
+ await manager.setup_all()
106
+
107
+ # Update user commands dynamically
108
+ await manager.update_user_commands(
109
+ user_id=12345,
110
+ is_registered=True,
111
+ has_vehicle=False,
112
+ user_language="en"
113
+ )
114
+ ```
115
+
116
+ ### Advanced Mode
117
+
118
+ ```python
119
+ from aiogram_cmds import CmdsConfig, CommandDef, ProfileDef, ScopeDef, MenuButtonDef
120
+
121
+ # Define your command configuration
122
+ config = CmdsConfig(
123
+ languages=["en", "ru", "es"],
124
+ commands={
125
+ "start": CommandDef(i18n_key="start.desc"),
126
+ "help": CommandDef(i18n_key="help.desc"),
127
+ "profile": CommandDef(i18n_key="profile.desc"),
128
+ "admin": CommandDef(i18n_key="admin.desc"),
129
+ },
130
+ profiles={
131
+ "guest": ProfileDef(include=["start", "help"]),
132
+ "user": ProfileDef(include=["start", "help", "profile"]),
133
+ "admin": ProfileDef(include=["start", "help", "profile", "admin"]),
134
+ },
135
+ scopes=[
136
+ ScopeDef(scope="all_private_chats", profile="guest"),
137
+ ScopeDef(scope="chat", chat_id=12345, profile="admin"),
138
+ ],
139
+ menu_button=MenuButtonDef(mode="commands"),
140
+ )
141
+
142
+ # Create manager with custom profile resolver
143
+ def my_profile_resolver(flags):
144
+ if flags.is_admin:
145
+ return "admin"
146
+ elif flags.is_registered:
147
+ return "user"
148
+ return "guest"
149
+
150
+ manager = CommandScopeManager(
151
+ bot,
152
+ config=config,
153
+ profile_resolver=my_profile_resolver
154
+ )
155
+ await manager.setup_all()
156
+ ```
157
+
158
+ ## 🎯 Use Cases
159
+
160
+ ### E-commerce Bot
161
+ ```python
162
+ # Different commands for different user states
163
+ profiles = {
164
+ "visitor": ProfileDef(include=["start", "catalog", "help"]),
165
+ "customer": ProfileDef(include=["start", "catalog", "cart", "orders", "help"]),
166
+ "vip": ProfileDef(include=["start", "catalog", "cart", "orders", "vip_offers", "help"]),
167
+ }
168
+ ```
169
+
170
+ ### Admin Bot
171
+ ```python
172
+ # Admin commands only for specific users
173
+ scopes = [
174
+ ScopeDef(scope="all_private_chats", profile="user"),
175
+ ScopeDef(scope="chat_member", chat_id=12345, user_id=67890, profile="admin"),
176
+ ]
177
+ ```
178
+
179
+ ### Multi-Language Bot
180
+ ```python
181
+ # Commands in multiple languages
182
+ config = CmdsConfig(
183
+ languages=["en", "ru", "es", "fr"],
184
+ commands={
185
+ "start": CommandDef(i18n_key="commands.start"),
186
+ "help": CommandDef(i18n_key="commands.help"),
187
+ }
188
+ )
189
+ ```
190
+
191
+ ## 📚 Documentation
192
+
193
+ 👉 **[Full Documentation](https://aiogram-cmds.dev)** ← Start here!
194
+
195
+ * **[Quickstart](https://aiogram-cmds.dev/quickstart/)** - Get started in 5 minutes
196
+ * **[Configuration](https://aiogram-cmds.dev/configuration/)** - Complete configuration guide
197
+ * **[API Reference](https://aiogram-cmds.dev/api/)** - Full API documentation
198
+ * **[Tutorials](https://aiogram-cmds.dev/tutorials/)** - Step-by-step guides
199
+ * **[Examples](https://aiogram-cmds.dev/examples/)** - Complete working examples
200
+
201
+ ## 🏗️ Architecture
202
+
203
+ aiogram-cmds provides three levels of complexity:
204
+
205
+ 1. **Auto-Setup**: Zero configuration, works out of the box
206
+ 2. **Simple Mode**: Settings-based configuration with policies
207
+ 3. **Advanced Mode**: Full control with profiles, scopes, and custom resolvers
208
+
209
+ ```
210
+ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
211
+ │ Auto-Setup │ │ Simple Mode │ │ Advanced Mode │
212
+ │ │ │ │ │ │
213
+ │ setup_commands_ │ │ CmdsSettings + │ │ CmdsConfig + │
214
+ │ auto() │ │ CommandPolicy │ │ ProfileResolver │
215
+ └─────────────────┘ └──────────────────┘ └─────────────────┘
216
+ │ │ │
217
+ └───────────────────────┼───────────────────────┘
218
+ │
219
+ ┌──────────────────┐
220
+ │ CommandScopeMgr │
221
+ │ │
222
+ │ • setup_all() │
223
+ │ • update_user() │
224
+ │ • clear_user() │
225
+ └──────────────────┘
226
+ ```
227
+
228
+ ## 🤝 Contributing
229
+
230
+ We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for details.
231
+
232
+ ### Development Setup
233
+
234
+ ```bash
235
+ # Clone the repository
236
+ git clone https://github.com/ArmanAvanesyan/aiogram-cmds.git
237
+ cd aiogram-cmds
238
+
239
+ # Set up development environment
240
+ uv venv
241
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
242
+ uv pip install -e ".[dev]"
243
+
244
+ # Run tests
245
+ uv run pytest
246
+
247
+ # Run linting
248
+ uv run ruff check .
249
+ uv run ruff format .
250
+ ```
251
+
252
+ ## 💬 Community & Support
253
+
254
+ * 💬 **[Discussions](https://github.com/ArmanAvanesyan/aiogram-cmds/discussions)** - Questions, ideas, and community chat
255
+ * 🐛 **[Issues](https://github.com/ArmanAvanesyan/aiogram-cmds/issues)** - Bug reports and feature requests
256
+ * 📖 **[Documentation](https://aiogram-cmds.dev)** - Complete guides and API reference
257
+
258
+ ## 🔒 Security
259
+
260
+ For security issues, see [SECURITY.md](SECURITY.md).
261
+
262
+ ## 📄 License
263
+
264
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
265
+
266
+ ## 🙏 Acknowledgments
267
+
268
+ * Built for [aiogram v3](https://github.com/aiogram/aiogram) - Modern Telegram Bot API framework
269
+ * Inspired by the need for robust command management in production environments
270
+
271
+ ## 📊 Project Status
272
+
273
+ - ✅ **Core Features**: Command management, i18n, profiles, scopes
274
+ - ✅ **Documentation**: Comprehensive guides and API reference
275
+ - ✅ **Testing**: Unit, integration, and performance tests
276
+ - ✅ **CI/CD**: Automated testing and deployment
277
+ - 🚧 **Examples**: Working bot examples (in progress)
278
+ - 🔮 **Future**: Rate limiting, caching, advanced analytics
279
+
280
+ ---
281
+
282
+ **Made with ❤️ for the aiogram community**
@@ -0,0 +1,18 @@
1
+ aiogram_cmds/__init__.py,sha256=DGYWAquSElFPKqOyqQd8VVjAKwVviC_1dZ4uuT0qmrs,2240
2
+ aiogram_cmds/apply.py,sha256=FwtmZD-xxnn_-otfMYZfqPoRqbaQDaCx0p8edBCh54Q,2662
3
+ aiogram_cmds/auto_setup.py,sha256=OC2oCW3Y8bh30cA2-grVtidLWjhrnFKG3j1tQS-kXOA,4921
4
+ aiogram_cmds/builder.py,sha256=ZlCJhpc2GPFcfmrqCz7aUfggMwjmB0X4lVUxJ5BywsI,2347
5
+ aiogram_cmds/builders_configured.py,sha256=sihthu9XG6Xwj3I-ystLR-DEQ5OetMORXBmcTueKmcs,1952
6
+ aiogram_cmds/customize.py,sha256=pLTzVw2W8vng5WN9CeTeMXHGbp8R9z_HJfYEz6zNfc4,1934
7
+ aiogram_cmds/dynamic.py,sha256=mGA05euNAbdc4rN-u8ROgUzaMk9U0-DLSDnUOLQAItc,1062
8
+ aiogram_cmds/manager.py,sha256=fuF_1wR_0C--b5v1rl0dRmh0IiPmNcuEMgNjSb8nTLY,4990
9
+ aiogram_cmds/policy.py,sha256=Ex68-vtYonUqbdC1p00GXSPSToHW5hTLF3s01snr0c0,990
10
+ aiogram_cmds/registry.py,sha256=5-tG-Cz3pAg4iNHiKuOG2RM3C-kXi1hzO7k6j0istNc,646
11
+ aiogram_cmds/setters.py,sha256=dKkM0VIoug2-LJfzTnUgpdB-YXD720hivXn3ssswnuI,2748
12
+ aiogram_cmds/settings.py,sha256=UctpHLsbP_jthkX1_zIKB_2ZSygz9W9v0n8WDiHWHTs,1656
13
+ aiogram_cmds/translator.py,sha256=499Y3nmu78vj-TZcaEWpN59ycV4osjHOgELsvOUzMuY,818
14
+ aiogram_cmds/version.py,sha256=NdAm1Oirm_zG5tKFJB3vRPaOpBT057H5usna3_9MM-g,67
15
+ aiogram_cmds-0.1.0.dist-info/METADATA,sha256=JZHTu58X1p53jcD8f31w7LIq4G2ORCvwOAeOPwldLRk,10002
16
+ aiogram_cmds-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
+ aiogram_cmds-0.1.0.dist-info/licenses/LICENSE,sha256=uboqwLY50frOnHJ4Mo4x3F85t7WiVK5CJRNBNokhSOg,1085
18
+ aiogram_cmds-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 aiogram-sentinel contributors
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.