re-aiogram 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.
re_aiogram/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ # features
2
+ from .core.bot import Bot
3
+
4
+ # aiogram
5
+ # from .client import *
6
+ # from .dispatcher import *
7
+ # from .enums import *
8
+ # from .exceptions import *
9
+ # from .filters import *
10
+ # from .fsm import *
11
+ # from .handlers import *
12
+ # from .loggers import *
13
+ # from .message import *
14
+ # from .methods import *
15
+ # from .types import *
16
+ # from .utils import *
17
+ # from .webhook import *
18
+
19
+ from .types import Message, CallbackQuery
20
+
21
+ from aiogram import Router
22
+ router = Router()
23
+
24
+ __all__=[
25
+ "Bot",
26
+ "Message",
27
+ "CallbackQuery",
28
+ "router"
29
+ ]
@@ -0,0 +1 @@
1
+ from aiogram.client import *
@@ -0,0 +1,2 @@
1
+ from bot import Bot
2
+ from mediagroup import MediaGroup
re_aiogram/core/bot.py ADDED
@@ -0,0 +1,97 @@
1
+ from aiogram import Bot as _Bot, Dispatcher as _Dispatcher
2
+ import asyncio
3
+ import logging
4
+ import signal
5
+ import sys
6
+ import importlib
7
+
8
+
9
+ class Bot:
10
+ def __init__(self, token: str = None):
11
+ if not token:
12
+ raise ValueError("Token is required")
13
+
14
+ self._bot = _Bot(token=token)
15
+ self._dp = _Dispatcher()
16
+
17
+ # middlewares
18
+ from .mediagroup import AlbumMiddleware
19
+
20
+ self._dp.message.middleware(AlbumMiddleware())
21
+
22
+ @property
23
+ def message(self):
24
+ return self._dp.message
25
+
26
+ def load(self, *paths: str):
27
+ """
28
+ Load routers from the specified paths.
29
+
30
+ :param paths:
31
+ """
32
+ routers = []
33
+
34
+ for path in paths:
35
+ module = importlib.import_module(path)
36
+
37
+ if hasattr(module, "router"):
38
+ routers.append(module.router)
39
+ else:
40
+ raise ValueError(
41
+ f"There is no 'router' variable in the {path} module"
42
+ )
43
+
44
+ for router in routers:
45
+ if router.parent_router is None:
46
+ self._dp.include_router(router)
47
+
48
+ def run(self, logging_enabled: bool = True):
49
+ """
50
+ Start the Bot with graceful shutdown.
51
+ """
52
+ if logging_enabled:
53
+ logging.basicConfig(level=logging.INFO)
54
+
55
+ try:
56
+ asyncio.run(self._start())
57
+ except KeyboardInterrupt:
58
+ logging.info("Bot stopped by user")
59
+ except Exception as e:
60
+ logging.error("Polling error: %s", e)
61
+ sys.exit(1)
62
+
63
+ async def _start(self):
64
+ """Start polling and ensure cleanup on exit."""
65
+ loop = asyncio.get_running_loop()
66
+
67
+ # Register signal handlers for graceful shutdown where supported
68
+ for sig in (signal.SIGINT, signal.SIGTERM):
69
+ try:
70
+ loop.add_signal_handler(sig, self._request_shutdown)
71
+ except (NotImplementedError, ValueError):
72
+ # Windows doesn't support add_signal_handler for all signals
73
+ break
74
+
75
+ try:
76
+ await self._dp.start_polling(self._bot)
77
+ finally:
78
+ await self._shutdown()
79
+
80
+ def _request_shutdown(self):
81
+ """Signal handler: initiate graceful stop."""
82
+ logging.info("Shutdown signal received, stopping polling...")
83
+ asyncio.create_task(self._dp.stop_polling())
84
+
85
+ async def _shutdown(self):
86
+ """Close sessions and clean up resources."""
87
+ logging.info("Closing bot session...")
88
+ await self._bot.session.close()
89
+
90
+ loop = asyncio.get_running_loop()
91
+ for sig in (signal.SIGINT, signal.SIGTERM):
92
+ try:
93
+ loop.remove_signal_handler(sig)
94
+ except (NotImplementedError, ValueError):
95
+ pass
96
+
97
+ logging.info("Shutdown complete")
@@ -0,0 +1,183 @@
1
+ from aiogram import BaseMiddleware as _BaseMiddleware
2
+ from aiogram.types import (
3
+ Message,
4
+ InputMediaPhoto,
5
+ InputMediaVideo,
6
+ InputMediaDocument,
7
+ InputMediaAudio,
8
+ InputMedia,
9
+ )
10
+ from typing import Callable, Any, Awaitable, Union, List
11
+ import asyncio
12
+ import time
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MediaGroup(list):
19
+ """
20
+ Custom list-like class for media groups.
21
+ Inherits from list for full aiogram compatibility (answer_media_group, etc.)
22
+ while providing convenient properties for introspection.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ messages: List[Message],
28
+ media: List[Union[InputMediaPhoto, InputMediaVideo, InputMediaDocument, InputMediaAudio, InputMedia]],
29
+ ):
30
+ super().__init__(media)
31
+ self._messages = messages
32
+
33
+ @property
34
+ def messages(self) -> List[Message]:
35
+ """Original aiogram Message objects."""
36
+ return self._messages.copy()
37
+
38
+ @property
39
+ def photos(self) -> List[Message]:
40
+ """Messages containing photos."""
41
+ return [m for m in self._messages if m.photo]
42
+
43
+ @property
44
+ def videos(self) -> List[Message]:
45
+ """Messages containing videos."""
46
+ return [m for m in self._messages if m.video]
47
+
48
+ @property
49
+ def documents(self) -> List[Message]:
50
+ """Messages containing documents."""
51
+ return [m for m in self._messages if m.document]
52
+
53
+ @property
54
+ def audio(self) -> List[Message]:
55
+ """Messages containing audio files."""
56
+ return [m for m in self._messages if m.audio]
57
+
58
+ @property
59
+ def caption(self) -> Union[str, None]:
60
+ """First caption found in the group (Telegram sends caption only on the first item)."""
61
+ for msg in self._messages:
62
+ if msg.caption:
63
+ return msg.caption
64
+ return None
65
+
66
+ @property
67
+ def captions(self) -> List[str]:
68
+ """All captions from the group."""
69
+ return [msg.caption for msg in self._messages if msg.caption]
70
+
71
+ @property
72
+ def is_mixed(self) -> bool:
73
+ """True if album contains different media types."""
74
+ types = {m.content_type for m in self._messages}
75
+ return len(types) > 1
76
+
77
+ @property
78
+ def count(self) -> int:
79
+ """Number of items in the group."""
80
+ return len(self._messages)
81
+
82
+ def __repr__(self) -> str:
83
+ types = [m.content_type for m in self._messages]
84
+ return f"<MediaGroup count={self.count} types={types}>"
85
+
86
+
87
+ class AlbumMiddleware(_BaseMiddleware):
88
+ """
89
+ Middleware for media groups with memory leak protection.
90
+ Injects a MediaGroup instance into the handler data.
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ latency: Union[int, float] = 0.01,
96
+ max_age: Union[int, float] = 60.0,
97
+ ):
98
+ self.latency = latency
99
+ self.max_age = max_age
100
+ self.album_data: dict[str, list[Message]] = {}
101
+ self._last_access: dict[str, float] = {}
102
+ self._lock = asyncio.Lock()
103
+
104
+ async def __call__(
105
+ self,
106
+ handler: Callable[[Message, dict[str, Any]], Awaitable[Any]],
107
+ message: Message,
108
+ data: dict[str, Any],
109
+ ) -> Any:
110
+ if not message.media_group_id:
111
+ return await handler(message, data)
112
+
113
+ await self._cleanup_old_albums()
114
+
115
+ async with self._lock:
116
+ if message.media_group_id in self.album_data:
117
+ self.album_data[message.media_group_id].append(message)
118
+ self._last_access[message.media_group_id] = time.time()
119
+ return None
120
+
121
+ self.album_data[message.media_group_id] = [message]
122
+ self._last_access[message.media_group_id] = time.time()
123
+
124
+ await asyncio.sleep(self.latency)
125
+
126
+ async with self._lock:
127
+ album = sorted(
128
+ self.album_data.get(message.media_group_id, []),
129
+ key=lambda m: m.message_id,
130
+ )
131
+
132
+ media_items: list = []
133
+ for msg in album:
134
+ if msg.photo:
135
+ file_id = msg.photo[-1].file_id
136
+ caption = msg.caption if msg.caption else None
137
+ media_items.append(InputMediaPhoto(media=file_id, caption=caption))
138
+ elif msg.video:
139
+ file_id = msg.video.file_id
140
+ caption = msg.caption if msg.caption else None
141
+ media_items.append(InputMediaVideo(media=file_id, caption=caption))
142
+ elif msg.document:
143
+ file_id = msg.document.file_id
144
+ caption = msg.caption if msg.caption else None
145
+ media_items.append(InputMediaDocument(media=file_id, caption=caption))
146
+ elif msg.audio:
147
+ file_id = msg.audio.file_id
148
+ caption = msg.caption if msg.caption else None
149
+ media_items.append(InputMediaAudio(media=file_id, caption=caption))
150
+ else:
151
+ try:
152
+ obj_dict = msg.model_dump()
153
+ file_id = obj_dict[msg.content_type]["file_id"]
154
+ media_items.append(InputMedia(media=file_id))
155
+ except (KeyError, TypeError):
156
+ logger.warning(
157
+ "Unsupported media type in album: %s", msg.content_type
158
+ )
159
+
160
+ data["_is_last"] = True
161
+ data["album"] = album
162
+ data["media_group"] = MediaGroup(album, media_items)
163
+
164
+ try:
165
+ return await handler(message, data)
166
+ finally:
167
+ async with self._lock:
168
+ self.album_data.pop(message.media_group_id, None)
169
+ self._last_access.pop(message.media_group_id, None)
170
+
171
+ async def _cleanup_old_albums(self):
172
+ now = time.time()
173
+ stale_ids = [
174
+ mg_id
175
+ for mg_id, last_time in list(self._last_access.items())
176
+ if now - last_time > self.max_age
177
+ ]
178
+ if stale_ids:
179
+ async with self._lock:
180
+ for mg_id in stale_ids:
181
+ self.album_data.pop(mg_id, None)
182
+ self._last_access.pop(mg_id, None)
183
+ logger.debug("Cleaned up %d stale album entries", len(stale_ids))
@@ -0,0 +1 @@
1
+ from aiogram.dispatcher import *
@@ -0,0 +1 @@
1
+ from aiogram.enums import *
@@ -0,0 +1 @@
1
+ from aiogram.exceptions import *
@@ -0,0 +1,4 @@
1
+ from aiogram.filters import *
2
+
3
+ from aiogram.utils.magic_filter import MagicFilter
4
+ F = MagicFilter()
@@ -0,0 +1 @@
1
+ from aiogram.fsm import *
@@ -0,0 +1 @@
1
+ from aiogram.handlers import *
@@ -0,0 +1 @@
1
+ from aiogram.loggers import *
@@ -0,0 +1 @@
1
+ from aiogram.methods import *
@@ -0,0 +1,2 @@
1
+ from aiogram.types import *
2
+ from ..core.mediagroup import MediaGroup
@@ -0,0 +1 @@
1
+ from aiogram.utils import *
@@ -0,0 +1 @@
1
+ from aiogram.webhook import *
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: re-aiogram
3
+ Version: 0.1.0
4
+ Summary: Lightweight wrapper over aiogram 3.x
5
+ Author: carrotgoodbye
6
+ License: MIT
7
+ Keywords: telegram,aiogram,bot,telebot,api,framework,wrapper,asyncio
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: aiogram<4.0.0,>=3.0.0
14
+ Dynamic: license-file
15
+
16
+ # re_aiogram
17
+
18
+ **re_aiogram** is a lightweight wrapper over [aiogram 3.x](https://aiogram.dev/) that keeps the familiar aiogram API while simplifying bot development and adding extra features.
19
+
20
+ The goal of the project is to stay fully compatible with aiogram, but provide a cleaner and more convenient developer experience.
21
+
22
+ ---
23
+
24
+ # Install
25
+
26
+ ```bash
27
+ pip install re_aiogram
28
+ ````
29
+
30
+ ```python
31
+ import re_aiogram
32
+ ```
33
+
34
+ ---
35
+
36
+ # Features
37
+
38
+ * familiar aiogram-style API
39
+ * simplified bot startup without explicit dispatcher
40
+ * router auto-loading
41
+ * built-in MediaGroup support
42
+ * aiogram-compatible imports
43
+
44
+ ---
45
+
46
+ # Quick Start
47
+
48
+ ```python
49
+ from re_aiogram import Bot, Message
50
+ from re_aiogram.filters import Command
51
+
52
+ API_TOKEN = "YOUR_API_TOKEN"
53
+
54
+ bot = Bot(token=API_TOKEN)
55
+
56
+
57
+ @bot.message(Command("start"))
58
+ async def cmd_start(message: Message):
59
+ await message.answer(
60
+ "Hi! I am a Telegram bot powered by re_aiogram!"
61
+ )
62
+
63
+
64
+ bot.run(logging_enabled=True)
65
+ ```
66
+
67
+ ---
68
+
69
+ # MediaGroup Handler
70
+
71
+ Built-in support for Telegram media groups.
72
+
73
+ ```python
74
+ from re_aiogram import Bot, Message
75
+ from re_aiogram.filters import F
76
+ from re_aiogram.types import MediaGroup
77
+
78
+ bot = Bot(token="TOKEN")
79
+
80
+
81
+ @bot.message(F.media_group_id)
82
+ async def handle_album(message: Message, media_group: MediaGroup):
83
+ await message.answer_media_group(media_group)
84
+ ```
85
+
86
+ `media_group` contains the collected media group messages.
87
+
88
+ ## Properties
89
+
90
+ | Property | Description |
91
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
92
+ | `media_group.count` | Returns the number of media items in the group. |
93
+ | `media_group.caption` | Returns the first caption found in the group (Telegram sends caption only on the first item). Returns `None` if no caption is present. |
94
+ | `media_group.captions` | Returns a list of all captions from the group. |
95
+ | `media_group.messages` | Returns a copy of the original `Message` objects from the group. |
96
+ | `media_group.photos` | Returns a list of `Message` objects that contain photos. |
97
+ | `media_group.videos` | Returns a list of `Message` objects that contain videos. |
98
+ | `media_group.documents` | Returns a list of `Message` objects that contain documents. |
99
+ | `media_group.audio` | Returns a list of `Message` objects that contain audio files. |
100
+ | `media_group.is_mixed` | Returns `True` if the album contains different media types (e.g., photos and videos together). |
101
+
102
+ ---
103
+
104
+ # Routers
105
+
106
+ Router connection is simplified with `bot.load()`
107
+
108
+ ## Project structure
109
+
110
+ ```text
111
+ project/
112
+
113
+ ├── main.py
114
+ └── handlers/
115
+ └── start.py
116
+ ```
117
+
118
+ ### main.py
119
+
120
+ ```python
121
+ from re_aiogram import Bot
122
+
123
+ API_TOKEN = "YOUR_API_TOKEN"
124
+
125
+ bot = Bot(token=API_TOKEN)
126
+
127
+ bot.load("handlers.start")
128
+
129
+ bot.run(logging_enabled=True)
130
+ ```
131
+
132
+ ### handlers/start.py
133
+
134
+ ```python
135
+ from re_aiogram import router, Message
136
+ from re_aiogram.filters import Command
137
+
138
+
139
+ @router.message(Command("start"))
140
+ async def start(message: Message):
141
+ await message.answer("Router connected!")
142
+ ```
143
+
144
+ ---
145
+
146
+ # Future improvements
147
+
148
+ These features are planned for upcoming versions and are not yet part of the core API.
149
+
150
+ ## Flow System (FSM alternative)
151
+
152
+ A lightweight state/flow system replacing traditional FSM:
153
+
154
+ * step-based flow control
155
+ * `ctx.next()` and `ctx.back()`
156
+ * shared `ctx.data`
157
+ * parallel flows via `scope()`
158
+ * simple chain-based conversation handling
159
+
160
+ Example concept:
161
+
162
+ ```python
163
+ @router.message(Command("register"))
164
+ async def register(message: Message, ctx: FlowContext):
165
+ await message.answer("What is your name?")
166
+ ctx.next(get_name)
167
+ ```
168
+
169
+ ---
170
+
171
+ # Philosophy
172
+
173
+ re_aiogram tries to make Telegram bot development:
174
+
175
+ * simpler
176
+ * cleaner
177
+ * less boilerplate-heavy
178
+ * more beginner-friendly
179
+
180
+ while still preserving compatibility with the `aiogram` ecosystem.
181
+
182
+ ---
183
+
184
+ # License
185
+
186
+ `MIT License`
@@ -0,0 +1,21 @@
1
+ re_aiogram/__init__.py,sha256=vKCRDj0ZMtixJbQPLJsfPfenhpSkEMA9VHYfdR0XsXU,558
2
+ re_aiogram/client/__init__.py,sha256=Z1NYBuXC6jTn1XL9FLFVRB5ZnH3G0CLk0k-1eHUPW2M,28
3
+ re_aiogram/core/__init__.py,sha256=A3Xl9YpeIm5fsxP3CzOoqvZXK9pSJDofF2BXWHjTc3I,56
4
+ re_aiogram/core/bot.py,sha256=Iv01Fw1tVRwivJepEPykFzoAc_8SUNdJELFd5GS7050,2920
5
+ re_aiogram/core/mediagroup.py,sha256=Hyx2pX4tC3dFXfmShPR_vccdj19noefmMkZvlRHBMqA,6447
6
+ re_aiogram/dispatcher/__init__.py,sha256=Q_zgqC_C04pu53QJCVbLaGI0hMgAgU1dRitAVQ_qYJs,32
7
+ re_aiogram/enums/__init__.py,sha256=xfZH0fSJZCsezFzz518yJHYmiXHzCIMfBX5Lc14w-Bk,27
8
+ re_aiogram/exceptions/__init__.py,sha256=rzWgxPe18zVTRrAkV8_oXHxInUKF2QHCbQjULvgzaiM,32
9
+ re_aiogram/filters/__init__.py,sha256=uiYN75D2bG3P4YcirVZA7RhucOkNaT8Vgh0PfMXpves,102
10
+ re_aiogram/fsm/__init__.py,sha256=KdWz1tgVAiz_L3epQsVI83i3cw31qBi2uUhk9HiCmnQ,27
11
+ re_aiogram/handlers/__init__.py,sha256=Hq954qso_xVxlansetEQZVeBtlFsnK4_hSi9-LVj9TI,30
12
+ re_aiogram/loggers/__init__.py,sha256=nmt8fCYDzLz5Oj0Pj4WviWgu0YdK23Mc8NmzM69aM24,29
13
+ re_aiogram/methods/__init__.py,sha256=e8vJWIdkYg5vzgokh6h8CU5es5B4SkkKgw7LAMV0QRo,29
14
+ re_aiogram/types/__init__.py,sha256=1zRW-DVikVDclJsWuve9g_0-uoupYV493oKLnUVQads,69
15
+ re_aiogram/utils/__init__.py,sha256=2PiWpdXKqe6Biy-M96FHxOj5GLXWZpU8mnIQomsszlg,27
16
+ re_aiogram/webhook/__init__.py,sha256=1uX57FdMFDuICRrVsCOwCMiv9gxAu82R9m0OfPKQkfI,29
17
+ re_aiogram-0.1.0.dist-info/licenses/LICENSE,sha256=pA6Bs2tlRvpm1ph7Wi_3BTfLJdPwzwp1io7jViEwf3o,1091
18
+ re_aiogram-0.1.0.dist-info/METADATA,sha256=7inct04jIOqKFdrUj_uf8lciIKcu0ihARTxwuhVtEu0,5230
19
+ re_aiogram-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
+ re_aiogram-0.1.0.dist-info/top_level.txt,sha256=YLIyi2WD6p8aJOCRNe2XVAKAmo-jRzERo-mY3JDB_II,11
21
+ re_aiogram-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 carrotgoodbye
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.
@@ -0,0 +1 @@
1
+ re_aiogram