Revert "Третий коммит, добавление share, share_kb, а также ADMIN_ID"
This reverts commit b98123f4dc
.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,3 @@ myvenv
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
__pycache__/
|
__pycache__/
|
||||||
debug_kb.py
|
|
||||||
handliers_test.py
|
|
||||||
share_2.py
|
|
||||||
|
@@ -63,11 +63,10 @@ python3 -m pip install -r req.txt
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
После того как вы установите зависимости необходимо будет настроить конфигурационный файл.
|
После того как вы установите зависимости необходимо будет настроить конфигурационный файл.
|
||||||
Для этого вам нужно зайти в ```config.py``` и вписать токен своего бота и ID администратора
|
Для этого вам нужно зайти в ```config.py``` и вписать токен своего бота
|
||||||
|
|
||||||
```
|
```
|
||||||
TOKEN='XXX:YYY'
|
TOKEN='XXX:YYY'
|
||||||
ADMIN_ID=XXXXXXXXX
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Теперь нам осталось только запустить бот:
|
Теперь нам осталось только запустить бот:
|
||||||
|
@@ -1,16 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from aiogram import F, Router, Bot
|
from aiogram import F, Router
|
||||||
from aiogram.filters import CommandStart, Command, CommandObject
|
from aiogram.filters import CommandStart, Command
|
||||||
from aiogram.types import Message, CallbackQuery
|
from aiogram.types import Message, CallbackQuery
|
||||||
from aiogram.enums import ParseMode
|
from aiogram.enums import ParseMode
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
import app.keyboard as kb
|
import app.keyboard as kb
|
||||||
import app.debug_kb as debug_kb
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import app.share
|
|
||||||
from random import randint
|
|
||||||
from config import ADMIN_ID, TOKEN
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -28,45 +22,8 @@ async def cmd_start(message: Message):
|
|||||||
|
|
||||||
cur.execute(insert_user, val)
|
cur.execute(insert_user, val)
|
||||||
|
|
||||||
cur.execute("SELECT * FROM users")
|
|
||||||
#print(cur.fetchall())
|
|
||||||
base.commit()
|
base.commit()
|
||||||
|
|
||||||
base.close()
|
base.close()
|
||||||
|
|
||||||
await message.answer("тестовый ответ")
|
await message.answer("тестовый ответ")
|
||||||
|
|
||||||
@router.message(Command("echo"))
|
|
||||||
async def echo(message: Message):
|
|
||||||
"""Повторяет сообщение пользователя."""
|
|
||||||
await message.reply(message.text)
|
|
||||||
|
|
||||||
# @router.message(Command("test"))
|
|
||||||
# async def echo(message: Message):
|
|
||||||
|
|
||||||
# await message.answer("")
|
|
||||||
|
|
||||||
# await message.reply(message.text)
|
|
||||||
|
|
||||||
@router.message(Command("shareold"))
|
|
||||||
async def share(message: Message, bot: Bot):
|
|
||||||
if message.from_user.id == ADMIN_ID:
|
|
||||||
base = sqlite3.connect('users.db')
|
|
||||||
|
|
||||||
cur = base.cursor()
|
|
||||||
|
|
||||||
cur.execute("SELECT id FROM users")
|
|
||||||
|
|
||||||
users_id = cur.fetchall()
|
|
||||||
|
|
||||||
for users in users_id:
|
|
||||||
user = users[0]
|
|
||||||
await bot.send_message(user, 'тестовая рассылка')
|
|
||||||
print(user)
|
|
||||||
|
|
||||||
base.commit()
|
|
||||||
|
|
||||||
base.close()
|
|
||||||
else:
|
|
||||||
await message.answer("вы не администратор")
|
|
||||||
|
|
@@ -1,23 +0,0 @@
|
|||||||
from aiogram.types import (
|
|
||||||
ReplyKeyboardMarkup,
|
|
||||||
KeyboardButton,
|
|
||||||
InlineKeyboardButton,
|
|
||||||
InlineKeyboardMarkup
|
|
||||||
)
|
|
||||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
|
||||||
|
|
||||||
# # тестовая клавиатура, может быть потом удалена
|
|
||||||
# test_kb = ReplyKeyboardMarkup(keyboard=
|
|
||||||
# [
|
|
||||||
# [
|
|
||||||
# KeyboardButton(text='кнопка1'),
|
|
||||||
# KeyboardButton(text="кнопка2")
|
|
||||||
# ]
|
|
||||||
# [
|
|
||||||
# KeyboardButton(text="test")
|
|
||||||
# ],
|
|
||||||
# ],
|
|
||||||
# resize_keyboard=True,
|
|
||||||
# one_time_keyboard=True,
|
|
||||||
# input_field_placeholder='тестовый ввод',
|
|
||||||
# selective=True)
|
|
104
app/share.py
104
app/share.py
@@ -1,104 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from aiogram import F, Router, Bot
|
|
||||||
from aiogram.filters import CommandStart, Command, CommandObject
|
|
||||||
from aiogram.types import ContentType
|
|
||||||
from aiogram.types import Message, CallbackQuery
|
|
||||||
from aiogram.types import ReplyKeyboardRemove, InputMediaPhoto
|
|
||||||
from aiogram.enums import ParseMode
|
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
import app.share_kb as share_kb
|
|
||||||
import sqlite3
|
|
||||||
from random import randint
|
|
||||||
from config import ADMIN_ID, TOKEN
|
|
||||||
|
|
||||||
share_router = Router()
|
|
||||||
|
|
||||||
class Share(StatesGroup):
|
|
||||||
share_begin = State()
|
|
||||||
share_msg = State()
|
|
||||||
|
|
||||||
async def send_media(bot: Bot, chat_id: int, message: Message):
|
|
||||||
"""Функция для точной пересылки сообщения без изменений"""
|
|
||||||
if message.photo:
|
|
||||||
await bot.send_photo(
|
|
||||||
chat_id=chat_id,
|
|
||||||
photo=message.photo[-1].file_id,
|
|
||||||
caption=message.caption
|
|
||||||
)
|
|
||||||
elif message.video:
|
|
||||||
await bot.send_video(
|
|
||||||
chat_id=chat_id,
|
|
||||||
video=message.video.file_id,
|
|
||||||
caption=message.caption
|
|
||||||
)
|
|
||||||
elif message.document:
|
|
||||||
await bot.send_document(
|
|
||||||
chat_id=chat_id,
|
|
||||||
document=message.document.file_id,
|
|
||||||
caption=message.caption
|
|
||||||
)
|
|
||||||
elif message.text:
|
|
||||||
await bot.send_message(
|
|
||||||
chat_id=chat_id,
|
|
||||||
text=message.text
|
|
||||||
)
|
|
||||||
|
|
||||||
@share_router.message(Command('share'))
|
|
||||||
async def start_share(message: Message, state: FSMContext):
|
|
||||||
if message.from_user.id == ADMIN_ID:
|
|
||||||
await message.answer("🤔вы уверены что хотите начать рассылку?", reply_markup=share_kb.share_starting)
|
|
||||||
else:
|
|
||||||
await message.answer("⛔вы не являетесь администратором")
|
|
||||||
|
|
||||||
@share_router.callback_query(F.data == 'share_cancel')
|
|
||||||
async def share_cancel_cmd(callback: CallbackQuery, state: FSMContext):
|
|
||||||
await callback.answer(show_alert=False)
|
|
||||||
await callback.message.edit_text('❌рассылка не была начата',
|
|
||||||
reply_markup=None)
|
|
||||||
await state.clear()
|
|
||||||
|
|
||||||
|
|
||||||
@share_router.callback_query(F.data == 'share_starting')
|
|
||||||
async def share_start_cmd(callback: CallbackQuery, state: FSMContext):
|
|
||||||
await callback.answer(show_alert=False)
|
|
||||||
await callback.message.edit_text(
|
|
||||||
'✉️отправьте сообщение для рассылки\n\nучтите что можно ' \
|
|
||||||
'приложить только один медиафайл',
|
|
||||||
reply_markup=None)
|
|
||||||
await state.set_state(Share.share_begin)
|
|
||||||
|
|
||||||
@share_router.message(Share.share_begin)
|
|
||||||
async def share_begin_cmd(message: Message, state: FSMContext, bot: Bot):
|
|
||||||
await state.update_data(share_msg=message)
|
|
||||||
await message.answer("🤔вы уверены что хотите разослать сообщение?",
|
|
||||||
reply_markup=share_kb.share_send)
|
|
||||||
await state.set_state(Share.share_msg)
|
|
||||||
|
|
||||||
@share_router.callback_query(F.data == 'share_starting_send')
|
|
||||||
async def share_send_cmd(callback: CallbackQuery, state: FSMContext, bot: Bot):
|
|
||||||
await callback.answer(show_alert=False)
|
|
||||||
|
|
||||||
data = await state.get_data()
|
|
||||||
share_msg = data.get('share_msg')
|
|
||||||
|
|
||||||
if not share_msg:
|
|
||||||
await callback.message.edit_text('❌Ошибка: сообщение для рассылки не найдено')
|
|
||||||
await state.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
base = sqlite3.connect('users.db')
|
|
||||||
cur = base.cursor()
|
|
||||||
cur.execute("SELECT id FROM users")
|
|
||||||
users_id = cur.fetchall()
|
|
||||||
|
|
||||||
await callback.message.edit_text('🔄Рассылаю сообщение...', reply_markup=None)
|
|
||||||
await asyncio.sleep(0.1) # Небольшая задержка перед началом рассылки
|
|
||||||
|
|
||||||
for user_id, in users_id:
|
|
||||||
if user_id != ADMIN_ID:
|
|
||||||
await send_media(bot, user_id, share_msg)
|
|
||||||
|
|
||||||
base.close()
|
|
||||||
await callback.message.edit_text('🎉Сообщение успешно разослано', reply_markup=None)
|
|
||||||
await state.clear()
|
|
@@ -1,19 +0,0 @@
|
|||||||
from aiogram.types import (
|
|
||||||
ReplyKeyboardMarkup,
|
|
||||||
KeyboardButton,
|
|
||||||
InlineKeyboardButton,
|
|
||||||
InlineKeyboardMarkup,
|
|
||||||
ReplyKeyboardRemove
|
|
||||||
)
|
|
||||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
|
|
||||||
from aiogram.filters.callback_data import CallbackData
|
|
||||||
|
|
||||||
share_starting = InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text='да✅', callback_data='share_starting'),
|
|
||||||
InlineKeyboardButton(text='нет❌', callback_data='share_cancel')]
|
|
||||||
], resize_keyboard=True, input_field_placeholder='выберите действие')
|
|
||||||
|
|
||||||
share_send = InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text='да✅', callback_data='share_starting_send'),
|
|
||||||
InlineKeyboardButton(text='нет❌', callback_data='share_cancel')]
|
|
||||||
], resize_keyboard=True, input_field_placeholder='выберите действие')
|
|
10
bot.py
10
bot.py
@@ -3,17 +3,17 @@ import logging
|
|||||||
from aiogram import F, Router, Dispatcher, Bot
|
from aiogram import F, Router, Dispatcher, Bot
|
||||||
from aiogram.types import Message, CallbackQuery, User
|
from aiogram.types import Message, CallbackQuery, User
|
||||||
from aiogram.filters import CommandStart, Command
|
from aiogram.filters import CommandStart, Command
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.enums import ParseMode
|
from aiogram.enums import ParseMode
|
||||||
from config import TOKEN, ADMIN_ID
|
from config import TOKEN
|
||||||
from app.handliers import router
|
from app.handliers import router
|
||||||
from app.share import share_router
|
|
||||||
bot = Bot(token=TOKEN, ParseMode="HTML")
|
bot = Bot(token=TOKEN)
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
dp.include_router(router)
|
dp.include_router(router)
|
||||||
dp.include_router(share_router)
|
|
||||||
await bot.delete_webhook(drop_pending_updates=True)
|
|
||||||
await dp.start_polling(bot)
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
11
myenv/.gitignore
vendored
11
myenv/.gitignore
vendored
@@ -1,11 +0,0 @@
|
|||||||
# Created by venv; see https://docs.python.org/3/library/venv.html
|
|
||||||
share_2.py
|
|
||||||
__pychahe__
|
|
||||||
myenv
|
|
||||||
debug_kb.py
|
|
||||||
handliers_test.py
|
|
||||||
share_2.py
|
|
||||||
__pychahe__
|
|
||||||
myenv
|
|
||||||
debug_kb.py
|
|
||||||
handliers_test.py
|
|
@@ -1 +0,0 @@
|
|||||||
pip
|
|
@@ -1,318 +0,0 @@
|
|||||||
Metadata-Version: 2.3
|
|
||||||
Name: aiofiles
|
|
||||||
Version: 24.1.0
|
|
||||||
Summary: File support for asyncio.
|
|
||||||
Project-URL: Changelog, https://github.com/Tinche/aiofiles#history
|
|
||||||
Project-URL: Bug Tracker, https://github.com/Tinche/aiofiles/issues
|
|
||||||
Project-URL: repository, https://github.com/Tinche/aiofiles
|
|
||||||
Author-email: Tin Tvrtkovic <tinchester@gmail.com>
|
|
||||||
License: Apache-2.0
|
|
||||||
License-File: LICENSE
|
|
||||||
License-File: NOTICE
|
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
|
||||||
Classifier: Framework :: AsyncIO
|
|
||||||
Classifier: License :: OSI Approved :: Apache Software License
|
|
||||||
Classifier: Operating System :: OS Independent
|
|
||||||
Classifier: Programming Language :: Python :: 3.8
|
|
||||||
Classifier: Programming Language :: Python :: 3.9
|
|
||||||
Classifier: Programming Language :: Python :: 3.10
|
|
||||||
Classifier: Programming Language :: Python :: 3.11
|
|
||||||
Classifier: Programming Language :: Python :: 3.12
|
|
||||||
Classifier: Programming Language :: Python :: 3.13
|
|
||||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
||||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
||||||
Requires-Python: >=3.8
|
|
||||||
Description-Content-Type: text/markdown
|
|
||||||
|
|
||||||
# aiofiles: file support for asyncio
|
|
||||||
|
|
||||||
[](https://pypi.python.org/pypi/aiofiles)
|
|
||||||
[](https://github.com/Tinche/aiofiles/actions)
|
|
||||||
[](https://github.com/Tinche/aiofiles/actions/workflows/main.yml)
|
|
||||||
[](https://github.com/Tinche/aiofiles)
|
|
||||||
[](https://github.com/psf/black)
|
|
||||||
|
|
||||||
**aiofiles** is an Apache2 licensed library, written in Python, for handling local
|
|
||||||
disk files in asyncio applications.
|
|
||||||
|
|
||||||
Ordinary local file IO is blocking, and cannot easily and portably be made
|
|
||||||
asynchronous. This means doing file IO may interfere with asyncio applications,
|
|
||||||
which shouldn't block the executing thread. aiofiles helps with this by
|
|
||||||
introducing asynchronous versions of files that support delegating operations to
|
|
||||||
a separate thread pool.
|
|
||||||
|
|
||||||
```python
|
|
||||||
async with aiofiles.open('filename', mode='r') as f:
|
|
||||||
contents = await f.read()
|
|
||||||
print(contents)
|
|
||||||
'My file contents'
|
|
||||||
```
|
|
||||||
|
|
||||||
Asynchronous iteration is also supported.
|
|
||||||
|
|
||||||
```python
|
|
||||||
async with aiofiles.open('filename') as f:
|
|
||||||
async for line in f:
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Asynchronous interface to tempfile module.
|
|
||||||
|
|
||||||
```python
|
|
||||||
async with aiofiles.tempfile.TemporaryFile('wb') as f:
|
|
||||||
await f.write(b'Hello, World!')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- a file API very similar to Python's standard, blocking API
|
|
||||||
- support for buffered and unbuffered binary files, and buffered text files
|
|
||||||
- support for `async`/`await` ([PEP 492](https://peps.python.org/pep-0492/)) constructs
|
|
||||||
- async interface to tempfile module
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To install aiofiles, simply:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ pip install aiofiles
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Files are opened using the `aiofiles.open()` coroutine, which in addition to
|
|
||||||
mirroring the builtin `open` accepts optional `loop` and `executor`
|
|
||||||
arguments. If `loop` is absent, the default loop will be used, as per the
|
|
||||||
set asyncio policy. If `executor` is not specified, the default event loop
|
|
||||||
executor will be used.
|
|
||||||
|
|
||||||
In case of success, an asynchronous file object is returned with an
|
|
||||||
API identical to an ordinary file, except the following methods are coroutines
|
|
||||||
and delegate to an executor:
|
|
||||||
|
|
||||||
- `close`
|
|
||||||
- `flush`
|
|
||||||
- `isatty`
|
|
||||||
- `read`
|
|
||||||
- `readall`
|
|
||||||
- `read1`
|
|
||||||
- `readinto`
|
|
||||||
- `readline`
|
|
||||||
- `readlines`
|
|
||||||
- `seek`
|
|
||||||
- `seekable`
|
|
||||||
- `tell`
|
|
||||||
- `truncate`
|
|
||||||
- `writable`
|
|
||||||
- `write`
|
|
||||||
- `writelines`
|
|
||||||
|
|
||||||
In case of failure, one of the usual exceptions will be raised.
|
|
||||||
|
|
||||||
`aiofiles.stdin`, `aiofiles.stdout`, `aiofiles.stderr`,
|
|
||||||
`aiofiles.stdin_bytes`, `aiofiles.stdout_bytes`, and
|
|
||||||
`aiofiles.stderr_bytes` provide async access to `sys.stdin`,
|
|
||||||
`sys.stdout`, `sys.stderr`, and their corresponding `.buffer` properties.
|
|
||||||
|
|
||||||
The `aiofiles.os` module contains executor-enabled coroutine versions of
|
|
||||||
several useful `os` functions that deal with files:
|
|
||||||
|
|
||||||
- `stat`
|
|
||||||
- `statvfs`
|
|
||||||
- `sendfile`
|
|
||||||
- `rename`
|
|
||||||
- `renames`
|
|
||||||
- `replace`
|
|
||||||
- `remove`
|
|
||||||
- `unlink`
|
|
||||||
- `mkdir`
|
|
||||||
- `makedirs`
|
|
||||||
- `rmdir`
|
|
||||||
- `removedirs`
|
|
||||||
- `link`
|
|
||||||
- `symlink`
|
|
||||||
- `readlink`
|
|
||||||
- `listdir`
|
|
||||||
- `scandir`
|
|
||||||
- `access`
|
|
||||||
- `getcwd`
|
|
||||||
- `path.abspath`
|
|
||||||
- `path.exists`
|
|
||||||
- `path.isfile`
|
|
||||||
- `path.isdir`
|
|
||||||
- `path.islink`
|
|
||||||
- `path.ismount`
|
|
||||||
- `path.getsize`
|
|
||||||
- `path.getatime`
|
|
||||||
- `path.getctime`
|
|
||||||
- `path.samefile`
|
|
||||||
- `path.sameopenfile`
|
|
||||||
|
|
||||||
### Tempfile
|
|
||||||
|
|
||||||
**aiofiles.tempfile** implements the following interfaces:
|
|
||||||
|
|
||||||
- TemporaryFile
|
|
||||||
- NamedTemporaryFile
|
|
||||||
- SpooledTemporaryFile
|
|
||||||
- TemporaryDirectory
|
|
||||||
|
|
||||||
Results return wrapped with a context manager allowing use with async with and async for.
|
|
||||||
|
|
||||||
```python
|
|
||||||
async with aiofiles.tempfile.NamedTemporaryFile('wb+') as f:
|
|
||||||
await f.write(b'Line1\n Line2')
|
|
||||||
await f.seek(0)
|
|
||||||
async for line in f:
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
async with aiofiles.tempfile.TemporaryDirectory() as d:
|
|
||||||
filename = os.path.join(d, "file.ext")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Writing tests for aiofiles
|
|
||||||
|
|
||||||
Real file IO can be mocked by patching `aiofiles.threadpool.sync_open`
|
|
||||||
as desired. The return type also needs to be registered with the
|
|
||||||
`aiofiles.threadpool.wrap` dispatcher:
|
|
||||||
|
|
||||||
```python
|
|
||||||
aiofiles.threadpool.wrap.register(mock.MagicMock)(
|
|
||||||
lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_stuff():
|
|
||||||
write_data = 'data'
|
|
||||||
read_file_chunks = [
|
|
||||||
b'file chunks 1',
|
|
||||||
b'file chunks 2',
|
|
||||||
b'file chunks 3',
|
|
||||||
b'',
|
|
||||||
]
|
|
||||||
file_chunks_iter = iter(read_file_chunks)
|
|
||||||
|
|
||||||
mock_file_stream = mock.MagicMock(
|
|
||||||
read=lambda *args, **kwargs: next(file_chunks_iter)
|
|
||||||
)
|
|
||||||
|
|
||||||
with mock.patch('aiofiles.threadpool.sync_open', return_value=mock_file_stream) as mock_open:
|
|
||||||
async with aiofiles.open('filename', 'w') as f:
|
|
||||||
await f.write(write_data)
|
|
||||||
assert f.read() == b'file chunks 1'
|
|
||||||
|
|
||||||
mock_file_stream.write.assert_called_once_with(write_data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### History
|
|
||||||
|
|
||||||
#### 24.1.0 (2024-06-24)
|
|
||||||
|
|
||||||
- Import `os.link` conditionally to fix importing on android.
|
|
||||||
[#175](https://github.com/Tinche/aiofiles/issues/175)
|
|
||||||
- Remove spurious items from `aiofiles.os.__all__` when running on Windows.
|
|
||||||
- Switch to more modern async idioms: Remove types.coroutine and make AiofilesContextManager an awaitable instead a coroutine.
|
|
||||||
- Add `aiofiles.os.path.abspath` and `aiofiles.os.getcwd`.
|
|
||||||
[#174](https://github.com/Tinche/aiofiles/issues/181)
|
|
||||||
- _aiofiles_ is now tested on Python 3.13 too.
|
|
||||||
[#184](https://github.com/Tinche/aiofiles/pull/184)
|
|
||||||
- Dropped Python 3.7 support. If you require it, use version 23.2.1.
|
|
||||||
|
|
||||||
#### 23.2.1 (2023-08-09)
|
|
||||||
|
|
||||||
- Import `os.statvfs` conditionally to fix importing on non-UNIX systems.
|
|
||||||
[#171](https://github.com/Tinche/aiofiles/issues/171) [#172](https://github.com/Tinche/aiofiles/pull/172)
|
|
||||||
- aiofiles is now also tested on Windows.
|
|
||||||
|
|
||||||
#### 23.2.0 (2023-08-09)
|
|
||||||
|
|
||||||
- aiofiles is now tested on Python 3.12 too.
|
|
||||||
[#166](https://github.com/Tinche/aiofiles/issues/166) [#168](https://github.com/Tinche/aiofiles/pull/168)
|
|
||||||
- On Python 3.12, `aiofiles.tempfile.NamedTemporaryFile` now accepts a `delete_on_close` argument, just like the stdlib version.
|
|
||||||
- On Python 3.12, `aiofiles.tempfile.NamedTemporaryFile` no longer exposes a `delete` attribute, just like the stdlib version.
|
|
||||||
- Added `aiofiles.os.statvfs` and `aiofiles.os.path.ismount`.
|
|
||||||
[#162](https://github.com/Tinche/aiofiles/pull/162)
|
|
||||||
- Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry.
|
|
||||||
[#169](https://github.com/Tinche/aiofiles/pull/169)
|
|
||||||
|
|
||||||
#### 23.1.0 (2023-02-09)
|
|
||||||
|
|
||||||
- Added `aiofiles.os.access`.
|
|
||||||
[#146](https://github.com/Tinche/aiofiles/pull/146)
|
|
||||||
- Removed `aiofiles.tempfile.temptypes.AsyncSpooledTemporaryFile.softspace`.
|
|
||||||
[#151](https://github.com/Tinche/aiofiles/pull/151)
|
|
||||||
- Added `aiofiles.stdin`, `aiofiles.stdin_bytes`, and other stdio streams.
|
|
||||||
[#154](https://github.com/Tinche/aiofiles/pull/154)
|
|
||||||
- Transition to `asyncio.get_running_loop` (vs `asyncio.get_event_loop`) internally.
|
|
||||||
|
|
||||||
#### 22.1.0 (2022-09-04)
|
|
||||||
|
|
||||||
- Added `aiofiles.os.path.islink`.
|
|
||||||
[#126](https://github.com/Tinche/aiofiles/pull/126)
|
|
||||||
- Added `aiofiles.os.readlink`.
|
|
||||||
[#125](https://github.com/Tinche/aiofiles/pull/125)
|
|
||||||
- Added `aiofiles.os.symlink`.
|
|
||||||
[#124](https://github.com/Tinche/aiofiles/pull/124)
|
|
||||||
- Added `aiofiles.os.unlink`.
|
|
||||||
[#123](https://github.com/Tinche/aiofiles/pull/123)
|
|
||||||
- Added `aiofiles.os.link`.
|
|
||||||
[#121](https://github.com/Tinche/aiofiles/pull/121)
|
|
||||||
- Added `aiofiles.os.renames`.
|
|
||||||
[#120](https://github.com/Tinche/aiofiles/pull/120)
|
|
||||||
- Added `aiofiles.os.{listdir, scandir}`.
|
|
||||||
[#143](https://github.com/Tinche/aiofiles/pull/143)
|
|
||||||
- Switched to CalVer.
|
|
||||||
- Dropped Python 3.6 support. If you require it, use version 0.8.0.
|
|
||||||
- aiofiles is now tested on Python 3.11.
|
|
||||||
|
|
||||||
#### 0.8.0 (2021-11-27)
|
|
||||||
|
|
||||||
- aiofiles is now tested on Python 3.10.
|
|
||||||
- Added `aiofiles.os.replace`.
|
|
||||||
[#107](https://github.com/Tinche/aiofiles/pull/107)
|
|
||||||
- Added `aiofiles.os.{makedirs, removedirs}`.
|
|
||||||
- Added `aiofiles.os.path.{exists, isfile, isdir, getsize, getatime, getctime, samefile, sameopenfile}`.
|
|
||||||
[#63](https://github.com/Tinche/aiofiles/pull/63)
|
|
||||||
- Added `suffix`, `prefix`, `dir` args to `aiofiles.tempfile.TemporaryDirectory`.
|
|
||||||
[#116](https://github.com/Tinche/aiofiles/pull/116)
|
|
||||||
|
|
||||||
#### 0.7.0 (2021-05-17)
|
|
||||||
|
|
||||||
- Added the `aiofiles.tempfile` module for async temporary files.
|
|
||||||
[#56](https://github.com/Tinche/aiofiles/pull/56)
|
|
||||||
- Switched to Poetry and GitHub actions.
|
|
||||||
- Dropped 3.5 support.
|
|
||||||
|
|
||||||
#### 0.6.0 (2020-10-27)
|
|
||||||
|
|
||||||
- `aiofiles` is now tested on ppc64le.
|
|
||||||
- Added `name` and `mode` properties to async file objects.
|
|
||||||
[#82](https://github.com/Tinche/aiofiles/pull/82)
|
|
||||||
- Fixed a DeprecationWarning internally.
|
|
||||||
[#75](https://github.com/Tinche/aiofiles/pull/75)
|
|
||||||
- Python 3.9 support and tests.
|
|
||||||
|
|
||||||
#### 0.5.0 (2020-04-12)
|
|
||||||
|
|
||||||
- Python 3.8 support. Code base modernization (using `async/await` instead of `asyncio.coroutine`/`yield from`).
|
|
||||||
- Added `aiofiles.os.remove`, `aiofiles.os.rename`, `aiofiles.os.mkdir`, `aiofiles.os.rmdir`.
|
|
||||||
[#62](https://github.com/Tinche/aiofiles/pull/62)
|
|
||||||
|
|
||||||
#### 0.4.0 (2018-08-11)
|
|
||||||
|
|
||||||
- Python 3.7 support.
|
|
||||||
- Removed Python 3.3/3.4 support. If you use these versions, stick to aiofiles 0.3.x.
|
|
||||||
|
|
||||||
#### 0.3.2 (2017-09-23)
|
|
||||||
|
|
||||||
- The LICENSE is now included in the sdist.
|
|
||||||
[#31](https://github.com/Tinche/aiofiles/pull/31)
|
|
||||||
|
|
||||||
#### 0.3.1 (2017-03-10)
|
|
||||||
|
|
||||||
- Introduced a changelog.
|
|
||||||
- `aiofiles.os.sendfile` will now work if the standard `os` module contains a `sendfile` function.
|
|
||||||
|
|
||||||
### Contributing
|
|
||||||
|
|
||||||
Contributions are very welcome. Tests can be run with `tox`, please ensure
|
|
||||||
the coverage at least stays the same before you submit a pull request.
|
|
@@ -1,26 +0,0 @@
|
|||||||
aiofiles-24.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
aiofiles-24.1.0.dist-info/METADATA,sha256=CvUJx21XclgI1Lp5Bt_4AyJesRYg0xCSx4exJZVmaSA,10708
|
|
||||||
aiofiles-24.1.0.dist-info/RECORD,,
|
|
||||||
aiofiles-24.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
||||||
aiofiles-24.1.0.dist-info/licenses/LICENSE,sha256=y16Ofl9KOYjhBjwULGDcLfdWBfTEZRXnduOspt-XbhQ,11325
|
|
||||||
aiofiles-24.1.0.dist-info/licenses/NOTICE,sha256=EExY0dRQvWR0wJ2LZLwBgnM6YKw9jCU-M0zegpRSD_E,55
|
|
||||||
aiofiles/__init__.py,sha256=1iAMJQyJtX3LGIS0AoFTJeO1aJ_RK2jpBSBhg0VoIrE,344
|
|
||||||
aiofiles/__pycache__/__init__.cpython-313.pyc,,
|
|
||||||
aiofiles/__pycache__/base.cpython-313.pyc,,
|
|
||||||
aiofiles/__pycache__/os.cpython-313.pyc,,
|
|
||||||
aiofiles/__pycache__/ospath.cpython-313.pyc,,
|
|
||||||
aiofiles/base.py,sha256=zo0FgkCqZ5aosjvxqIvDf2t-RFg1Lc6X8P6rZ56p6fQ,1784
|
|
||||||
aiofiles/os.py,sha256=0DrsG-eH4h7xRzglv9pIWsQuzqe7ZhVYw5FQS18fIys,1153
|
|
||||||
aiofiles/ospath.py,sha256=WaYelz_k6ykAFRLStr4bqYIfCVQ-5GGzIqIizykbY2Q,794
|
|
||||||
aiofiles/tempfile/__init__.py,sha256=hFSNTOjOUv371Ozdfy6FIxeln46Nm3xOVh4ZR3Q94V0,10244
|
|
||||||
aiofiles/tempfile/__pycache__/__init__.cpython-313.pyc,,
|
|
||||||
aiofiles/tempfile/__pycache__/temptypes.cpython-313.pyc,,
|
|
||||||
aiofiles/tempfile/temptypes.py,sha256=ddEvNjMLVlr7WUILCe6ypTqw77yREeIonTk16Uw_NVs,2093
|
|
||||||
aiofiles/threadpool/__init__.py,sha256=kt0hwwx3bLiYtnA1SORhW8mJ6z4W9Xr7MbY80UIJJrI,3133
|
|
||||||
aiofiles/threadpool/__pycache__/__init__.cpython-313.pyc,,
|
|
||||||
aiofiles/threadpool/__pycache__/binary.cpython-313.pyc,,
|
|
||||||
aiofiles/threadpool/__pycache__/text.cpython-313.pyc,,
|
|
||||||
aiofiles/threadpool/__pycache__/utils.cpython-313.pyc,,
|
|
||||||
aiofiles/threadpool/binary.py,sha256=hp-km9VCRu0MLz_wAEUfbCz7OL7xtn9iGAawabpnp5U,2315
|
|
||||||
aiofiles/threadpool/text.py,sha256=fNmpw2PEkj0BZSldipJXAgZqVGLxALcfOMiuDQ54Eas,1223
|
|
||||||
aiofiles/threadpool/utils.py,sha256=B59dSZwO_WZs2dFFycKeA91iD2Xq2nNw1EFF8YMBI5k,1868
|
|
@@ -1,4 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: hatchling 1.25.0
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
@@ -1,202 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright {yyyy} {name of copyright owner}
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
@@ -1,2 +0,0 @@
|
|||||||
Asyncio support for files
|
|
||||||
Copyright 2016 Tin Tvrtkovic
|
|
@@ -1,22 +0,0 @@
|
|||||||
"""Utilities for asyncio-friendly file handling."""
|
|
||||||
from .threadpool import (
|
|
||||||
open,
|
|
||||||
stdin,
|
|
||||||
stdout,
|
|
||||||
stderr,
|
|
||||||
stdin_bytes,
|
|
||||||
stdout_bytes,
|
|
||||||
stderr_bytes,
|
|
||||||
)
|
|
||||||
from . import tempfile
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"open",
|
|
||||||
"tempfile",
|
|
||||||
"stdin",
|
|
||||||
"stdout",
|
|
||||||
"stderr",
|
|
||||||
"stdin_bytes",
|
|
||||||
"stdout_bytes",
|
|
||||||
"stderr_bytes",
|
|
||||||
]
|
|
@@ -1,69 +0,0 @@
|
|||||||
"""Various base classes."""
|
|
||||||
from collections.abc import Awaitable
|
|
||||||
from contextlib import AbstractAsyncContextManager
|
|
||||||
from asyncio import get_running_loop
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncBase:
|
|
||||||
def __init__(self, file, loop, executor):
|
|
||||||
self._file = file
|
|
||||||
self._executor = executor
|
|
||||||
self._ref_loop = loop
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _loop(self):
|
|
||||||
return self._ref_loop or get_running_loop()
|
|
||||||
|
|
||||||
def __aiter__(self):
|
|
||||||
"""We are our own iterator."""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return super().__repr__() + " wrapping " + repr(self._file)
|
|
||||||
|
|
||||||
async def __anext__(self):
|
|
||||||
"""Simulate normal file iteration."""
|
|
||||||
line = await self.readline()
|
|
||||||
if line:
|
|
||||||
return line
|
|
||||||
else:
|
|
||||||
raise StopAsyncIteration
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncIndirectBase(AsyncBase):
|
|
||||||
def __init__(self, name, loop, executor, indirect):
|
|
||||||
self._indirect = indirect
|
|
||||||
self._name = name
|
|
||||||
super().__init__(None, loop, executor)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _file(self):
|
|
||||||
return self._indirect()
|
|
||||||
|
|
||||||
@_file.setter
|
|
||||||
def _file(self, v):
|
|
||||||
pass # discard writes
|
|
||||||
|
|
||||||
|
|
||||||
class AiofilesContextManager(Awaitable, AbstractAsyncContextManager):
|
|
||||||
"""An adjusted async context manager for aiofiles."""
|
|
||||||
|
|
||||||
__slots__ = ("_coro", "_obj")
|
|
||||||
|
|
||||||
def __init__(self, coro):
|
|
||||||
self._coro = coro
|
|
||||||
self._obj = None
|
|
||||||
|
|
||||||
def __await__(self):
|
|
||||||
if self._obj is None:
|
|
||||||
self._obj = yield from self._coro.__await__()
|
|
||||||
return self._obj
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
return await self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
await get_running_loop().run_in_executor(
|
|
||||||
None, self._obj._file.__exit__, exc_type, exc_val, exc_tb
|
|
||||||
)
|
|
||||||
self._obj = None
|
|
@@ -1,58 +0,0 @@
|
|||||||
"""Async executor versions of file functions from the os module."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from . import ospath as path
|
|
||||||
from .ospath import wrap
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"path",
|
|
||||||
"stat",
|
|
||||||
"rename",
|
|
||||||
"renames",
|
|
||||||
"replace",
|
|
||||||
"remove",
|
|
||||||
"unlink",
|
|
||||||
"mkdir",
|
|
||||||
"makedirs",
|
|
||||||
"rmdir",
|
|
||||||
"removedirs",
|
|
||||||
"symlink",
|
|
||||||
"readlink",
|
|
||||||
"listdir",
|
|
||||||
"scandir",
|
|
||||||
"access",
|
|
||||||
"wrap",
|
|
||||||
"getcwd",
|
|
||||||
]
|
|
||||||
if hasattr(os, "link"):
|
|
||||||
__all__ += ["link"]
|
|
||||||
if hasattr(os, "sendfile"):
|
|
||||||
__all__ += ["sendfile"]
|
|
||||||
if hasattr(os, "statvfs"):
|
|
||||||
__all__ += ["statvfs"]
|
|
||||||
|
|
||||||
|
|
||||||
stat = wrap(os.stat)
|
|
||||||
rename = wrap(os.rename)
|
|
||||||
renames = wrap(os.renames)
|
|
||||||
replace = wrap(os.replace)
|
|
||||||
remove = wrap(os.remove)
|
|
||||||
unlink = wrap(os.unlink)
|
|
||||||
mkdir = wrap(os.mkdir)
|
|
||||||
makedirs = wrap(os.makedirs)
|
|
||||||
rmdir = wrap(os.rmdir)
|
|
||||||
removedirs = wrap(os.removedirs)
|
|
||||||
symlink = wrap(os.symlink)
|
|
||||||
readlink = wrap(os.readlink)
|
|
||||||
listdir = wrap(os.listdir)
|
|
||||||
scandir = wrap(os.scandir)
|
|
||||||
access = wrap(os.access)
|
|
||||||
getcwd = wrap(os.getcwd)
|
|
||||||
|
|
||||||
if hasattr(os, "link"):
|
|
||||||
link = wrap(os.link)
|
|
||||||
if hasattr(os, "sendfile"):
|
|
||||||
sendfile = wrap(os.sendfile)
|
|
||||||
if hasattr(os, "statvfs"):
|
|
||||||
statvfs = wrap(os.statvfs)
|
|
@@ -1,30 +0,0 @@
|
|||||||
"""Async executor versions of file functions from the os.path module."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from functools import partial, wraps
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
|
|
||||||
def wrap(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def run(*args, loop=None, executor=None, **kwargs):
|
|
||||||
if loop is None:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
pfunc = partial(func, *args, **kwargs)
|
|
||||||
return await loop.run_in_executor(executor, pfunc)
|
|
||||||
|
|
||||||
return run
|
|
||||||
|
|
||||||
|
|
||||||
exists = wrap(path.exists)
|
|
||||||
isfile = wrap(path.isfile)
|
|
||||||
isdir = wrap(path.isdir)
|
|
||||||
islink = wrap(path.islink)
|
|
||||||
ismount = wrap(path.ismount)
|
|
||||||
getsize = wrap(path.getsize)
|
|
||||||
getmtime = wrap(path.getmtime)
|
|
||||||
getatime = wrap(path.getatime)
|
|
||||||
getctime = wrap(path.getctime)
|
|
||||||
samefile = wrap(path.samefile)
|
|
||||||
sameopenfile = wrap(path.sameopenfile)
|
|
||||||
abspath = wrap(path.abspath)
|
|
@@ -1,357 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from functools import partial, singledispatch
|
|
||||||
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOBase
|
|
||||||
from tempfile import NamedTemporaryFile as syncNamedTemporaryFile
|
|
||||||
from tempfile import SpooledTemporaryFile as syncSpooledTemporaryFile
|
|
||||||
from tempfile import TemporaryDirectory as syncTemporaryDirectory
|
|
||||||
from tempfile import TemporaryFile as syncTemporaryFile
|
|
||||||
from tempfile import _TemporaryFileWrapper as syncTemporaryFileWrapper
|
|
||||||
|
|
||||||
from ..base import AiofilesContextManager
|
|
||||||
from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO
|
|
||||||
from ..threadpool.text import AsyncTextIOWrapper
|
|
||||||
from .temptypes import AsyncSpooledTemporaryFile, AsyncTemporaryDirectory
|
|
||||||
import sys
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"NamedTemporaryFile",
|
|
||||||
"TemporaryFile",
|
|
||||||
"SpooledTemporaryFile",
|
|
||||||
"TemporaryDirectory",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# Public methods for async open and return of temp file/directory
|
|
||||||
# objects with async interface
|
|
||||||
# ================================================================
|
|
||||||
if sys.version_info >= (3, 12):
|
|
||||||
|
|
||||||
def NamedTemporaryFile(
|
|
||||||
mode="w+b",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
newline=None,
|
|
||||||
suffix=None,
|
|
||||||
prefix=None,
|
|
||||||
dir=None,
|
|
||||||
delete=True,
|
|
||||||
delete_on_close=True,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
):
|
|
||||||
"""Async open a named temporary file"""
|
|
||||||
return AiofilesContextManager(
|
|
||||||
_temporary_file(
|
|
||||||
named=True,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
delete=delete,
|
|
||||||
delete_on_close=delete_on_close,
|
|
||||||
loop=loop,
|
|
||||||
executor=executor,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def NamedTemporaryFile(
|
|
||||||
mode="w+b",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
newline=None,
|
|
||||||
suffix=None,
|
|
||||||
prefix=None,
|
|
||||||
dir=None,
|
|
||||||
delete=True,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
):
|
|
||||||
"""Async open a named temporary file"""
|
|
||||||
return AiofilesContextManager(
|
|
||||||
_temporary_file(
|
|
||||||
named=True,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
delete=delete,
|
|
||||||
loop=loop,
|
|
||||||
executor=executor,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def TemporaryFile(
|
|
||||||
mode="w+b",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
newline=None,
|
|
||||||
suffix=None,
|
|
||||||
prefix=None,
|
|
||||||
dir=None,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
):
|
|
||||||
"""Async open an unnamed temporary file"""
|
|
||||||
return AiofilesContextManager(
|
|
||||||
_temporary_file(
|
|
||||||
named=False,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
loop=loop,
|
|
||||||
executor=executor,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def SpooledTemporaryFile(
|
|
||||||
max_size=0,
|
|
||||||
mode="w+b",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
newline=None,
|
|
||||||
suffix=None,
|
|
||||||
prefix=None,
|
|
||||||
dir=None,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
):
|
|
||||||
"""Async open a spooled temporary file"""
|
|
||||||
return AiofilesContextManager(
|
|
||||||
_spooled_temporary_file(
|
|
||||||
max_size=max_size,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
loop=loop,
|
|
||||||
executor=executor,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def TemporaryDirectory(suffix=None, prefix=None, dir=None, loop=None, executor=None):
|
|
||||||
"""Async open a temporary directory"""
|
|
||||||
return AiofilesContextManagerTempDir(
|
|
||||||
_temporary_directory(
|
|
||||||
suffix=suffix, prefix=prefix, dir=dir, loop=loop, executor=executor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# =========================================================
|
|
||||||
# Internal coroutines to open new temp files/directories
|
|
||||||
# =========================================================
|
|
||||||
if sys.version_info >= (3, 12):
|
|
||||||
|
|
||||||
async def _temporary_file(
|
|
||||||
named=True,
|
|
||||||
mode="w+b",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
newline=None,
|
|
||||||
suffix=None,
|
|
||||||
prefix=None,
|
|
||||||
dir=None,
|
|
||||||
delete=True,
|
|
||||||
delete_on_close=True,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
max_size=0,
|
|
||||||
):
|
|
||||||
"""Async method to open a temporary file with async interface"""
|
|
||||||
if loop is None:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
|
|
||||||
if named:
|
|
||||||
cb = partial(
|
|
||||||
syncNamedTemporaryFile,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
delete=delete,
|
|
||||||
delete_on_close=delete_on_close,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cb = partial(
|
|
||||||
syncTemporaryFile,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
f = await loop.run_in_executor(executor, cb)
|
|
||||||
|
|
||||||
# Wrap based on type of underlying IO object
|
|
||||||
if type(f) is syncTemporaryFileWrapper:
|
|
||||||
# _TemporaryFileWrapper was used (named files)
|
|
||||||
result = wrap(f.file, f, loop=loop, executor=executor)
|
|
||||||
result._closer = f._closer
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
# IO object was returned directly without wrapper
|
|
||||||
return wrap(f, f, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
async def _temporary_file(
|
|
||||||
named=True,
|
|
||||||
mode="w+b",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
newline=None,
|
|
||||||
suffix=None,
|
|
||||||
prefix=None,
|
|
||||||
dir=None,
|
|
||||||
delete=True,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
max_size=0,
|
|
||||||
):
|
|
||||||
"""Async method to open a temporary file with async interface"""
|
|
||||||
if loop is None:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
|
|
||||||
if named:
|
|
||||||
cb = partial(
|
|
||||||
syncNamedTemporaryFile,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
delete=delete,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cb = partial(
|
|
||||||
syncTemporaryFile,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
f = await loop.run_in_executor(executor, cb)
|
|
||||||
|
|
||||||
# Wrap based on type of underlying IO object
|
|
||||||
if type(f) is syncTemporaryFileWrapper:
|
|
||||||
# _TemporaryFileWrapper was used (named files)
|
|
||||||
result = wrap(f.file, f, loop=loop, executor=executor)
|
|
||||||
# add delete property
|
|
||||||
result.delete = f.delete
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
# IO object was returned directly without wrapper
|
|
||||||
return wrap(f, f, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
async def _spooled_temporary_file(
|
|
||||||
max_size=0,
|
|
||||||
mode="w+b",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
newline=None,
|
|
||||||
suffix=None,
|
|
||||||
prefix=None,
|
|
||||||
dir=None,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
):
|
|
||||||
"""Open a spooled temporary file with async interface"""
|
|
||||||
if loop is None:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
|
|
||||||
cb = partial(
|
|
||||||
syncSpooledTemporaryFile,
|
|
||||||
max_size=max_size,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
newline=newline,
|
|
||||||
suffix=suffix,
|
|
||||||
prefix=prefix,
|
|
||||||
dir=dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
f = await loop.run_in_executor(executor, cb)
|
|
||||||
|
|
||||||
# Single interface provided by SpooledTemporaryFile for all modes
|
|
||||||
return AsyncSpooledTemporaryFile(f, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
async def _temporary_directory(
|
|
||||||
suffix=None, prefix=None, dir=None, loop=None, executor=None
|
|
||||||
):
|
|
||||||
"""Async method to open a temporary directory with async interface"""
|
|
||||||
if loop is None:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
|
|
||||||
cb = partial(syncTemporaryDirectory, suffix, prefix, dir)
|
|
||||||
f = await loop.run_in_executor(executor, cb)
|
|
||||||
|
|
||||||
return AsyncTemporaryDirectory(f, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
class AiofilesContextManagerTempDir(AiofilesContextManager):
|
|
||||||
"""With returns the directory location, not the object (matching sync lib)"""
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
self._obj = await self._coro
|
|
||||||
return self._obj.name
|
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
|
||||||
def wrap(base_io_obj, file, *, loop=None, executor=None):
|
|
||||||
"""Wrap the object with interface based on type of underlying IO"""
|
|
||||||
raise TypeError("Unsupported IO type: {}".format(base_io_obj))
|
|
||||||
|
|
||||||
|
|
||||||
@wrap.register(TextIOBase)
|
|
||||||
def _(base_io_obj, file, *, loop=None, executor=None):
|
|
||||||
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
@wrap.register(BufferedWriter)
|
|
||||||
def _(base_io_obj, file, *, loop=None, executor=None):
|
|
||||||
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
@wrap.register(BufferedReader)
|
|
||||||
@wrap.register(BufferedRandom)
|
|
||||||
def _(base_io_obj, file, *, loop=None, executor=None):
|
|
||||||
return AsyncBufferedReader(file, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
@wrap.register(FileIO)
|
|
||||||
def _(base_io_obj, file, *, loop=None, executor=None):
|
|
||||||
return AsyncFileIO(file, loop=loop, executor=executor)
|
|
@@ -1,69 +0,0 @@
|
|||||||
"""Async wrappers for spooled temp files and temp directory objects"""
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from ..base import AsyncBase
|
|
||||||
from ..threadpool.utils import (
|
|
||||||
cond_delegate_to_executor,
|
|
||||||
delegate_to_executor,
|
|
||||||
proxy_property_directly,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor("fileno", "rollover")
|
|
||||||
@cond_delegate_to_executor(
|
|
||||||
"close",
|
|
||||||
"flush",
|
|
||||||
"isatty",
|
|
||||||
"read",
|
|
||||||
"readline",
|
|
||||||
"readlines",
|
|
||||||
"seek",
|
|
||||||
"tell",
|
|
||||||
"truncate",
|
|
||||||
)
|
|
||||||
@proxy_property_directly("closed", "encoding", "mode", "name", "newlines")
|
|
||||||
class AsyncSpooledTemporaryFile(AsyncBase):
|
|
||||||
"""Async wrapper for SpooledTemporaryFile class"""
|
|
||||||
|
|
||||||
async def _check(self):
|
|
||||||
if self._file._rolled:
|
|
||||||
return
|
|
||||||
max_size = self._file._max_size
|
|
||||||
if max_size and self._file.tell() > max_size:
|
|
||||||
await self.rollover()
|
|
||||||
|
|
||||||
async def write(self, s):
|
|
||||||
"""Implementation to anticipate rollover"""
|
|
||||||
if self._file._rolled:
|
|
||||||
cb = partial(self._file.write, s)
|
|
||||||
return await self._loop.run_in_executor(self._executor, cb)
|
|
||||||
else:
|
|
||||||
file = self._file._file # reference underlying base IO object
|
|
||||||
rv = file.write(s)
|
|
||||||
await self._check()
|
|
||||||
return rv
|
|
||||||
|
|
||||||
async def writelines(self, iterable):
|
|
||||||
"""Implementation to anticipate rollover"""
|
|
||||||
if self._file._rolled:
|
|
||||||
cb = partial(self._file.writelines, iterable)
|
|
||||||
return await self._loop.run_in_executor(self._executor, cb)
|
|
||||||
else:
|
|
||||||
file = self._file._file # reference underlying base IO object
|
|
||||||
rv = file.writelines(iterable)
|
|
||||||
await self._check()
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor("cleanup")
|
|
||||||
@proxy_property_directly("name")
|
|
||||||
class AsyncTemporaryDirectory:
|
|
||||||
"""Async wrapper for TemporaryDirectory class"""
|
|
||||||
|
|
||||||
def __init__(self, file, loop, executor):
|
|
||||||
self._file = file
|
|
||||||
self._loop = loop
|
|
||||||
self._executor = executor
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
await self.cleanup()
|
|
@@ -1,139 +0,0 @@
|
|||||||
"""Handle files using a thread pool executor."""
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
from functools import partial, singledispatch
|
|
||||||
from io import (
|
|
||||||
BufferedIOBase,
|
|
||||||
BufferedRandom,
|
|
||||||
BufferedReader,
|
|
||||||
BufferedWriter,
|
|
||||||
FileIO,
|
|
||||||
TextIOBase,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ..base import AiofilesContextManager
|
|
||||||
from .binary import (
|
|
||||||
AsyncBufferedIOBase,
|
|
||||||
AsyncBufferedReader,
|
|
||||||
AsyncFileIO,
|
|
||||||
AsyncIndirectBufferedIOBase,
|
|
||||||
)
|
|
||||||
from .text import AsyncTextIndirectIOWrapper, AsyncTextIOWrapper
|
|
||||||
|
|
||||||
sync_open = open
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"open",
|
|
||||||
"stdin",
|
|
||||||
"stdout",
|
|
||||||
"stderr",
|
|
||||||
"stdin_bytes",
|
|
||||||
"stdout_bytes",
|
|
||||||
"stderr_bytes",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def open(
|
|
||||||
file,
|
|
||||||
mode="r",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
errors=None,
|
|
||||||
newline=None,
|
|
||||||
closefd=True,
|
|
||||||
opener=None,
|
|
||||||
*,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
):
|
|
||||||
return AiofilesContextManager(
|
|
||||||
_open(
|
|
||||||
file,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
errors=errors,
|
|
||||||
newline=newline,
|
|
||||||
closefd=closefd,
|
|
||||||
opener=opener,
|
|
||||||
loop=loop,
|
|
||||||
executor=executor,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _open(
|
|
||||||
file,
|
|
||||||
mode="r",
|
|
||||||
buffering=-1,
|
|
||||||
encoding=None,
|
|
||||||
errors=None,
|
|
||||||
newline=None,
|
|
||||||
closefd=True,
|
|
||||||
opener=None,
|
|
||||||
*,
|
|
||||||
loop=None,
|
|
||||||
executor=None,
|
|
||||||
):
|
|
||||||
"""Open an asyncio file."""
|
|
||||||
if loop is None:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
cb = partial(
|
|
||||||
sync_open,
|
|
||||||
file,
|
|
||||||
mode=mode,
|
|
||||||
buffering=buffering,
|
|
||||||
encoding=encoding,
|
|
||||||
errors=errors,
|
|
||||||
newline=newline,
|
|
||||||
closefd=closefd,
|
|
||||||
opener=opener,
|
|
||||||
)
|
|
||||||
f = await loop.run_in_executor(executor, cb)
|
|
||||||
|
|
||||||
return wrap(f, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
|
||||||
def wrap(file, *, loop=None, executor=None):
|
|
||||||
raise TypeError("Unsupported io type: {}.".format(file))
|
|
||||||
|
|
||||||
|
|
||||||
@wrap.register(TextIOBase)
|
|
||||||
def _(file, *, loop=None, executor=None):
|
|
||||||
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
@wrap.register(BufferedWriter)
|
|
||||||
@wrap.register(BufferedIOBase)
|
|
||||||
def _(file, *, loop=None, executor=None):
|
|
||||||
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
@wrap.register(BufferedReader)
|
|
||||||
@wrap.register(BufferedRandom)
|
|
||||||
def _(file, *, loop=None, executor=None):
|
|
||||||
return AsyncBufferedReader(file, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
@wrap.register(FileIO)
|
|
||||||
def _(file, *, loop=None, executor=None):
|
|
||||||
return AsyncFileIO(file, loop=loop, executor=executor)
|
|
||||||
|
|
||||||
|
|
||||||
stdin = AsyncTextIndirectIOWrapper("sys.stdin", None, None, indirect=lambda: sys.stdin)
|
|
||||||
stdout = AsyncTextIndirectIOWrapper(
|
|
||||||
"sys.stdout", None, None, indirect=lambda: sys.stdout
|
|
||||||
)
|
|
||||||
stderr = AsyncTextIndirectIOWrapper(
|
|
||||||
"sys.stderr", None, None, indirect=lambda: sys.stderr
|
|
||||||
)
|
|
||||||
stdin_bytes = AsyncIndirectBufferedIOBase(
|
|
||||||
"sys.stdin.buffer", None, None, indirect=lambda: sys.stdin.buffer
|
|
||||||
)
|
|
||||||
stdout_bytes = AsyncIndirectBufferedIOBase(
|
|
||||||
"sys.stdout.buffer", None, None, indirect=lambda: sys.stdout.buffer
|
|
||||||
)
|
|
||||||
stderr_bytes = AsyncIndirectBufferedIOBase(
|
|
||||||
"sys.stderr.buffer", None, None, indirect=lambda: sys.stderr.buffer
|
|
||||||
)
|
|
@@ -1,104 +0,0 @@
|
|||||||
from ..base import AsyncBase, AsyncIndirectBase
|
|
||||||
from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor(
|
|
||||||
"close",
|
|
||||||
"flush",
|
|
||||||
"isatty",
|
|
||||||
"read",
|
|
||||||
"read1",
|
|
||||||
"readinto",
|
|
||||||
"readline",
|
|
||||||
"readlines",
|
|
||||||
"seek",
|
|
||||||
"seekable",
|
|
||||||
"tell",
|
|
||||||
"truncate",
|
|
||||||
"writable",
|
|
||||||
"write",
|
|
||||||
"writelines",
|
|
||||||
)
|
|
||||||
@proxy_method_directly("detach", "fileno", "readable")
|
|
||||||
@proxy_property_directly("closed", "raw", "name", "mode")
|
|
||||||
class AsyncBufferedIOBase(AsyncBase):
|
|
||||||
"""The asyncio executor version of io.BufferedWriter and BufferedIOBase."""
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor("peek")
|
|
||||||
class AsyncBufferedReader(AsyncBufferedIOBase):
|
|
||||||
"""The asyncio executor version of io.BufferedReader and Random."""
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor(
|
|
||||||
"close",
|
|
||||||
"flush",
|
|
||||||
"isatty",
|
|
||||||
"read",
|
|
||||||
"readall",
|
|
||||||
"readinto",
|
|
||||||
"readline",
|
|
||||||
"readlines",
|
|
||||||
"seek",
|
|
||||||
"seekable",
|
|
||||||
"tell",
|
|
||||||
"truncate",
|
|
||||||
"writable",
|
|
||||||
"write",
|
|
||||||
"writelines",
|
|
||||||
)
|
|
||||||
@proxy_method_directly("fileno", "readable")
|
|
||||||
@proxy_property_directly("closed", "name", "mode")
|
|
||||||
class AsyncFileIO(AsyncBase):
|
|
||||||
"""The asyncio executor version of io.FileIO."""
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor(
|
|
||||||
"close",
|
|
||||||
"flush",
|
|
||||||
"isatty",
|
|
||||||
"read",
|
|
||||||
"read1",
|
|
||||||
"readinto",
|
|
||||||
"readline",
|
|
||||||
"readlines",
|
|
||||||
"seek",
|
|
||||||
"seekable",
|
|
||||||
"tell",
|
|
||||||
"truncate",
|
|
||||||
"writable",
|
|
||||||
"write",
|
|
||||||
"writelines",
|
|
||||||
)
|
|
||||||
@proxy_method_directly("detach", "fileno", "readable")
|
|
||||||
@proxy_property_directly("closed", "raw", "name", "mode")
|
|
||||||
class AsyncIndirectBufferedIOBase(AsyncIndirectBase):
|
|
||||||
"""The indirect asyncio executor version of io.BufferedWriter and BufferedIOBase."""
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor("peek")
|
|
||||||
class AsyncIndirectBufferedReader(AsyncIndirectBufferedIOBase):
|
|
||||||
"""The indirect asyncio executor version of io.BufferedReader and Random."""
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor(
|
|
||||||
"close",
|
|
||||||
"flush",
|
|
||||||
"isatty",
|
|
||||||
"read",
|
|
||||||
"readall",
|
|
||||||
"readinto",
|
|
||||||
"readline",
|
|
||||||
"readlines",
|
|
||||||
"seek",
|
|
||||||
"seekable",
|
|
||||||
"tell",
|
|
||||||
"truncate",
|
|
||||||
"writable",
|
|
||||||
"write",
|
|
||||||
"writelines",
|
|
||||||
)
|
|
||||||
@proxy_method_directly("fileno", "readable")
|
|
||||||
@proxy_property_directly("closed", "name", "mode")
|
|
||||||
class AsyncIndirectFileIO(AsyncIndirectBase):
|
|
||||||
"""The indirect asyncio executor version of io.FileIO."""
|
|
@@ -1,64 +0,0 @@
|
|||||||
from ..base import AsyncBase, AsyncIndirectBase
|
|
||||||
from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor(
|
|
||||||
"close",
|
|
||||||
"flush",
|
|
||||||
"isatty",
|
|
||||||
"read",
|
|
||||||
"readable",
|
|
||||||
"readline",
|
|
||||||
"readlines",
|
|
||||||
"seek",
|
|
||||||
"seekable",
|
|
||||||
"tell",
|
|
||||||
"truncate",
|
|
||||||
"write",
|
|
||||||
"writable",
|
|
||||||
"writelines",
|
|
||||||
)
|
|
||||||
@proxy_method_directly("detach", "fileno", "readable")
|
|
||||||
@proxy_property_directly(
|
|
||||||
"buffer",
|
|
||||||
"closed",
|
|
||||||
"encoding",
|
|
||||||
"errors",
|
|
||||||
"line_buffering",
|
|
||||||
"newlines",
|
|
||||||
"name",
|
|
||||||
"mode",
|
|
||||||
)
|
|
||||||
class AsyncTextIOWrapper(AsyncBase):
|
|
||||||
"""The asyncio executor version of io.TextIOWrapper."""
|
|
||||||
|
|
||||||
|
|
||||||
@delegate_to_executor(
|
|
||||||
"close",
|
|
||||||
"flush",
|
|
||||||
"isatty",
|
|
||||||
"read",
|
|
||||||
"readable",
|
|
||||||
"readline",
|
|
||||||
"readlines",
|
|
||||||
"seek",
|
|
||||||
"seekable",
|
|
||||||
"tell",
|
|
||||||
"truncate",
|
|
||||||
"write",
|
|
||||||
"writable",
|
|
||||||
"writelines",
|
|
||||||
)
|
|
||||||
@proxy_method_directly("detach", "fileno", "readable")
|
|
||||||
@proxy_property_directly(
|
|
||||||
"buffer",
|
|
||||||
"closed",
|
|
||||||
"encoding",
|
|
||||||
"errors",
|
|
||||||
"line_buffering",
|
|
||||||
"newlines",
|
|
||||||
"name",
|
|
||||||
"mode",
|
|
||||||
)
|
|
||||||
class AsyncTextIndirectIOWrapper(AsyncIndirectBase):
|
|
||||||
"""The indirect asyncio executor version of io.TextIOWrapper."""
|
|
@@ -1,72 +0,0 @@
|
|||||||
import functools
|
|
||||||
|
|
||||||
|
|
||||||
def delegate_to_executor(*attrs):
|
|
||||||
def cls_builder(cls):
|
|
||||||
for attr_name in attrs:
|
|
||||||
setattr(cls, attr_name, _make_delegate_method(attr_name))
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return cls_builder
|
|
||||||
|
|
||||||
|
|
||||||
def proxy_method_directly(*attrs):
|
|
||||||
def cls_builder(cls):
|
|
||||||
for attr_name in attrs:
|
|
||||||
setattr(cls, attr_name, _make_proxy_method(attr_name))
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return cls_builder
|
|
||||||
|
|
||||||
|
|
||||||
def proxy_property_directly(*attrs):
|
|
||||||
def cls_builder(cls):
|
|
||||||
for attr_name in attrs:
|
|
||||||
setattr(cls, attr_name, _make_proxy_property(attr_name))
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return cls_builder
|
|
||||||
|
|
||||||
|
|
||||||
def cond_delegate_to_executor(*attrs):
|
|
||||||
def cls_builder(cls):
|
|
||||||
for attr_name in attrs:
|
|
||||||
setattr(cls, attr_name, _make_cond_delegate_method(attr_name))
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return cls_builder
|
|
||||||
|
|
||||||
|
|
||||||
def _make_delegate_method(attr_name):
|
|
||||||
async def method(self, *args, **kwargs):
|
|
||||||
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
|
|
||||||
return await self._loop.run_in_executor(self._executor, cb)
|
|
||||||
|
|
||||||
return method
|
|
||||||
|
|
||||||
|
|
||||||
def _make_proxy_method(attr_name):
|
|
||||||
def method(self, *args, **kwargs):
|
|
||||||
return getattr(self._file, attr_name)(*args, **kwargs)
|
|
||||||
|
|
||||||
return method
|
|
||||||
|
|
||||||
|
|
||||||
def _make_proxy_property(attr_name):
|
|
||||||
def proxy_property(self):
|
|
||||||
return getattr(self._file, attr_name)
|
|
||||||
|
|
||||||
return property(proxy_property)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_cond_delegate_method(attr_name):
|
|
||||||
"""For spooled temp files, delegate only if rolled to file object"""
|
|
||||||
|
|
||||||
async def method(self, *args, **kwargs):
|
|
||||||
if self._file._rolled:
|
|
||||||
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
|
|
||||||
return await self._loop.run_in_executor(self._executor, cb)
|
|
||||||
else:
|
|
||||||
return getattr(self._file, attr_name)(*args, **kwargs)
|
|
||||||
|
|
||||||
return method
|
|
@@ -1 +0,0 @@
|
|||||||
pip
|
|
@@ -1,164 +0,0 @@
|
|||||||
Metadata-Version: 2.4
|
|
||||||
Name: aiogram
|
|
||||||
Version: 3.20.0.post0
|
|
||||||
Summary: Modern and fully asynchronous framework for Telegram Bot API
|
|
||||||
Project-URL: Homepage, https://aiogram.dev/
|
|
||||||
Project-URL: Documentation, https://docs.aiogram.dev/
|
|
||||||
Project-URL: Repository, https://github.com/aiogram/aiogram/
|
|
||||||
Author-email: Alex Root Junior <jroot.junior@gmail.com>
|
|
||||||
Maintainer-email: Alex Root Junior <jroot.junior@gmail.com>
|
|
||||||
License-Expression: MIT
|
|
||||||
License-File: LICENSE
|
|
||||||
Keywords: api,asyncio,bot,framework,telegram,wrapper
|
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
|
||||||
Classifier: Environment :: Console
|
|
||||||
Classifier: Framework :: AsyncIO
|
|
||||||
Classifier: Intended Audience :: Developers
|
|
||||||
Classifier: Intended Audience :: System Administrators
|
|
||||||
Classifier: License :: OSI Approved :: MIT License
|
|
||||||
Classifier: Programming Language :: Python :: 3.9
|
|
||||||
Classifier: Programming Language :: Python :: 3.10
|
|
||||||
Classifier: Programming Language :: Python :: 3.11
|
|
||||||
Classifier: Programming Language :: Python :: 3.12
|
|
||||||
Classifier: Programming Language :: Python :: 3.13
|
|
||||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
||||||
Classifier: Topic :: Communications :: Chat
|
|
||||||
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
||||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
||||||
Classifier: Typing :: Typed
|
|
||||||
Requires-Python: >=3.9
|
|
||||||
Requires-Dist: aiofiles<24.2,>=23.2.1
|
|
||||||
Requires-Dist: aiohttp<3.12,>=3.9.0
|
|
||||||
Requires-Dist: certifi>=2023.7.22
|
|
||||||
Requires-Dist: magic-filter<1.1,>=1.0.12
|
|
||||||
Requires-Dist: pydantic<2.12,>=2.4.1
|
|
||||||
Requires-Dist: typing-extensions<=5.0,>=4.7.0
|
|
||||||
Provides-Extra: cli
|
|
||||||
Requires-Dist: aiogram-cli<2.0.0,>=1.1.0; extra == 'cli'
|
|
||||||
Provides-Extra: dev
|
|
||||||
Requires-Dist: black~=24.4.2; extra == 'dev'
|
|
||||||
Requires-Dist: isort~=5.13.2; extra == 'dev'
|
|
||||||
Requires-Dist: motor-types~=1.0.0b4; extra == 'dev'
|
|
||||||
Requires-Dist: mypy~=1.10.0; extra == 'dev'
|
|
||||||
Requires-Dist: packaging~=24.1; extra == 'dev'
|
|
||||||
Requires-Dist: pre-commit~=3.5; extra == 'dev'
|
|
||||||
Requires-Dist: ruff~=0.5.1; extra == 'dev'
|
|
||||||
Requires-Dist: toml~=0.10.2; extra == 'dev'
|
|
||||||
Provides-Extra: docs
|
|
||||||
Requires-Dist: furo~=2024.8.6; extra == 'docs'
|
|
||||||
Requires-Dist: markdown-include~=0.8.1; extra == 'docs'
|
|
||||||
Requires-Dist: pygments~=2.18.0; extra == 'docs'
|
|
||||||
Requires-Dist: pymdown-extensions~=10.3; extra == 'docs'
|
|
||||||
Requires-Dist: sphinx-autobuild~=2024.9.3; extra == 'docs'
|
|
||||||
Requires-Dist: sphinx-copybutton~=0.5.2; extra == 'docs'
|
|
||||||
Requires-Dist: sphinx-intl~=2.2.0; extra == 'docs'
|
|
||||||
Requires-Dist: sphinx-substitution-extensions~=2024.8.6; extra == 'docs'
|
|
||||||
Requires-Dist: sphinxcontrib-towncrier~=0.4.0a0; extra == 'docs'
|
|
||||||
Requires-Dist: sphinx~=8.0.2; extra == 'docs'
|
|
||||||
Requires-Dist: towncrier~=24.8.0; extra == 'docs'
|
|
||||||
Provides-Extra: fast
|
|
||||||
Requires-Dist: aiodns>=3.0.0; extra == 'fast'
|
|
||||||
Requires-Dist: uvloop>=0.17.0; ((sys_platform == 'darwin' or sys_platform == 'linux') and platform_python_implementation != 'PyPy' and python_version < '3.13') and extra == 'fast'
|
|
||||||
Requires-Dist: uvloop>=0.21.0; ((sys_platform == 'darwin' or sys_platform == 'linux') and platform_python_implementation != 'PyPy' and python_version >= '3.13') and extra == 'fast'
|
|
||||||
Provides-Extra: i18n
|
|
||||||
Requires-Dist: babel~=2.13.0; extra == 'i18n'
|
|
||||||
Provides-Extra: mongo
|
|
||||||
Requires-Dist: motor<3.7.0,>=3.3.2; extra == 'mongo'
|
|
||||||
Provides-Extra: proxy
|
|
||||||
Requires-Dist: aiohttp-socks~=0.8.3; extra == 'proxy'
|
|
||||||
Provides-Extra: redis
|
|
||||||
Requires-Dist: redis[hiredis]<5.3.0,>=5.0.1; extra == 'redis'
|
|
||||||
Provides-Extra: test
|
|
||||||
Requires-Dist: aresponses~=2.1.6; extra == 'test'
|
|
||||||
Requires-Dist: pycryptodomex~=3.19.0; extra == 'test'
|
|
||||||
Requires-Dist: pytest-aiohttp~=1.0.5; extra == 'test'
|
|
||||||
Requires-Dist: pytest-asyncio~=0.21.1; extra == 'test'
|
|
||||||
Requires-Dist: pytest-cov~=4.1.0; extra == 'test'
|
|
||||||
Requires-Dist: pytest-html~=4.0.2; extra == 'test'
|
|
||||||
Requires-Dist: pytest-lazy-fixture~=0.6.3; extra == 'test'
|
|
||||||
Requires-Dist: pytest-mock~=3.12.0; extra == 'test'
|
|
||||||
Requires-Dist: pytest-mypy~=0.10.3; extra == 'test'
|
|
||||||
Requires-Dist: pytest~=7.4.2; extra == 'test'
|
|
||||||
Requires-Dist: pytz~=2023.3; extra == 'test'
|
|
||||||
Description-Content-Type: text/x-rst
|
|
||||||
|
|
||||||
#######
|
|
||||||
aiogram
|
|
||||||
#######
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/l/aiogram.svg?style=flat-square
|
|
||||||
:target: https://opensource.org/licenses/MIT
|
|
||||||
:alt: MIT License
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/status/aiogram.svg?style=flat-square
|
|
||||||
:target: https://pypi.python.org/pypi/aiogram
|
|
||||||
:alt: PyPi status
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/v/aiogram.svg?style=flat-square
|
|
||||||
:target: https://pypi.python.org/pypi/aiogram
|
|
||||||
:alt: PyPi Package Version
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square
|
|
||||||
:target: https://pypi.python.org/pypi/aiogram
|
|
||||||
:alt: Downloads
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square
|
|
||||||
:target: https://pypi.python.org/pypi/aiogram
|
|
||||||
:alt: Supported python versions
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/dynamic/json?color=blue&logo=telegram&label=Telegram%20Bot%20API&query=%24.api.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Faiogram%2Faiogram%2Fdev-3.x%2F.butcher%2Fschema%2Fschema.json&style=flat-square
|
|
||||||
:target: https://core.telegram.org/bots/api
|
|
||||||
:alt: Telegram Bot API
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/github/actions/workflow/status/aiogram/aiogram/tests.yml?branch=dev-3.x&style=flat-square
|
|
||||||
:target: https://github.com/aiogram/aiogram/actions
|
|
||||||
:alt: Tests
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/codecov/c/github/aiogram/aiogram?style=flat-square
|
|
||||||
:target: https://app.codecov.io/gh/aiogram/aiogram
|
|
||||||
:alt: Codecov
|
|
||||||
|
|
||||||
**aiogram** is a modern and fully asynchronous framework for
|
|
||||||
`Telegram Bot API <https://core.telegram.org/bots/api>`_ written in Python 3.8+ using
|
|
||||||
`asyncio <https://docs.python.org/3/library/asyncio.html>`_ and
|
|
||||||
`aiohttp <https://github.com/aio-libs/aiohttp>`_.
|
|
||||||
|
|
||||||
Make your bots faster and more powerful!
|
|
||||||
|
|
||||||
Documentation:
|
|
||||||
- 🇺🇸 `English <https://docs.aiogram.dev/en/dev-3.x/>`_
|
|
||||||
- 🇺🇦 `Ukrainian <https://docs.aiogram.dev/uk_UA/dev-3.x/>`_
|
|
||||||
|
|
||||||
|
|
||||||
Features
|
|
||||||
========
|
|
||||||
|
|
||||||
- Asynchronous (`asyncio docs <https://docs.python.org/3/library/asyncio.html>`_, :pep:`492`)
|
|
||||||
- Has type hints (:pep:`484`) and can be used with `mypy <http://mypy-lang.org/>`_
|
|
||||||
- Supports `PyPy <https://www.pypy.org/>`_
|
|
||||||
- Supports `Telegram Bot API 9.0 <https://core.telegram.org/bots/api>`_ and gets fast updates to the latest versions of the Bot API
|
|
||||||
- Telegram Bot API integration code was `autogenerated <https://github.com/aiogram/tg-codegen>`_ and can be easily re-generated when API gets updated
|
|
||||||
- Updates router (Blueprints)
|
|
||||||
- Has Finite State Machine
|
|
||||||
- Uses powerful `magic filters <https://docs.aiogram.dev/en/latest/dispatcher/filters/magic_filters.html#magic-filters>`_
|
|
||||||
- Middlewares (incoming updates and API calls)
|
|
||||||
- Provides `Replies into Webhook <https://core.telegram.org/bots/faq#how-can-i-make-requests-in-response-to-updates>`_
|
|
||||||
- Integrated I18n/L10n support with GNU Gettext (or Fluent)
|
|
||||||
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
It is strongly advised that you have prior experience working
|
|
||||||
with `asyncio <https://docs.python.org/3/library/asyncio.html>`_
|
|
||||||
before beginning to use **aiogram**.
|
|
||||||
|
|
||||||
If you have any questions, you can visit our community chats on Telegram:
|
|
||||||
|
|
||||||
- 🇺🇸 `@aiogram <https://t.me/aiogram>`_
|
|
||||||
- 🇺🇦 `@aiogramua <https://t.me/aiogramua>`_
|
|
||||||
- 🇺🇿 `@aiogram_uz <https://t.me/aiogram_uz>`_
|
|
||||||
- 🇰🇿 `@aiogram_kz <https://t.me/aiogram_kz>`_
|
|
||||||
- 🇷🇺 `@aiogram_ru <https://t.me/aiogram_ru>`_
|
|
||||||
- 🇮🇷 `@aiogram_fa <https://t.me/aiogram_fa>`_
|
|
||||||
- 🇮🇹 `@aiogram_it <https://t.me/aiogram_it>`_
|
|
||||||
- 🇧🇷 `@aiogram_br <https://t.me/aiogram_br>`_
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: hatchling 1.27.0
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
@@ -1,18 +0,0 @@
|
|||||||
Copyright (c) 2017 - present Alex Root Junior
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
||||||
software and associated documentation files (the "Software"), to deal in the Software
|
|
||||||
without restriction, including without limitation the rights to use, copy, modify,
|
|
||||||
merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
|
||||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
|
||||||
following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies
|
|
||||||
or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
||||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
||||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -1,41 +0,0 @@
|
|||||||
import asyncio as _asyncio
|
|
||||||
from contextlib import suppress
|
|
||||||
|
|
||||||
from aiogram.dispatcher.flags import FlagGenerator
|
|
||||||
|
|
||||||
from . import enums, methods, types
|
|
||||||
from .__meta__ import __api_version__, __version__
|
|
||||||
from .client import session
|
|
||||||
from .client.bot import Bot
|
|
||||||
from .dispatcher.dispatcher import Dispatcher
|
|
||||||
from .dispatcher.middlewares.base import BaseMiddleware
|
|
||||||
from .dispatcher.router import Router
|
|
||||||
from .utils.magic_filter import MagicFilter
|
|
||||||
from .utils.text_decorations import html_decoration as html
|
|
||||||
from .utils.text_decorations import markdown_decoration as md
|
|
||||||
|
|
||||||
with suppress(ImportError):
|
|
||||||
import uvloop as _uvloop
|
|
||||||
|
|
||||||
_asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy())
|
|
||||||
|
|
||||||
|
|
||||||
F = MagicFilter()
|
|
||||||
flags = FlagGenerator()
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"__api_version__",
|
|
||||||
"__version__",
|
|
||||||
"types",
|
|
||||||
"methods",
|
|
||||||
"enums",
|
|
||||||
"Bot",
|
|
||||||
"session",
|
|
||||||
"Dispatcher",
|
|
||||||
"Router",
|
|
||||||
"BaseMiddleware",
|
|
||||||
"F",
|
|
||||||
"html",
|
|
||||||
"md",
|
|
||||||
"flags",
|
|
||||||
)
|
|
@@ -1,2 +0,0 @@
|
|||||||
__version__ = "3.20.0.post0"
|
|
||||||
__api_version__ = "9.0"
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
|||||||
from typing import TYPE_CHECKING, Any, Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, PrivateAttr
|
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from aiogram.client.bot import Bot
|
|
||||||
|
|
||||||
|
|
||||||
class BotContextController(BaseModel):
|
|
||||||
_bot: Optional["Bot"] = PrivateAttr()
|
|
||||||
|
|
||||||
def model_post_init(self, __context: Any) -> None:
|
|
||||||
self._bot = __context.get("bot") if __context else None
|
|
||||||
|
|
||||||
def as_(self, bot: Optional["Bot"]) -> Self:
|
|
||||||
"""
|
|
||||||
Bind object to a bot instance.
|
|
||||||
|
|
||||||
:param bot: Bot instance
|
|
||||||
:return: self
|
|
||||||
"""
|
|
||||||
self._bot = bot
|
|
||||||
return self
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bot(self) -> Optional["Bot"]:
|
|
||||||
"""
|
|
||||||
Get bot instance.
|
|
||||||
|
|
||||||
:return: Bot instance
|
|
||||||
"""
|
|
||||||
return self._bot
|
|
@@ -1,80 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING, Any, Optional
|
|
||||||
|
|
||||||
from aiogram.utils.dataclass import dataclass_kwargs
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from aiogram.types import LinkPreviewOptions
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass ??
|
|
||||||
class Default:
|
|
||||||
# Is not a dataclass because of JSON serialization.
|
|
||||||
|
|
||||||
__slots__ = ("_name",)
|
|
||||||
|
|
||||||
def __init__(self, name: str) -> None:
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"Default({self._name!r})"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<{self}>"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(**dataclass_kwargs(slots=True, kw_only=True))
|
|
||||||
class DefaultBotProperties:
|
|
||||||
"""
|
|
||||||
Default bot properties.
|
|
||||||
"""
|
|
||||||
|
|
||||||
parse_mode: Optional[str] = None
|
|
||||||
"""Default parse mode for messages."""
|
|
||||||
disable_notification: Optional[bool] = None
|
|
||||||
"""Sends the message silently. Users will receive a notification with no sound."""
|
|
||||||
protect_content: Optional[bool] = None
|
|
||||||
"""Protects content from copying."""
|
|
||||||
allow_sending_without_reply: Optional[bool] = None
|
|
||||||
"""Allows to send messages without reply."""
|
|
||||||
link_preview: Optional[LinkPreviewOptions] = None
|
|
||||||
"""Link preview settings."""
|
|
||||||
link_preview_is_disabled: Optional[bool] = None
|
|
||||||
"""Disables link preview."""
|
|
||||||
link_preview_prefer_small_media: Optional[bool] = None
|
|
||||||
"""Prefer small media in link preview."""
|
|
||||||
link_preview_prefer_large_media: Optional[bool] = None
|
|
||||||
"""Prefer large media in link preview."""
|
|
||||||
link_preview_show_above_text: Optional[bool] = None
|
|
||||||
"""Show link preview above text."""
|
|
||||||
show_caption_above_media: Optional[bool] = None
|
|
||||||
"""Show caption above media."""
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
has_any_link_preview_option = any(
|
|
||||||
(
|
|
||||||
self.link_preview_is_disabled,
|
|
||||||
self.link_preview_prefer_small_media,
|
|
||||||
self.link_preview_prefer_large_media,
|
|
||||||
self.link_preview_show_above_text,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if has_any_link_preview_option and self.link_preview is None:
|
|
||||||
from ..types import LinkPreviewOptions
|
|
||||||
|
|
||||||
self.link_preview = LinkPreviewOptions(
|
|
||||||
is_disabled=self.link_preview_is_disabled,
|
|
||||||
prefer_small_media=self.link_preview_prefer_small_media,
|
|
||||||
prefer_large_media=self.link_preview_prefer_large_media,
|
|
||||||
show_above_text=self.link_preview_show_above_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __getitem__(self, item: str) -> Any:
|
|
||||||
return getattr(self, item, None)
|
|
@@ -1,211 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import ssl
|
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
AsyncGenerator,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Tuple,
|
|
||||||
Type,
|
|
||||||
Union,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
import certifi
|
|
||||||
from aiohttp import BasicAuth, ClientError, ClientSession, FormData, TCPConnector
|
|
||||||
from aiohttp.hdrs import USER_AGENT
|
|
||||||
from aiohttp.http import SERVER_SOFTWARE
|
|
||||||
|
|
||||||
from aiogram.__meta__ import __version__
|
|
||||||
from aiogram.methods import TelegramMethod
|
|
||||||
|
|
||||||
from ...exceptions import TelegramNetworkError
|
|
||||||
from ...methods.base import TelegramType
|
|
||||||
from ...types import InputFile
|
|
||||||
from .base import BaseSession
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..bot import Bot
|
|
||||||
|
|
||||||
_ProxyBasic = Union[str, Tuple[str, BasicAuth]]
|
|
||||||
_ProxyChain = Iterable[_ProxyBasic]
|
|
||||||
_ProxyType = Union[_ProxyChain, _ProxyBasic]
|
|
||||||
|
|
||||||
|
|
||||||
def _retrieve_basic(basic: _ProxyBasic) -> Dict[str, Any]:
|
|
||||||
from aiohttp_socks.utils import parse_proxy_url
|
|
||||||
|
|
||||||
proxy_auth: Optional[BasicAuth] = None
|
|
||||||
|
|
||||||
if isinstance(basic, str):
|
|
||||||
proxy_url = basic
|
|
||||||
else:
|
|
||||||
proxy_url, proxy_auth = basic
|
|
||||||
|
|
||||||
proxy_type, host, port, username, password = parse_proxy_url(proxy_url)
|
|
||||||
if isinstance(proxy_auth, BasicAuth):
|
|
||||||
username = proxy_auth.login
|
|
||||||
password = proxy_auth.password
|
|
||||||
|
|
||||||
return {
|
|
||||||
"proxy_type": proxy_type,
|
|
||||||
"host": host,
|
|
||||||
"port": port,
|
|
||||||
"username": username,
|
|
||||||
"password": password,
|
|
||||||
"rdns": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_connector(chain_or_plain: _ProxyType) -> Tuple[Type["TCPConnector"], Dict[str, Any]]:
|
|
||||||
from aiohttp_socks import ChainProxyConnector, ProxyConnector, ProxyInfo
|
|
||||||
|
|
||||||
# since tuple is Iterable(compatible with _ProxyChain) object, we assume that
|
|
||||||
# user wants chained proxies if tuple is a pair of string(url) and BasicAuth
|
|
||||||
if isinstance(chain_or_plain, str) or (
|
|
||||||
isinstance(chain_or_plain, tuple) and len(chain_or_plain) == 2
|
|
||||||
):
|
|
||||||
chain_or_plain = cast(_ProxyBasic, chain_or_plain)
|
|
||||||
return ProxyConnector, _retrieve_basic(chain_or_plain)
|
|
||||||
|
|
||||||
chain_or_plain = cast(_ProxyChain, chain_or_plain)
|
|
||||||
infos: List[ProxyInfo] = []
|
|
||||||
for basic in chain_or_plain:
|
|
||||||
infos.append(ProxyInfo(**_retrieve_basic(basic)))
|
|
||||||
|
|
||||||
return ChainProxyConnector, {"proxy_infos": infos}
|
|
||||||
|
|
||||||
|
|
||||||
class AiohttpSession(BaseSession):
|
|
||||||
def __init__(
|
|
||||||
self, proxy: Optional[_ProxyType] = None, limit: int = 100, **kwargs: Any
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Client session based on aiohttp.
|
|
||||||
|
|
||||||
:param proxy: The proxy to be used for requests. Default is None.
|
|
||||||
:param limit: The total number of simultaneous connections. Default is 100.
|
|
||||||
:param kwargs: Additional keyword arguments.
|
|
||||||
"""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
self._session: Optional[ClientSession] = None
|
|
||||||
self._connector_type: Type[TCPConnector] = TCPConnector
|
|
||||||
self._connector_init: Dict[str, Any] = {
|
|
||||||
"ssl": ssl.create_default_context(cafile=certifi.where()),
|
|
||||||
"limit": limit,
|
|
||||||
"ttl_dns_cache": 3600, # Workaround for https://github.com/aiogram/aiogram/issues/1500
|
|
||||||
}
|
|
||||||
self._should_reset_connector = True # flag determines connector state
|
|
||||||
self._proxy: Optional[_ProxyType] = None
|
|
||||||
|
|
||||||
if proxy is not None:
|
|
||||||
try:
|
|
||||||
self._setup_proxy_connector(proxy)
|
|
||||||
except ImportError as exc: # pragma: no cover
|
|
||||||
raise RuntimeError(
|
|
||||||
"In order to use aiohttp client for proxy requests, install "
|
|
||||||
"https://pypi.org/project/aiohttp-socks/"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
def _setup_proxy_connector(self, proxy: _ProxyType) -> None:
|
|
||||||
self._connector_type, self._connector_init = _prepare_connector(proxy)
|
|
||||||
self._proxy = proxy
|
|
||||||
|
|
||||||
@property
|
|
||||||
def proxy(self) -> Optional[_ProxyType]:
|
|
||||||
return self._proxy
|
|
||||||
|
|
||||||
@proxy.setter
|
|
||||||
def proxy(self, proxy: _ProxyType) -> None:
|
|
||||||
self._setup_proxy_connector(proxy)
|
|
||||||
self._should_reset_connector = True
|
|
||||||
|
|
||||||
async def create_session(self) -> ClientSession:
|
|
||||||
if self._should_reset_connector:
|
|
||||||
await self.close()
|
|
||||||
|
|
||||||
if self._session is None or self._session.closed:
|
|
||||||
self._session = ClientSession(
|
|
||||||
connector=self._connector_type(**self._connector_init),
|
|
||||||
headers={
|
|
||||||
USER_AGENT: f"{SERVER_SOFTWARE} aiogram/{__version__}",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self._should_reset_connector = False
|
|
||||||
|
|
||||||
return self._session
|
|
||||||
|
|
||||||
async def close(self) -> None:
|
|
||||||
if self._session is not None and not self._session.closed:
|
|
||||||
await self._session.close()
|
|
||||||
|
|
||||||
# Wait 250 ms for the underlying SSL connections to close
|
|
||||||
# https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown
|
|
||||||
await asyncio.sleep(0.25)
|
|
||||||
|
|
||||||
def build_form_data(self, bot: Bot, method: TelegramMethod[TelegramType]) -> FormData:
|
|
||||||
form = FormData(quote_fields=False)
|
|
||||||
files: Dict[str, InputFile] = {}
|
|
||||||
for key, value in method.model_dump(warnings=False).items():
|
|
||||||
value = self.prepare_value(value, bot=bot, files=files)
|
|
||||||
if not value:
|
|
||||||
continue
|
|
||||||
form.add_field(key, value)
|
|
||||||
for key, value in files.items():
|
|
||||||
form.add_field(
|
|
||||||
key,
|
|
||||||
value.read(bot),
|
|
||||||
filename=value.filename or key,
|
|
||||||
)
|
|
||||||
return form
|
|
||||||
|
|
||||||
async def make_request(
|
|
||||||
self, bot: Bot, method: TelegramMethod[TelegramType], timeout: Optional[int] = None
|
|
||||||
) -> TelegramType:
|
|
||||||
session = await self.create_session()
|
|
||||||
|
|
||||||
url = self.api.api_url(token=bot.token, method=method.__api_method__)
|
|
||||||
form = self.build_form_data(bot=bot, method=method)
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with session.post(
|
|
||||||
url, data=form, timeout=self.timeout if timeout is None else timeout
|
|
||||||
) as resp:
|
|
||||||
raw_result = await resp.text()
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
raise TelegramNetworkError(method=method, message="Request timeout error")
|
|
||||||
except ClientError as e:
|
|
||||||
raise TelegramNetworkError(method=method, message=f"{type(e).__name__}: {e}")
|
|
||||||
response = self.check_response(
|
|
||||||
bot=bot, method=method, status_code=resp.status, content=raw_result
|
|
||||||
)
|
|
||||||
return cast(TelegramType, response.result)
|
|
||||||
|
|
||||||
async def stream_content(
|
|
||||||
self,
|
|
||||||
url: str,
|
|
||||||
headers: Optional[Dict[str, Any]] = None,
|
|
||||||
timeout: int = 30,
|
|
||||||
chunk_size: int = 65536,
|
|
||||||
raise_for_status: bool = True,
|
|
||||||
) -> AsyncGenerator[bytes, None]:
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
|
|
||||||
session = await self.create_session()
|
|
||||||
|
|
||||||
async with session.get(
|
|
||||||
url, timeout=timeout, headers=headers, raise_for_status=raise_for_status
|
|
||||||
) as resp:
|
|
||||||
async for chunk in resp.content.iter_chunked(chunk_size):
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
async def __aenter__(self) -> AiohttpSession:
|
|
||||||
await self.create_session()
|
|
||||||
return self
|
|
@@ -1,265 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import secrets
|
|
||||||
from enum import Enum
|
|
||||||
from http import HTTPStatus
|
|
||||||
from types import TracebackType
|
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
AsyncGenerator,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Final,
|
|
||||||
Optional,
|
|
||||||
Type,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pydantic import ValidationError
|
|
||||||
|
|
||||||
from aiogram.exceptions import (
|
|
||||||
ClientDecodeError,
|
|
||||||
RestartingTelegram,
|
|
||||||
TelegramAPIError,
|
|
||||||
TelegramBadRequest,
|
|
||||||
TelegramConflictError,
|
|
||||||
TelegramEntityTooLarge,
|
|
||||||
TelegramForbiddenError,
|
|
||||||
TelegramMigrateToChat,
|
|
||||||
TelegramNotFound,
|
|
||||||
TelegramRetryAfter,
|
|
||||||
TelegramServerError,
|
|
||||||
TelegramUnauthorizedError,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ...methods import Response, TelegramMethod
|
|
||||||
from ...methods.base import TelegramType
|
|
||||||
from ...types import InputFile, TelegramObject
|
|
||||||
from ..default import Default
|
|
||||||
from ..telegram import PRODUCTION, TelegramAPIServer
|
|
||||||
from .middlewares.manager import RequestMiddlewareManager
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..bot import Bot
|
|
||||||
|
|
||||||
_JsonLoads = Callable[..., Any]
|
|
||||||
_JsonDumps = Callable[..., str]
|
|
||||||
|
|
||||||
DEFAULT_TIMEOUT: Final[float] = 60.0
|
|
||||||
|
|
||||||
|
|
||||||
class BaseSession(abc.ABC):
|
|
||||||
"""
|
|
||||||
This is base class for all HTTP sessions in aiogram.
|
|
||||||
|
|
||||||
If you want to create your own session, you must inherit from this class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
api: TelegramAPIServer = PRODUCTION,
|
|
||||||
json_loads: _JsonLoads = json.loads,
|
|
||||||
json_dumps: _JsonDumps = json.dumps,
|
|
||||||
timeout: float = DEFAULT_TIMEOUT,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
|
|
||||||
:param api: Telegram Bot API URL patterns
|
|
||||||
:param json_loads: JSON loader
|
|
||||||
:param json_dumps: JSON dumper
|
|
||||||
:param timeout: Session scope request timeout
|
|
||||||
"""
|
|
||||||
self.api = api
|
|
||||||
self.json_loads = json_loads
|
|
||||||
self.json_dumps = json_dumps
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
self.middleware = RequestMiddlewareManager()
|
|
||||||
|
|
||||||
def check_response(
|
|
||||||
self, bot: Bot, method: TelegramMethod[TelegramType], status_code: int, content: str
|
|
||||||
) -> Response[TelegramType]:
|
|
||||||
"""
|
|
||||||
Check response status
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
json_data = self.json_loads(content)
|
|
||||||
except Exception as e:
|
|
||||||
# Handled error type can't be classified as specific error
|
|
||||||
# in due to decoder can be customized and raise any exception
|
|
||||||
|
|
||||||
raise ClientDecodeError("Failed to decode object", e, content)
|
|
||||||
|
|
||||||
try:
|
|
||||||
response_type = Response[method.__returning__] # type: ignore
|
|
||||||
response = response_type.model_validate(json_data, context={"bot": bot})
|
|
||||||
except ValidationError as e:
|
|
||||||
raise ClientDecodeError("Failed to deserialize object", e, json_data)
|
|
||||||
|
|
||||||
if HTTPStatus.OK <= status_code <= HTTPStatus.IM_USED and response.ok:
|
|
||||||
return response
|
|
||||||
|
|
||||||
description = cast(str, response.description)
|
|
||||||
|
|
||||||
if parameters := response.parameters:
|
|
||||||
if parameters.retry_after:
|
|
||||||
raise TelegramRetryAfter(
|
|
||||||
method=method, message=description, retry_after=parameters.retry_after
|
|
||||||
)
|
|
||||||
if parameters.migrate_to_chat_id:
|
|
||||||
raise TelegramMigrateToChat(
|
|
||||||
method=method,
|
|
||||||
message=description,
|
|
||||||
migrate_to_chat_id=parameters.migrate_to_chat_id,
|
|
||||||
)
|
|
||||||
if status_code == HTTPStatus.BAD_REQUEST:
|
|
||||||
raise TelegramBadRequest(method=method, message=description)
|
|
||||||
if status_code == HTTPStatus.NOT_FOUND:
|
|
||||||
raise TelegramNotFound(method=method, message=description)
|
|
||||||
if status_code == HTTPStatus.CONFLICT:
|
|
||||||
raise TelegramConflictError(method=method, message=description)
|
|
||||||
if status_code == HTTPStatus.UNAUTHORIZED:
|
|
||||||
raise TelegramUnauthorizedError(method=method, message=description)
|
|
||||||
if status_code == HTTPStatus.FORBIDDEN:
|
|
||||||
raise TelegramForbiddenError(method=method, message=description)
|
|
||||||
if status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
|
|
||||||
raise TelegramEntityTooLarge(method=method, message=description)
|
|
||||||
if status_code >= HTTPStatus.INTERNAL_SERVER_ERROR:
|
|
||||||
if "restart" in description:
|
|
||||||
raise RestartingTelegram(method=method, message=description)
|
|
||||||
raise TelegramServerError(method=method, message=description)
|
|
||||||
|
|
||||||
raise TelegramAPIError(
|
|
||||||
method=method,
|
|
||||||
message=description,
|
|
||||||
)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def close(self) -> None: # pragma: no cover
|
|
||||||
"""
|
|
||||||
Close client session
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def make_request(
|
|
||||||
self,
|
|
||||||
bot: Bot,
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
timeout: Optional[int] = None,
|
|
||||||
) -> TelegramType: # pragma: no cover
|
|
||||||
"""
|
|
||||||
Make request to Telegram Bot API
|
|
||||||
|
|
||||||
:param bot: Bot instance
|
|
||||||
:param method: Method instance
|
|
||||||
:param timeout: Request timeout
|
|
||||||
:return:
|
|
||||||
:raise TelegramApiError:
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def stream_content(
|
|
||||||
self,
|
|
||||||
url: str,
|
|
||||||
headers: Optional[Dict[str, Any]] = None,
|
|
||||||
timeout: int = 30,
|
|
||||||
chunk_size: int = 65536,
|
|
||||||
raise_for_status: bool = True,
|
|
||||||
) -> AsyncGenerator[bytes, None]: # pragma: no cover
|
|
||||||
"""
|
|
||||||
Stream reader
|
|
||||||
"""
|
|
||||||
yield b""
|
|
||||||
|
|
||||||
def prepare_value(
|
|
||||||
self,
|
|
||||||
value: Any,
|
|
||||||
bot: Bot,
|
|
||||||
files: Dict[str, Any],
|
|
||||||
_dumps_json: bool = True,
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
Prepare value before send
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if isinstance(value, str):
|
|
||||||
return value
|
|
||||||
if isinstance(value, Default):
|
|
||||||
default_value = bot.default[value.name]
|
|
||||||
return self.prepare_value(default_value, bot=bot, files=files, _dumps_json=_dumps_json)
|
|
||||||
if isinstance(value, InputFile):
|
|
||||||
key = secrets.token_urlsafe(10)
|
|
||||||
files[key] = value
|
|
||||||
return f"attach://{key}"
|
|
||||||
if isinstance(value, dict):
|
|
||||||
value = {
|
|
||||||
key: prepared_item
|
|
||||||
for key, item in value.items()
|
|
||||||
if (
|
|
||||||
prepared_item := self.prepare_value(
|
|
||||||
item, bot=bot, files=files, _dumps_json=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is not None
|
|
||||||
}
|
|
||||||
if _dumps_json:
|
|
||||||
return self.json_dumps(value)
|
|
||||||
return value
|
|
||||||
if isinstance(value, list):
|
|
||||||
value = [
|
|
||||||
prepared_item
|
|
||||||
for item in value
|
|
||||||
if (
|
|
||||||
prepared_item := self.prepare_value(
|
|
||||||
item, bot=bot, files=files, _dumps_json=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is not None
|
|
||||||
]
|
|
||||||
if _dumps_json:
|
|
||||||
return self.json_dumps(value)
|
|
||||||
return value
|
|
||||||
if isinstance(value, datetime.timedelta):
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
return str(round((now + value).timestamp()))
|
|
||||||
if isinstance(value, datetime.datetime):
|
|
||||||
return str(round(value.timestamp()))
|
|
||||||
if isinstance(value, Enum):
|
|
||||||
return self.prepare_value(value.value, bot=bot, files=files)
|
|
||||||
if isinstance(value, TelegramObject):
|
|
||||||
return self.prepare_value(
|
|
||||||
value.model_dump(warnings=False),
|
|
||||||
bot=bot,
|
|
||||||
files=files,
|
|
||||||
_dumps_json=_dumps_json,
|
|
||||||
)
|
|
||||||
if _dumps_json:
|
|
||||||
return self.json_dumps(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
bot: Bot,
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
timeout: Optional[int] = None,
|
|
||||||
) -> TelegramType:
|
|
||||||
middleware = self.middleware.wrap_middlewares(self.make_request, timeout=timeout)
|
|
||||||
return cast(TelegramType, await middleware(bot, method))
|
|
||||||
|
|
||||||
async def __aenter__(self) -> BaseSession:
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(
|
|
||||||
self,
|
|
||||||
exc_type: Optional[Type[BaseException]],
|
|
||||||
exc_value: Optional[BaseException],
|
|
||||||
traceback: Optional[TracebackType],
|
|
||||||
) -> None:
|
|
||||||
await self.close()
|
|
@@ -1,53 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import TYPE_CHECKING, Protocol
|
|
||||||
|
|
||||||
from aiogram.methods import Response, TelegramMethod
|
|
||||||
from aiogram.methods.base import TelegramType
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ...bot import Bot
|
|
||||||
|
|
||||||
|
|
||||||
class NextRequestMiddlewareType(Protocol[TelegramType]): # pragma: no cover
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
bot: "Bot",
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
) -> Response[TelegramType]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RequestMiddlewareType(Protocol): # pragma: no cover
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
make_request: NextRequestMiddlewareType[TelegramType],
|
|
||||||
bot: "Bot",
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
) -> Response[TelegramType]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BaseRequestMiddleware(ABC):
|
|
||||||
"""
|
|
||||||
Generic middleware class
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
make_request: NextRequestMiddlewareType[TelegramType],
|
|
||||||
bot: "Bot",
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
) -> Response[TelegramType]:
|
|
||||||
"""
|
|
||||||
Execute middleware
|
|
||||||
|
|
||||||
:param make_request: Wrapped make_request in middlewares chain
|
|
||||||
:param bot: bot for request making
|
|
||||||
:param method: Request method (Subclass of :class:`aiogram.methods.base.TelegramMethod`)
|
|
||||||
|
|
||||||
:return: :class:`aiogram.methods.Response`
|
|
||||||
"""
|
|
||||||
pass
|
|
@@ -1,62 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
from typing import Any, Callable, List, Optional, Sequence, Union, cast, overload
|
|
||||||
|
|
||||||
from aiogram.client.session.middlewares.base import (
|
|
||||||
NextRequestMiddlewareType,
|
|
||||||
RequestMiddlewareType,
|
|
||||||
)
|
|
||||||
from aiogram.methods.base import TelegramType
|
|
||||||
|
|
||||||
|
|
||||||
class RequestMiddlewareManager(Sequence[RequestMiddlewareType]):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._middlewares: List[RequestMiddlewareType] = []
|
|
||||||
|
|
||||||
def register(
|
|
||||||
self,
|
|
||||||
middleware: RequestMiddlewareType,
|
|
||||||
) -> RequestMiddlewareType:
|
|
||||||
self._middlewares.append(middleware)
|
|
||||||
return middleware
|
|
||||||
|
|
||||||
def unregister(self, middleware: RequestMiddlewareType) -> None:
|
|
||||||
self._middlewares.remove(middleware)
|
|
||||||
|
|
||||||
def __call__(
|
|
||||||
self,
|
|
||||||
middleware: Optional[RequestMiddlewareType] = None,
|
|
||||||
) -> Union[
|
|
||||||
Callable[[RequestMiddlewareType], RequestMiddlewareType],
|
|
||||||
RequestMiddlewareType,
|
|
||||||
]:
|
|
||||||
if middleware is None:
|
|
||||||
return self.register
|
|
||||||
return self.register(middleware)
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __getitem__(self, item: int) -> RequestMiddlewareType:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __getitem__(self, item: slice) -> Sequence[RequestMiddlewareType]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __getitem__(
|
|
||||||
self, item: Union[int, slice]
|
|
||||||
) -> Union[RequestMiddlewareType, Sequence[RequestMiddlewareType]]:
|
|
||||||
return self._middlewares[item]
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._middlewares)
|
|
||||||
|
|
||||||
def wrap_middlewares(
|
|
||||||
self,
|
|
||||||
callback: NextRequestMiddlewareType[TelegramType],
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> NextRequestMiddlewareType[TelegramType]:
|
|
||||||
middleware = partial(callback, **kwargs)
|
|
||||||
for m in reversed(self._middlewares):
|
|
||||||
middleware = partial(m, middleware)
|
|
||||||
return cast(NextRequestMiddlewareType[TelegramType], middleware)
|
|
@@ -1,37 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import TYPE_CHECKING, Any, List, Optional, Type
|
|
||||||
|
|
||||||
from aiogram import loggers
|
|
||||||
from aiogram.methods import TelegramMethod
|
|
||||||
from aiogram.methods.base import Response, TelegramType
|
|
||||||
|
|
||||||
from .base import BaseRequestMiddleware, NextRequestMiddlewareType
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ...bot import Bot
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RequestLogging(BaseRequestMiddleware):
|
|
||||||
def __init__(self, ignore_methods: Optional[List[Type[TelegramMethod[Any]]]] = None):
|
|
||||||
"""
|
|
||||||
Middleware for logging outgoing requests
|
|
||||||
|
|
||||||
:param ignore_methods: methods to ignore in logging middleware
|
|
||||||
"""
|
|
||||||
self.ignore_methods = ignore_methods if ignore_methods else []
|
|
||||||
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
make_request: NextRequestMiddlewareType[TelegramType],
|
|
||||||
bot: "Bot",
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
) -> Response[TelegramType]:
|
|
||||||
if type(method) not in self.ignore_methods:
|
|
||||||
loggers.middlewares.info(
|
|
||||||
"Make request with method=%r by bot id=%d",
|
|
||||||
type(method).__name__,
|
|
||||||
bot.id,
|
|
||||||
)
|
|
||||||
return await make_request(bot, method)
|
|
@@ -1,103 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Union
|
|
||||||
|
|
||||||
|
|
||||||
class FilesPathWrapper(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def to_local(self, path: Union[Path, str]) -> Union[Path, str]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def to_server(self, path: Union[Path, str]) -> Union[Path, str]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BareFilesPathWrapper(FilesPathWrapper):
|
|
||||||
def to_local(self, path: Union[Path, str]) -> Union[Path, str]:
|
|
||||||
return path
|
|
||||||
|
|
||||||
def to_server(self, path: Union[Path, str]) -> Union[Path, str]:
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleFilesPathWrapper(FilesPathWrapper):
|
|
||||||
def __init__(self, server_path: Path, local_path: Path) -> None:
|
|
||||||
self.server_path = server_path
|
|
||||||
self.local_path = local_path
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _resolve(
|
|
||||||
cls, base1: Union[Path, str], base2: Union[Path, str], value: Union[Path, str]
|
|
||||||
) -> Path:
|
|
||||||
relative = Path(value).relative_to(base1)
|
|
||||||
return base2 / relative
|
|
||||||
|
|
||||||
def to_local(self, path: Union[Path, str]) -> Union[Path, str]:
|
|
||||||
return self._resolve(base1=self.server_path, base2=self.local_path, value=path)
|
|
||||||
|
|
||||||
def to_server(self, path: Union[Path, str]) -> Union[Path, str]:
|
|
||||||
return self._resolve(base1=self.local_path, base2=self.server_path, value=path)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class TelegramAPIServer:
|
|
||||||
"""
|
|
||||||
Base config for API Endpoints
|
|
||||||
"""
|
|
||||||
|
|
||||||
base: str
|
|
||||||
"""Base URL"""
|
|
||||||
file: str
|
|
||||||
"""Files URL"""
|
|
||||||
is_local: bool = False
|
|
||||||
"""Mark this server is
|
|
||||||
in `local mode <https://core.telegram.org/bots/api#using-a-local-bot-api-server>`_."""
|
|
||||||
wrap_local_file: FilesPathWrapper = BareFilesPathWrapper()
|
|
||||||
"""Callback to wrap files path in local mode"""
|
|
||||||
|
|
||||||
def api_url(self, token: str, method: str) -> str:
|
|
||||||
"""
|
|
||||||
Generate URL for API methods
|
|
||||||
|
|
||||||
:param token: Bot token
|
|
||||||
:param method: API method name (case insensitive)
|
|
||||||
:return: URL
|
|
||||||
"""
|
|
||||||
return self.base.format(token=token, method=method)
|
|
||||||
|
|
||||||
def file_url(self, token: str, path: Union[str, Path]) -> str:
|
|
||||||
"""
|
|
||||||
Generate URL for downloading files
|
|
||||||
|
|
||||||
:param token: Bot token
|
|
||||||
:param path: file path
|
|
||||||
:return: URL
|
|
||||||
"""
|
|
||||||
return self.file.format(token=token, path=path)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_base(cls, base: str, **kwargs: Any) -> "TelegramAPIServer":
|
|
||||||
"""
|
|
||||||
Use this method to auto-generate TelegramAPIServer instance from base URL
|
|
||||||
|
|
||||||
:param base: Base URL
|
|
||||||
:return: instance of :class:`TelegramAPIServer`
|
|
||||||
"""
|
|
||||||
base = base.rstrip("/")
|
|
||||||
return cls(
|
|
||||||
base=f"{base}/bot{{token}}/{{method}}",
|
|
||||||
file=f"{base}/file/bot{{token}}/{{path}}",
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
PRODUCTION = TelegramAPIServer(
|
|
||||||
base="https://api.telegram.org/bot{token}/{method}",
|
|
||||||
file="https://api.telegram.org/file/bot{token}/{path}",
|
|
||||||
)
|
|
||||||
TEST = TelegramAPIServer(
|
|
||||||
base="https://api.telegram.org/bot{token}/test/{method}",
|
|
||||||
file="https://api.telegram.org/file/bot{token}/test/{path}",
|
|
||||||
)
|
|
@@ -1,642 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import contextvars
|
|
||||||
import signal
|
|
||||||
import warnings
|
|
||||||
from asyncio import CancelledError, Event, Future, Lock
|
|
||||||
from contextlib import suppress
|
|
||||||
from typing import Any, AsyncGenerator, Awaitable, Dict, List, Optional, Set, Union
|
|
||||||
|
|
||||||
from .. import loggers
|
|
||||||
from ..client.bot import Bot
|
|
||||||
from ..exceptions import TelegramAPIError
|
|
||||||
from ..fsm.middleware import FSMContextMiddleware
|
|
||||||
from ..fsm.storage.base import BaseEventIsolation, BaseStorage
|
|
||||||
from ..fsm.storage.memory import DisabledEventIsolation, MemoryStorage
|
|
||||||
from ..fsm.strategy import FSMStrategy
|
|
||||||
from ..methods import GetUpdates, TelegramMethod
|
|
||||||
from ..methods.base import TelegramType
|
|
||||||
from ..types import Update, User
|
|
||||||
from ..types.base import UNSET, UNSET_TYPE
|
|
||||||
from ..types.update import UpdateTypeLookupError
|
|
||||||
from ..utils.backoff import Backoff, BackoffConfig
|
|
||||||
from .event.bases import UNHANDLED, SkipHandler
|
|
||||||
from .event.telegram import TelegramEventObserver
|
|
||||||
from .middlewares.error import ErrorsMiddleware
|
|
||||||
from .middlewares.user_context import UserContextMiddleware
|
|
||||||
from .router import Router
|
|
||||||
|
|
||||||
DEFAULT_BACKOFF_CONFIG = BackoffConfig(min_delay=1.0, max_delay=5.0, factor=1.3, jitter=0.1)
|
|
||||||
|
|
||||||
|
|
||||||
class Dispatcher(Router):
|
|
||||||
"""
|
|
||||||
Root router
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*, # * - Preventing to pass instance of Bot to the FSM storage
|
|
||||||
storage: Optional[BaseStorage] = None,
|
|
||||||
fsm_strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
|
|
||||||
events_isolation: Optional[BaseEventIsolation] = None,
|
|
||||||
disable_fsm: bool = False,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Root router
|
|
||||||
|
|
||||||
:param storage: Storage for FSM
|
|
||||||
:param fsm_strategy: FSM strategy
|
|
||||||
:param events_isolation: Events isolation
|
|
||||||
:param disable_fsm: Disable FSM, note that if you disable FSM
|
|
||||||
then you should not use storage and events isolation
|
|
||||||
:param kwargs: Other arguments, will be passed as keyword arguments to handlers
|
|
||||||
"""
|
|
||||||
super(Dispatcher, self).__init__(name=name)
|
|
||||||
|
|
||||||
if storage and not isinstance(storage, BaseStorage):
|
|
||||||
raise TypeError(
|
|
||||||
f"FSM storage should be instance of 'BaseStorage' not {type(storage).__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Telegram API provides originally only one event type - Update
|
|
||||||
# For making easily interactions with events here is registered handler which helps
|
|
||||||
# to separate Update to different event types like Message, CallbackQuery etc.
|
|
||||||
self.update = self.observers["update"] = TelegramEventObserver(
|
|
||||||
router=self, event_name="update"
|
|
||||||
)
|
|
||||||
self.update.register(self._listen_update)
|
|
||||||
|
|
||||||
# Error handlers should work is out of all other functions
|
|
||||||
# and should be registered before all others middlewares
|
|
||||||
self.update.outer_middleware(ErrorsMiddleware(self))
|
|
||||||
|
|
||||||
# User context middleware makes small optimization for all other builtin
|
|
||||||
# middlewares via caching the user and chat instances in the event context
|
|
||||||
self.update.outer_middleware(UserContextMiddleware())
|
|
||||||
|
|
||||||
# FSM middleware should always be registered after User context middleware
|
|
||||||
# because here is used context from previous step
|
|
||||||
self.fsm = FSMContextMiddleware(
|
|
||||||
storage=storage or MemoryStorage(),
|
|
||||||
strategy=fsm_strategy,
|
|
||||||
events_isolation=events_isolation or DisabledEventIsolation(),
|
|
||||||
)
|
|
||||||
if not disable_fsm:
|
|
||||||
# Note that when FSM middleware is disabled, the event isolation is also disabled
|
|
||||||
# Because the isolation mechanism is a part of the FSM
|
|
||||||
self.update.outer_middleware(self.fsm)
|
|
||||||
self.shutdown.register(self.fsm.close)
|
|
||||||
|
|
||||||
self.workflow_data: Dict[str, Any] = kwargs
|
|
||||||
self._running_lock = Lock()
|
|
||||||
self._stop_signal: Optional[Event] = None
|
|
||||||
self._stopped_signal: Optional[Event] = None
|
|
||||||
self._handle_update_tasks: Set[asyncio.Task[Any]] = set()
|
|
||||||
|
|
||||||
def __getitem__(self, item: str) -> Any:
|
|
||||||
return self.workflow_data[item]
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: Any) -> None:
|
|
||||||
self.workflow_data[key] = value
|
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
|
||||||
del self.workflow_data[key]
|
|
||||||
|
|
||||||
def get(self, key: str, /, default: Optional[Any] = None) -> Optional[Any]:
|
|
||||||
return self.workflow_data.get(key, default)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def storage(self) -> BaseStorage:
|
|
||||||
return self.fsm.storage
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parent_router(self) -> Optional[Router]:
|
|
||||||
"""
|
|
||||||
Dispatcher has no parent router and can't be included to any other routers or dispatchers
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return None # noqa: RET501
|
|
||||||
|
|
||||||
@parent_router.setter
|
|
||||||
def parent_router(self, value: Router) -> None:
|
|
||||||
"""
|
|
||||||
Dispatcher is root Router then configuring parent router is not allowed
|
|
||||||
|
|
||||||
:param value:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
raise RuntimeError("Dispatcher can not be attached to another Router.")
|
|
||||||
|
|
||||||
async def feed_update(self, bot: Bot, update: Update, **kwargs: Any) -> Any:
|
|
||||||
"""
|
|
||||||
Main entry point for incoming updates
|
|
||||||
Response of this method can be used as Webhook response
|
|
||||||
|
|
||||||
:param bot:
|
|
||||||
:param update:
|
|
||||||
"""
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
handled = False
|
|
||||||
start_time = loop.time()
|
|
||||||
|
|
||||||
if update.bot != bot:
|
|
||||||
# Re-mounting update to the current bot instance for making possible to
|
|
||||||
# use it in shortcuts.
|
|
||||||
# Here is update is re-created because we need to propagate context to
|
|
||||||
# all nested objects and attributes of the Update, but it
|
|
||||||
# is impossible without roundtrip to JSON :(
|
|
||||||
# The preferred way is that pass already mounted Bot instance to this update
|
|
||||||
# before call feed_update method
|
|
||||||
update = Update.model_validate(update.model_dump(), context={"bot": bot})
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = await self.update.wrap_outer_middleware(
|
|
||||||
self.update.trigger,
|
|
||||||
update,
|
|
||||||
{
|
|
||||||
**self.workflow_data,
|
|
||||||
**kwargs,
|
|
||||||
"bot": bot,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
handled = response is not UNHANDLED
|
|
||||||
return response
|
|
||||||
finally:
|
|
||||||
finish_time = loop.time()
|
|
||||||
duration = (finish_time - start_time) * 1000
|
|
||||||
loggers.event.info(
|
|
||||||
"Update id=%s is %s. Duration %d ms by bot id=%d",
|
|
||||||
update.update_id,
|
|
||||||
"handled" if handled else "not handled",
|
|
||||||
duration,
|
|
||||||
bot.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def feed_raw_update(self, bot: Bot, update: Dict[str, Any], **kwargs: Any) -> Any:
|
|
||||||
"""
|
|
||||||
Main entry point for incoming updates with automatic Dict->Update serializer
|
|
||||||
|
|
||||||
:param bot:
|
|
||||||
:param update:
|
|
||||||
:param kwargs:
|
|
||||||
"""
|
|
||||||
parsed_update = Update.model_validate(update, context={"bot": bot})
|
|
||||||
return await self._feed_webhook_update(bot=bot, update=parsed_update, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def _listen_updates(
|
|
||||||
cls,
|
|
||||||
bot: Bot,
|
|
||||||
polling_timeout: int = 30,
|
|
||||||
backoff_config: BackoffConfig = DEFAULT_BACKOFF_CONFIG,
|
|
||||||
allowed_updates: Optional[List[str]] = None,
|
|
||||||
) -> AsyncGenerator[Update, None]:
|
|
||||||
"""
|
|
||||||
Endless updates reader with correctly handling any server-side or connection errors.
|
|
||||||
|
|
||||||
So you may not worry that the polling will stop working.
|
|
||||||
"""
|
|
||||||
backoff = Backoff(config=backoff_config)
|
|
||||||
get_updates = GetUpdates(timeout=polling_timeout, allowed_updates=allowed_updates)
|
|
||||||
kwargs = {}
|
|
||||||
if bot.session.timeout:
|
|
||||||
# Request timeout can be lower than session timeout and that's OK.
|
|
||||||
# To prevent false-positive TimeoutError we should wait longer than polling timeout
|
|
||||||
kwargs["request_timeout"] = int(bot.session.timeout + polling_timeout)
|
|
||||||
failed = False
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
updates = await bot(get_updates, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
failed = True
|
|
||||||
# In cases when Telegram Bot API was inaccessible don't need to stop polling
|
|
||||||
# process because some developers can't make auto-restarting of the script
|
|
||||||
loggers.dispatcher.error("Failed to fetch updates - %s: %s", type(e).__name__, e)
|
|
||||||
# And also backoff timeout is best practice to retry any network activity
|
|
||||||
loggers.dispatcher.warning(
|
|
||||||
"Sleep for %f seconds and try again... (tryings = %d, bot id = %d)",
|
|
||||||
backoff.next_delay,
|
|
||||||
backoff.counter,
|
|
||||||
bot.id,
|
|
||||||
)
|
|
||||||
await backoff.asleep()
|
|
||||||
continue
|
|
||||||
|
|
||||||
# In case when network connection was fixed let's reset the backoff
|
|
||||||
# to initial value and then process updates
|
|
||||||
if failed:
|
|
||||||
loggers.dispatcher.info(
|
|
||||||
"Connection established (tryings = %d, bot id = %d)",
|
|
||||||
backoff.counter,
|
|
||||||
bot.id,
|
|
||||||
)
|
|
||||||
backoff.reset()
|
|
||||||
failed = False
|
|
||||||
|
|
||||||
for update in updates:
|
|
||||||
yield update
|
|
||||||
# The getUpdates method returns the earliest 100 unconfirmed updates.
|
|
||||||
# To confirm an update, use the offset parameter when calling getUpdates
|
|
||||||
# All updates with update_id less than or equal to offset will be marked
|
|
||||||
# as confirmed on the server and will no longer be returned.
|
|
||||||
get_updates.offset = update.update_id + 1
|
|
||||||
|
|
||||||
async def _listen_update(self, update: Update, **kwargs: Any) -> Any:
|
|
||||||
"""
|
|
||||||
Main updates listener
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
- Detect content type and propagate to observers in current router
|
|
||||||
- If no one filter is pass - propagate update to child routers as Update
|
|
||||||
|
|
||||||
:param update:
|
|
||||||
:param kwargs:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
update_type = update.event_type
|
|
||||||
event = update.event
|
|
||||||
except UpdateTypeLookupError as e:
|
|
||||||
warnings.warn(
|
|
||||||
"Detected unknown update type.\n"
|
|
||||||
"Seems like Telegram Bot API was updated and you have "
|
|
||||||
"installed not latest version of aiogram framework"
|
|
||||||
f"\nUpdate: {update.model_dump_json(exclude_unset=True)}",
|
|
||||||
RuntimeWarning,
|
|
||||||
)
|
|
||||||
raise SkipHandler() from e
|
|
||||||
|
|
||||||
kwargs.update(event_update=update)
|
|
||||||
|
|
||||||
return await self.propagate_event(update_type=update_type, event=event, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def silent_call_request(cls, bot: Bot, result: TelegramMethod[Any]) -> None:
|
|
||||||
"""
|
|
||||||
Simulate answer into WebHook
|
|
||||||
|
|
||||||
:param bot:
|
|
||||||
:param result:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await bot(result)
|
|
||||||
except TelegramAPIError as e:
|
|
||||||
# In due to WebHook mechanism doesn't allow getting response for
|
|
||||||
# requests called in answer to WebHook request.
|
|
||||||
# Need to skip unsuccessful responses.
|
|
||||||
# For debugging here is added logging.
|
|
||||||
loggers.event.error("Failed to make answer: %s: %s", e.__class__.__name__, e)
|
|
||||||
|
|
||||||
async def _process_update(
|
|
||||||
self, bot: Bot, update: Update, call_answer: bool = True, **kwargs: Any
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Propagate update to event listeners
|
|
||||||
|
|
||||||
:param bot: instance of Bot
|
|
||||||
:param update: instance of Update
|
|
||||||
:param call_answer: need to execute response as Telegram method (like answer into webhook)
|
|
||||||
:param kwargs: contextual data for middlewares, filters and handlers
|
|
||||||
:return: status
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = await self.feed_update(bot, update, **kwargs)
|
|
||||||
if call_answer and isinstance(response, TelegramMethod):
|
|
||||||
await self.silent_call_request(bot=bot, result=response)
|
|
||||||
return response is not UNHANDLED
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
loggers.event.exception(
|
|
||||||
"Cause exception while process update id=%d by bot id=%d\n%s: %s",
|
|
||||||
update.update_id,
|
|
||||||
bot.id,
|
|
||||||
e.__class__.__name__,
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
return True # because update was processed but unsuccessful
|
|
||||||
|
|
||||||
async def _process_with_semaphore(
|
|
||||||
self, handle_update: Awaitable[bool], semaphore: asyncio.Semaphore
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Process update with semaphore to limit concurrent tasks
|
|
||||||
|
|
||||||
:param handle_update: Coroutine that processes the update
|
|
||||||
:param semaphore: Semaphore to limit concurrent tasks
|
|
||||||
:return: bool indicating the result of the update processing
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await handle_update
|
|
||||||
finally:
|
|
||||||
semaphore.release()
|
|
||||||
|
|
||||||
async def _polling(
|
|
||||||
self,
|
|
||||||
bot: Bot,
|
|
||||||
polling_timeout: int = 30,
|
|
||||||
handle_as_tasks: bool = True,
|
|
||||||
backoff_config: BackoffConfig = DEFAULT_BACKOFF_CONFIG,
|
|
||||||
allowed_updates: Optional[List[str]] = None,
|
|
||||||
tasks_concurrency_limit: Optional[int] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Internal polling process
|
|
||||||
|
|
||||||
:param bot:
|
|
||||||
:param polling_timeout: Long-polling wait time
|
|
||||||
:param handle_as_tasks: Run task for each event and no wait result
|
|
||||||
:param backoff_config: backoff-retry config
|
|
||||||
:param allowed_updates: List of the update types you want your bot to receive
|
|
||||||
:param tasks_concurrency_limit: Maximum number of concurrent updates to process
|
|
||||||
(None = no limit), used only if handle_as_tasks is True
|
|
||||||
:param kwargs:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
user: User = await bot.me()
|
|
||||||
loggers.dispatcher.info(
|
|
||||||
"Run polling for bot @%s id=%d - %r", user.username, bot.id, user.full_name
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create semaphore if tasks_concurrency_limit is specified
|
|
||||||
semaphore = None
|
|
||||||
if tasks_concurrency_limit is not None and handle_as_tasks:
|
|
||||||
semaphore = asyncio.Semaphore(tasks_concurrency_limit)
|
|
||||||
|
|
||||||
try:
|
|
||||||
async for update in self._listen_updates(
|
|
||||||
bot,
|
|
||||||
polling_timeout=polling_timeout,
|
|
||||||
backoff_config=backoff_config,
|
|
||||||
allowed_updates=allowed_updates,
|
|
||||||
):
|
|
||||||
handle_update = self._process_update(bot=bot, update=update, **kwargs)
|
|
||||||
if handle_as_tasks:
|
|
||||||
if semaphore:
|
|
||||||
# Use semaphore to limit concurrent tasks
|
|
||||||
await semaphore.acquire()
|
|
||||||
handle_update_task = asyncio.create_task(
|
|
||||||
self._process_with_semaphore(handle_update, semaphore)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
handle_update_task = asyncio.create_task(handle_update)
|
|
||||||
|
|
||||||
self._handle_update_tasks.add(handle_update_task)
|
|
||||||
handle_update_task.add_done_callback(self._handle_update_tasks.discard)
|
|
||||||
else:
|
|
||||||
await handle_update
|
|
||||||
finally:
|
|
||||||
loggers.dispatcher.info(
|
|
||||||
"Polling stopped for bot @%s id=%d - %r", user.username, bot.id, user.full_name
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _feed_webhook_update(self, bot: Bot, update: Update, **kwargs: Any) -> Any:
|
|
||||||
"""
|
|
||||||
The same with `Dispatcher.process_update()` but returns real response instead of bool
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await self.feed_update(bot, update, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
loggers.event.exception(
|
|
||||||
"Cause exception while process update id=%d by bot id=%d\n%s: %s",
|
|
||||||
update.update_id,
|
|
||||||
bot.id,
|
|
||||||
e.__class__.__name__,
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def feed_webhook_update(
|
|
||||||
self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: float = 55, **kwargs: Any
|
|
||||||
) -> Optional[TelegramMethod[TelegramType]]:
|
|
||||||
if not isinstance(update, Update): # Allow to use raw updates
|
|
||||||
update = Update.model_validate(update, context={"bot": bot})
|
|
||||||
|
|
||||||
ctx = contextvars.copy_context()
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
waiter = loop.create_future()
|
|
||||||
|
|
||||||
def release_waiter(*_: Any) -> None:
|
|
||||||
if not waiter.done():
|
|
||||||
waiter.set_result(None)
|
|
||||||
|
|
||||||
timeout_handle = loop.call_later(_timeout, release_waiter)
|
|
||||||
|
|
||||||
process_updates: Future[Any] = asyncio.ensure_future(
|
|
||||||
self._feed_webhook_update(bot=bot, update=update, **kwargs)
|
|
||||||
)
|
|
||||||
process_updates.add_done_callback(release_waiter, context=ctx)
|
|
||||||
|
|
||||||
def process_response(task: Future[Any]) -> None:
|
|
||||||
warnings.warn(
|
|
||||||
"Detected slow response into webhook.\n"
|
|
||||||
"Telegram is waiting for response only first 60 seconds and then re-send update.\n"
|
|
||||||
"For preventing this situation response into webhook returned immediately "
|
|
||||||
"and handler is moved to background and still processing update.",
|
|
||||||
RuntimeWarning,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
result = task.result()
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
if isinstance(result, TelegramMethod):
|
|
||||||
asyncio.ensure_future(self.silent_call_request(bot=bot, result=result))
|
|
||||||
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
await waiter
|
|
||||||
except CancelledError: # pragma: no cover
|
|
||||||
process_updates.remove_done_callback(release_waiter)
|
|
||||||
process_updates.cancel()
|
|
||||||
raise
|
|
||||||
|
|
||||||
if process_updates.done():
|
|
||||||
# TODO: handle exceptions
|
|
||||||
response: Any = process_updates.result()
|
|
||||||
if isinstance(response, TelegramMethod):
|
|
||||||
return response
|
|
||||||
|
|
||||||
else:
|
|
||||||
process_updates.remove_done_callback(release_waiter)
|
|
||||||
process_updates.add_done_callback(process_response, context=ctx)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
timeout_handle.cancel()
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def stop_polling(self) -> None:
|
|
||||||
"""
|
|
||||||
Execute this method if you want to stop polling programmatically
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not self._running_lock.locked():
|
|
||||||
raise RuntimeError("Polling is not started")
|
|
||||||
if not self._stop_signal or not self._stopped_signal:
|
|
||||||
return
|
|
||||||
self._stop_signal.set()
|
|
||||||
await self._stopped_signal.wait()
|
|
||||||
|
|
||||||
def _signal_stop_polling(self, sig: signal.Signals) -> None:
|
|
||||||
if not self._running_lock.locked():
|
|
||||||
return
|
|
||||||
|
|
||||||
loggers.dispatcher.warning("Received %s signal", sig.name)
|
|
||||||
if not self._stop_signal:
|
|
||||||
return
|
|
||||||
self._stop_signal.set()
|
|
||||||
|
|
||||||
async def start_polling(
|
|
||||||
self,
|
|
||||||
*bots: Bot,
|
|
||||||
polling_timeout: int = 10,
|
|
||||||
handle_as_tasks: bool = True,
|
|
||||||
backoff_config: BackoffConfig = DEFAULT_BACKOFF_CONFIG,
|
|
||||||
allowed_updates: Optional[Union[List[str], UNSET_TYPE]] = UNSET,
|
|
||||||
handle_signals: bool = True,
|
|
||||||
close_bot_session: bool = True,
|
|
||||||
tasks_concurrency_limit: Optional[int] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Polling runner
|
|
||||||
|
|
||||||
:param bots: Bot instances (one or more)
|
|
||||||
:param polling_timeout: Long-polling wait time
|
|
||||||
:param handle_as_tasks: Run task for each event and no wait result
|
|
||||||
:param backoff_config: backoff-retry config
|
|
||||||
:param allowed_updates: List of the update types you want your bot to receive
|
|
||||||
By default, all used update types are enabled (resolved from handlers)
|
|
||||||
:param handle_signals: handle signals (SIGINT/SIGTERM)
|
|
||||||
:param close_bot_session: close bot sessions on shutdown
|
|
||||||
:param tasks_concurrency_limit: Maximum number of concurrent updates to process
|
|
||||||
(None = no limit), used only if handle_as_tasks is True
|
|
||||||
:param kwargs: contextual data
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not bots:
|
|
||||||
raise ValueError("At least one bot instance is required to start polling")
|
|
||||||
if "bot" in kwargs:
|
|
||||||
raise ValueError(
|
|
||||||
"Keyword argument 'bot' is not acceptable, "
|
|
||||||
"the bot instance should be passed as positional argument"
|
|
||||||
)
|
|
||||||
|
|
||||||
async with self._running_lock: # Prevent to run this method twice at a once
|
|
||||||
if self._stop_signal is None:
|
|
||||||
self._stop_signal = Event()
|
|
||||||
if self._stopped_signal is None:
|
|
||||||
self._stopped_signal = Event()
|
|
||||||
|
|
||||||
if allowed_updates is UNSET:
|
|
||||||
allowed_updates = self.resolve_used_update_types()
|
|
||||||
|
|
||||||
self._stop_signal.clear()
|
|
||||||
self._stopped_signal.clear()
|
|
||||||
|
|
||||||
if handle_signals:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
with suppress(NotImplementedError): # pragma: no cover
|
|
||||||
# Signals handling is not supported on Windows
|
|
||||||
# It also can't be covered on Windows
|
|
||||||
loop.add_signal_handler(
|
|
||||||
signal.SIGTERM, self._signal_stop_polling, signal.SIGTERM
|
|
||||||
)
|
|
||||||
loop.add_signal_handler(
|
|
||||||
signal.SIGINT, self._signal_stop_polling, signal.SIGINT
|
|
||||||
)
|
|
||||||
|
|
||||||
workflow_data = {
|
|
||||||
"dispatcher": self,
|
|
||||||
"bots": bots,
|
|
||||||
**self.workflow_data,
|
|
||||||
**kwargs,
|
|
||||||
}
|
|
||||||
if "bot" in workflow_data:
|
|
||||||
workflow_data.pop("bot")
|
|
||||||
|
|
||||||
await self.emit_startup(bot=bots[-1], **workflow_data)
|
|
||||||
loggers.dispatcher.info("Start polling")
|
|
||||||
try:
|
|
||||||
tasks: List[asyncio.Task[Any]] = [
|
|
||||||
asyncio.create_task(
|
|
||||||
self._polling(
|
|
||||||
bot=bot,
|
|
||||||
handle_as_tasks=handle_as_tasks,
|
|
||||||
polling_timeout=polling_timeout,
|
|
||||||
backoff_config=backoff_config,
|
|
||||||
allowed_updates=allowed_updates,
|
|
||||||
tasks_concurrency_limit=tasks_concurrency_limit,
|
|
||||||
**workflow_data,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for bot in bots
|
|
||||||
]
|
|
||||||
tasks.append(asyncio.create_task(self._stop_signal.wait()))
|
|
||||||
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
||||||
|
|
||||||
for task in pending:
|
|
||||||
# (mostly) Graceful shutdown unfinished tasks
|
|
||||||
task.cancel()
|
|
||||||
with suppress(CancelledError):
|
|
||||||
await task
|
|
||||||
# Wait finished tasks to propagate unhandled exceptions
|
|
||||||
await asyncio.gather(*done)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
loggers.dispatcher.info("Polling stopped")
|
|
||||||
try:
|
|
||||||
await self.emit_shutdown(bot=bots[-1], **workflow_data)
|
|
||||||
finally:
|
|
||||||
if close_bot_session:
|
|
||||||
await asyncio.gather(*(bot.session.close() for bot in bots))
|
|
||||||
self._stopped_signal.set()
|
|
||||||
|
|
||||||
def run_polling(
|
|
||||||
self,
|
|
||||||
*bots: Bot,
|
|
||||||
polling_timeout: int = 10,
|
|
||||||
handle_as_tasks: bool = True,
|
|
||||||
backoff_config: BackoffConfig = DEFAULT_BACKOFF_CONFIG,
|
|
||||||
allowed_updates: Optional[Union[List[str], UNSET_TYPE]] = UNSET,
|
|
||||||
handle_signals: bool = True,
|
|
||||||
close_bot_session: bool = True,
|
|
||||||
tasks_concurrency_limit: Optional[int] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Run many bots with polling
|
|
||||||
|
|
||||||
:param bots: Bot instances (one or more)
|
|
||||||
:param polling_timeout: Long-polling wait time
|
|
||||||
:param handle_as_tasks: Run task for each event and no wait result
|
|
||||||
:param backoff_config: backoff-retry config
|
|
||||||
:param allowed_updates: List of the update types you want your bot to receive
|
|
||||||
:param handle_signals: handle signals (SIGINT/SIGTERM)
|
|
||||||
:param close_bot_session: close bot sessions on shutdown
|
|
||||||
:param tasks_concurrency_limit: Maximum number of concurrent updates to process
|
|
||||||
(None = no limit), used only if handle_as_tasks is True
|
|
||||||
:param kwargs: contextual data
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
with suppress(KeyboardInterrupt):
|
|
||||||
return asyncio.run(
|
|
||||||
self.start_polling(
|
|
||||||
*bots,
|
|
||||||
**kwargs,
|
|
||||||
polling_timeout=polling_timeout,
|
|
||||||
handle_as_tasks=handle_as_tasks,
|
|
||||||
backoff_config=backoff_config,
|
|
||||||
allowed_updates=allowed_updates,
|
|
||||||
handle_signals=handle_signals,
|
|
||||||
close_bot_session=close_bot_session,
|
|
||||||
tasks_concurrency_limit=tasks_concurrency_limit,
|
|
||||||
)
|
|
||||||
)
|
|
@@ -1,35 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Awaitable, Callable, Dict, NoReturn, Optional, TypeVar, Union
|
|
||||||
from unittest.mock import sentinel
|
|
||||||
|
|
||||||
from ...types import TelegramObject
|
|
||||||
from ..middlewares.base import BaseMiddleware
|
|
||||||
|
|
||||||
MiddlewareEventType = TypeVar("MiddlewareEventType", bound=TelegramObject)
|
|
||||||
NextMiddlewareType = Callable[[MiddlewareEventType, Dict[str, Any]], Awaitable[Any]]
|
|
||||||
MiddlewareType = Union[
|
|
||||||
BaseMiddleware,
|
|
||||||
Callable[
|
|
||||||
[NextMiddlewareType[MiddlewareEventType], MiddlewareEventType, Dict[str, Any]],
|
|
||||||
Awaitable[Any],
|
|
||||||
],
|
|
||||||
]
|
|
||||||
|
|
||||||
UNHANDLED = sentinel.UNHANDLED
|
|
||||||
REJECTED = sentinel.REJECTED
|
|
||||||
|
|
||||||
|
|
||||||
class SkipHandler(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CancelHandler(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def skip(message: Optional[str] = None) -> NoReturn:
|
|
||||||
"""
|
|
||||||
Raise an SkipHandler
|
|
||||||
"""
|
|
||||||
raise SkipHandler(message or "Event skipped")
|
|
@@ -1,53 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Callable, List
|
|
||||||
|
|
||||||
from .handler import CallbackType, HandlerObject
|
|
||||||
|
|
||||||
|
|
||||||
class EventObserver:
|
|
||||||
"""
|
|
||||||
Simple events observer
|
|
||||||
|
|
||||||
Is used for managing events is not related with Telegram
|
|
||||||
(For example startup/shutdown processes)
|
|
||||||
|
|
||||||
Handlers can be registered via decorator or method
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
<observer>.register(my_handler)
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@<observer>()
|
|
||||||
async def my_handler(*args, **kwargs): ...
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.handlers: List[HandlerObject] = []
|
|
||||||
|
|
||||||
def register(self, callback: CallbackType) -> None:
|
|
||||||
"""
|
|
||||||
Register callback with filters
|
|
||||||
"""
|
|
||||||
self.handlers.append(HandlerObject(callback=callback))
|
|
||||||
|
|
||||||
async def trigger(self, *args: Any, **kwargs: Any) -> None:
|
|
||||||
"""
|
|
||||||
Propagate event to handlers.
|
|
||||||
Handler will be called when all its filters is pass.
|
|
||||||
"""
|
|
||||||
for handler in self.handlers:
|
|
||||||
await handler.call(*args, **kwargs)
|
|
||||||
|
|
||||||
def __call__(self) -> Callable[[CallbackType], CallbackType]:
|
|
||||||
"""
|
|
||||||
Decorator for registering event handlers
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(callback: CallbackType) -> CallbackType:
|
|
||||||
self.register(callback)
|
|
||||||
return callback
|
|
||||||
|
|
||||||
return wrapper
|
|
@@ -1,95 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import contextvars
|
|
||||||
import inspect
|
|
||||||
import warnings
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from functools import partial
|
|
||||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
|
||||||
|
|
||||||
from magic_filter.magic import MagicFilter as OriginalMagicFilter
|
|
||||||
|
|
||||||
from aiogram.dispatcher.flags import extract_flags_from_object
|
|
||||||
from aiogram.filters.base import Filter
|
|
||||||
from aiogram.handlers import BaseHandler
|
|
||||||
from aiogram.utils.magic_filter import MagicFilter
|
|
||||||
from aiogram.utils.warnings import Recommendation
|
|
||||||
|
|
||||||
CallbackType = Callable[..., Any]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CallableObject:
|
|
||||||
callback: CallbackType
|
|
||||||
awaitable: bool = field(init=False)
|
|
||||||
params: Set[str] = field(init=False)
|
|
||||||
varkw: bool = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
callback = inspect.unwrap(self.callback)
|
|
||||||
self.awaitable = inspect.isawaitable(callback) or inspect.iscoroutinefunction(callback)
|
|
||||||
spec = inspect.getfullargspec(callback)
|
|
||||||
self.params = {*spec.args, *spec.kwonlyargs}
|
|
||||||
self.varkw = spec.varkw is not None
|
|
||||||
|
|
||||||
def _prepare_kwargs(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
if self.varkw:
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
return {k: kwargs[k] for k in self.params if k in kwargs}
|
|
||||||
|
|
||||||
async def call(self, *args: Any, **kwargs: Any) -> Any:
|
|
||||||
wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs))
|
|
||||||
if self.awaitable:
|
|
||||||
return await wrapped()
|
|
||||||
return await asyncio.to_thread(wrapped)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FilterObject(CallableObject):
|
|
||||||
magic: Optional[MagicFilter] = None
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
if isinstance(self.callback, OriginalMagicFilter):
|
|
||||||
# MagicFilter instance is callable but generates
|
|
||||||
# only "CallOperation" instead of applying the filter
|
|
||||||
self.magic = self.callback
|
|
||||||
self.callback = self.callback.resolve
|
|
||||||
if not isinstance(self.magic, MagicFilter):
|
|
||||||
# Issue: https://github.com/aiogram/aiogram/issues/990
|
|
||||||
warnings.warn(
|
|
||||||
category=Recommendation,
|
|
||||||
message="You are using F provided by magic_filter package directly, "
|
|
||||||
"but it lacks `.as_()` extension."
|
|
||||||
"\n Please change the import statement: from `from magic_filter import F` "
|
|
||||||
"to `from aiogram import F` to silence this warning.",
|
|
||||||
stacklevel=6,
|
|
||||||
)
|
|
||||||
|
|
||||||
super(FilterObject, self).__post_init__()
|
|
||||||
|
|
||||||
if isinstance(self.callback, Filter):
|
|
||||||
self.awaitable = True
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HandlerObject(CallableObject):
|
|
||||||
filters: Optional[List[FilterObject]] = None
|
|
||||||
flags: Dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
super(HandlerObject, self).__post_init__()
|
|
||||||
callback = inspect.unwrap(self.callback)
|
|
||||||
if inspect.isclass(callback) and issubclass(callback, BaseHandler):
|
|
||||||
self.awaitable = True
|
|
||||||
self.flags.update(extract_flags_from_object(callback))
|
|
||||||
|
|
||||||
async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]:
|
|
||||||
if not self.filters:
|
|
||||||
return True, kwargs
|
|
||||||
for event_filter in self.filters:
|
|
||||||
check = await event_filter.call(*args, **kwargs)
|
|
||||||
if not check:
|
|
||||||
return False, kwargs
|
|
||||||
if isinstance(check, dict):
|
|
||||||
kwargs.update(check)
|
|
||||||
return True, kwargs
|
|
@@ -1,141 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
|
||||||
|
|
||||||
from aiogram.dispatcher.middlewares.manager import MiddlewareManager
|
|
||||||
|
|
||||||
from ...exceptions import UnsupportedKeywordArgument
|
|
||||||
from ...filters.base import Filter
|
|
||||||
from ...types import TelegramObject
|
|
||||||
from .bases import UNHANDLED, MiddlewareType, SkipHandler
|
|
||||||
from .handler import CallbackType, FilterObject, HandlerObject
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from aiogram.dispatcher.router import Router
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramEventObserver:
|
|
||||||
"""
|
|
||||||
Event observer for Telegram events
|
|
||||||
|
|
||||||
Here you can register handler with filter.
|
|
||||||
This observer will stop event propagation when first handler is pass.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, router: Router, event_name: str) -> None:
|
|
||||||
self.router: Router = router
|
|
||||||
self.event_name: str = event_name
|
|
||||||
|
|
||||||
self.handlers: List[HandlerObject] = []
|
|
||||||
|
|
||||||
self.middleware = MiddlewareManager()
|
|
||||||
self.outer_middleware = MiddlewareManager()
|
|
||||||
|
|
||||||
# Re-used filters check method from already implemented handler object
|
|
||||||
# with dummy callback which never will be used
|
|
||||||
self._handler = HandlerObject(callback=lambda: True, filters=[])
|
|
||||||
|
|
||||||
def filter(self, *filters: CallbackType) -> None:
|
|
||||||
"""
|
|
||||||
Register filter for all handlers of this event observer
|
|
||||||
|
|
||||||
:param filters: positional filters
|
|
||||||
"""
|
|
||||||
if self._handler.filters is None:
|
|
||||||
self._handler.filters = []
|
|
||||||
self._handler.filters.extend([FilterObject(filter_) for filter_ in filters])
|
|
||||||
|
|
||||||
def _resolve_middlewares(self) -> List[MiddlewareType[TelegramObject]]:
|
|
||||||
middlewares: List[MiddlewareType[TelegramObject]] = []
|
|
||||||
for router in reversed(tuple(self.router.chain_head)):
|
|
||||||
observer = router.observers.get(self.event_name)
|
|
||||||
if observer:
|
|
||||||
middlewares.extend(observer.middleware)
|
|
||||||
|
|
||||||
return middlewares
|
|
||||||
|
|
||||||
def register(
|
|
||||||
self,
|
|
||||||
callback: CallbackType,
|
|
||||||
*filters: CallbackType,
|
|
||||||
flags: Optional[Dict[str, Any]] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> CallbackType:
|
|
||||||
"""
|
|
||||||
Register event handler
|
|
||||||
"""
|
|
||||||
if kwargs:
|
|
||||||
raise UnsupportedKeywordArgument(
|
|
||||||
"Passing any additional keyword arguments to the registrar method "
|
|
||||||
"is not supported.\n"
|
|
||||||
"This error may be caused when you are trying to register filters like in 2.x "
|
|
||||||
"version of this framework, if it's true just look at correspoding "
|
|
||||||
"documentation pages.\n"
|
|
||||||
f"Please remove the {set(kwargs.keys())} arguments from this call.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
if flags is None:
|
|
||||||
flags = {}
|
|
||||||
|
|
||||||
for item in filters:
|
|
||||||
if isinstance(item, Filter):
|
|
||||||
item.update_handler_flags(flags=flags)
|
|
||||||
|
|
||||||
self.handlers.append(
|
|
||||||
HandlerObject(
|
|
||||||
callback=callback,
|
|
||||||
filters=[FilterObject(filter_) for filter_ in filters],
|
|
||||||
flags=flags,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
def wrap_outer_middleware(
|
|
||||||
self, callback: Any, event: TelegramObject, data: Dict[str, Any]
|
|
||||||
) -> Any:
|
|
||||||
wrapped_outer = self.middleware.wrap_middlewares(
|
|
||||||
self.outer_middleware,
|
|
||||||
callback,
|
|
||||||
)
|
|
||||||
return wrapped_outer(event, data)
|
|
||||||
|
|
||||||
def check_root_filters(self, event: TelegramObject, **kwargs: Any) -> Any:
|
|
||||||
return self._handler.check(event, **kwargs)
|
|
||||||
|
|
||||||
async def trigger(self, event: TelegramObject, **kwargs: Any) -> Any:
|
|
||||||
"""
|
|
||||||
Propagate event to handlers and stops propagation on first match.
|
|
||||||
Handler will be called when all its filters are pass.
|
|
||||||
"""
|
|
||||||
for handler in self.handlers:
|
|
||||||
kwargs["handler"] = handler
|
|
||||||
result, data = await handler.check(event, **kwargs)
|
|
||||||
if result:
|
|
||||||
kwargs.update(data)
|
|
||||||
try:
|
|
||||||
wrapped_inner = self.outer_middleware.wrap_middlewares(
|
|
||||||
self._resolve_middlewares(),
|
|
||||||
handler.call,
|
|
||||||
)
|
|
||||||
return await wrapped_inner(event, kwargs)
|
|
||||||
except SkipHandler:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return UNHANDLED
|
|
||||||
|
|
||||||
def __call__(
|
|
||||||
self,
|
|
||||||
*filters: CallbackType,
|
|
||||||
flags: Optional[Dict[str, Any]] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> Callable[[CallbackType], CallbackType]:
|
|
||||||
"""
|
|
||||||
Decorator for registering event handlers
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(callback: CallbackType) -> CallbackType:
|
|
||||||
self.register(callback, *filters, flags=flags, **kwargs)
|
|
||||||
return callback
|
|
||||||
|
|
||||||
return wrapper
|
|
@@ -1,127 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast, overload
|
|
||||||
|
|
||||||
from magic_filter import AttrDict, MagicFilter
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from aiogram.dispatcher.event.handler import HandlerObject
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Flag:
|
|
||||||
name: str
|
|
||||||
value: Any
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class FlagDecorator:
|
|
||||||
flag: Flag
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _with_flag(cls, flag: Flag) -> "FlagDecorator":
|
|
||||||
return cls(flag)
|
|
||||||
|
|
||||||
def _with_value(self, value: Any) -> "FlagDecorator":
|
|
||||||
new_flag = Flag(self.flag.name, value)
|
|
||||||
return self._with_flag(new_flag)
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __call__(self, value: Callable[..., Any], /) -> Callable[..., Any]: # type: ignore
|
|
||||||
pass
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __call__(self, value: Any, /) -> "FlagDecorator":
|
|
||||||
pass
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __call__(self, **kwargs: Any) -> "FlagDecorator":
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __call__(
|
|
||||||
self,
|
|
||||||
value: Optional[Any] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> Union[Callable[..., Any], "FlagDecorator"]:
|
|
||||||
if value and kwargs:
|
|
||||||
raise ValueError("The arguments `value` and **kwargs can not be used together")
|
|
||||||
|
|
||||||
if value is not None and callable(value):
|
|
||||||
value.aiogram_flag = {
|
|
||||||
**extract_flags_from_object(value),
|
|
||||||
self.flag.name: self.flag.value,
|
|
||||||
}
|
|
||||||
return cast(Callable[..., Any], value)
|
|
||||||
return self._with_value(AttrDict(kwargs) if value is None else value)
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
|
|
||||||
class _ChatActionFlagProtocol(FlagDecorator):
|
|
||||||
def __call__( # type: ignore[override]
|
|
||||||
self,
|
|
||||||
action: str = ...,
|
|
||||||
interval: float = ...,
|
|
||||||
initial_sleep: float = ...,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> FlagDecorator:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FlagGenerator:
|
|
||||||
def __getattr__(self, name: str) -> FlagDecorator:
|
|
||||||
if name[0] == "_":
|
|
||||||
raise AttributeError("Flag name must NOT start with underscore")
|
|
||||||
return FlagDecorator(Flag(name, True))
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
chat_action: _ChatActionFlagProtocol
|
|
||||||
|
|
||||||
|
|
||||||
def extract_flags_from_object(obj: Any) -> Dict[str, Any]:
|
|
||||||
if not hasattr(obj, "aiogram_flag"):
|
|
||||||
return {}
|
|
||||||
return cast(Dict[str, Any], obj.aiogram_flag)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_flags(handler: Union["HandlerObject", Dict[str, Any]]) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Extract flags from handler or middleware context data
|
|
||||||
|
|
||||||
:param handler: handler object or data
|
|
||||||
:return: dictionary with all handler flags
|
|
||||||
"""
|
|
||||||
if isinstance(handler, dict) and "handler" in handler:
|
|
||||||
handler = handler["handler"]
|
|
||||||
if hasattr(handler, "flags"):
|
|
||||||
return handler.flags
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def get_flag(
|
|
||||||
handler: Union["HandlerObject", Dict[str, Any]],
|
|
||||||
name: str,
|
|
||||||
*,
|
|
||||||
default: Optional[Any] = None,
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
Get flag by name
|
|
||||||
|
|
||||||
:param handler: handler object or data
|
|
||||||
:param name: name of the flag
|
|
||||||
:param default: default value (None)
|
|
||||||
:return: value of the flag or default
|
|
||||||
"""
|
|
||||||
flags = extract_flags(handler)
|
|
||||||
return flags.get(name, default)
|
|
||||||
|
|
||||||
|
|
||||||
def check_flags(handler: Union["HandlerObject", Dict[str, Any]], magic: MagicFilter) -> Any:
|
|
||||||
"""
|
|
||||||
Check flags via magic filter
|
|
||||||
|
|
||||||
:param handler: handler object or data
|
|
||||||
:param magic: instance of the magic
|
|
||||||
:return: the result of magic filter check
|
|
||||||
"""
|
|
||||||
flags = extract_flags(handler)
|
|
||||||
return magic.resolve(AttrDict(flags))
|
|
@@ -1,29 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Any, Awaitable, Callable, Dict, TypeVar
|
|
||||||
|
|
||||||
from aiogram.types import TelegramObject
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMiddleware(ABC):
|
|
||||||
"""
|
|
||||||
Generic middleware class
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
|
||||||
event: TelegramObject,
|
|
||||||
data: Dict[str, Any],
|
|
||||||
) -> Any: # pragma: no cover
|
|
||||||
"""
|
|
||||||
Execute middleware
|
|
||||||
|
|
||||||
:param handler: Wrapped handler in middlewares chain
|
|
||||||
:param event: Incoming event (Subclass of :class:`aiogram.types.base.TelegramObject`)
|
|
||||||
:param data: Contextual data. Will be mapped to handler arguments
|
|
||||||
:return: :class:`Any`
|
|
||||||
"""
|
|
||||||
pass
|
|
@@ -1,97 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, TypedDict
|
|
||||||
|
|
||||||
from typing_extensions import NotRequired
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from aiogram import Bot, Dispatcher, Router
|
|
||||||
from aiogram.dispatcher.event.handler import HandlerObject
|
|
||||||
from aiogram.dispatcher.middlewares.user_context import EventContext
|
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
from aiogram.fsm.storage.base import BaseStorage
|
|
||||||
from aiogram.types import Chat, Update, User
|
|
||||||
from aiogram.utils.i18n import I18n, I18nMiddleware
|
|
||||||
|
|
||||||
|
|
||||||
class DispatcherData(TypedDict, total=False):
|
|
||||||
"""
|
|
||||||
Dispatcher and bot related data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
dispatcher: Dispatcher
|
|
||||||
"""Instance of the Dispatcher from which the handler was called."""
|
|
||||||
bot: Bot
|
|
||||||
"""Bot that received the update."""
|
|
||||||
bots: NotRequired[list[Bot]]
|
|
||||||
"""List of all bots in the Dispatcher. Used only in polling mode."""
|
|
||||||
event_update: Update
|
|
||||||
"""Update object that triggered the handler."""
|
|
||||||
event_router: Router
|
|
||||||
"""Router that was used to find the handler."""
|
|
||||||
handler: NotRequired[HandlerObject]
|
|
||||||
"""Handler object that was called.
|
|
||||||
Available only in the handler itself and inner middlewares."""
|
|
||||||
|
|
||||||
|
|
||||||
class UserContextData(TypedDict, total=False):
|
|
||||||
"""
|
|
||||||
Event context related data about user and chat.
|
|
||||||
"""
|
|
||||||
|
|
||||||
event_context: EventContext
|
|
||||||
"""Event context object that contains user and chat data."""
|
|
||||||
event_from_user: NotRequired[User]
|
|
||||||
"""User object that triggered the handler."""
|
|
||||||
event_chat: NotRequired[Chat]
|
|
||||||
"""Chat object that triggered the handler.
|
|
||||||
.. deprecated:: 3.5.0
|
|
||||||
Use :attr:`event_context.chat` instead."""
|
|
||||||
event_thread_id: NotRequired[int]
|
|
||||||
"""Thread ID of the chat that triggered the handler.
|
|
||||||
.. deprecated:: 3.5.0
|
|
||||||
Use :attr:`event_context.chat` instead."""
|
|
||||||
event_business_connection_id: NotRequired[str]
|
|
||||||
"""Business connection ID of the chat that triggered the handler.
|
|
||||||
.. deprecated:: 3.5.0
|
|
||||||
Use :attr:`event_context.business_connection_id` instead."""
|
|
||||||
|
|
||||||
|
|
||||||
class FSMData(TypedDict, total=False):
|
|
||||||
"""
|
|
||||||
FSM related data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
fsm_storage: BaseStorage
|
|
||||||
"""Storage used for FSM."""
|
|
||||||
state: NotRequired[FSMContext]
|
|
||||||
"""Current state of the FSM."""
|
|
||||||
raw_state: NotRequired[str | None]
|
|
||||||
"""Raw state of the FSM."""
|
|
||||||
|
|
||||||
|
|
||||||
class I18nData(TypedDict, total=False):
|
|
||||||
"""
|
|
||||||
I18n related data.
|
|
||||||
|
|
||||||
Is not included by default, you need to add it to your own Data class if you need it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
i18n: I18n
|
|
||||||
"""I18n object."""
|
|
||||||
i18n_middleware: I18nMiddleware
|
|
||||||
"""I18n middleware."""
|
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareData(
|
|
||||||
DispatcherData,
|
|
||||||
UserContextData,
|
|
||||||
FSMData,
|
|
||||||
# I18nData, # Disabled by default, add it if you need it to your own Data class.
|
|
||||||
total=False,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Data passed to the handler by the middlewares.
|
|
||||||
|
|
||||||
You can add your own data by extending this class.
|
|
||||||
"""
|
|
@@ -1,36 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, cast
|
|
||||||
|
|
||||||
from ...types import TelegramObject, Update
|
|
||||||
from ...types.error_event import ErrorEvent
|
|
||||||
from ..event.bases import UNHANDLED, CancelHandler, SkipHandler
|
|
||||||
from .base import BaseMiddleware
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..router import Router
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorsMiddleware(BaseMiddleware):
|
|
||||||
def __init__(self, router: Router):
|
|
||||||
self.router = router
|
|
||||||
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
|
||||||
event: TelegramObject,
|
|
||||||
data: Dict[str, Any],
|
|
||||||
) -> Any:
|
|
||||||
try:
|
|
||||||
return await handler(event, data)
|
|
||||||
except (SkipHandler, CancelHandler): # pragma: no cover
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
response = await self.router.propagate_event(
|
|
||||||
update_type="error",
|
|
||||||
event=ErrorEvent(update=cast(Update, event), exception=e),
|
|
||||||
**data,
|
|
||||||
)
|
|
||||||
if response is not UNHANDLED:
|
|
||||||
return response
|
|
||||||
raise
|
|
@@ -1,65 +0,0 @@
|
|||||||
import functools
|
|
||||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Union, overload
|
|
||||||
|
|
||||||
from aiogram.dispatcher.event.bases import (
|
|
||||||
MiddlewareEventType,
|
|
||||||
MiddlewareType,
|
|
||||||
NextMiddlewareType,
|
|
||||||
)
|
|
||||||
from aiogram.dispatcher.event.handler import CallbackType
|
|
||||||
from aiogram.types import TelegramObject
|
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareManager(Sequence[MiddlewareType[TelegramObject]]):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._middlewares: List[MiddlewareType[TelegramObject]] = []
|
|
||||||
|
|
||||||
def register(
|
|
||||||
self,
|
|
||||||
middleware: MiddlewareType[TelegramObject],
|
|
||||||
) -> MiddlewareType[TelegramObject]:
|
|
||||||
self._middlewares.append(middleware)
|
|
||||||
return middleware
|
|
||||||
|
|
||||||
def unregister(self, middleware: MiddlewareType[TelegramObject]) -> None:
|
|
||||||
self._middlewares.remove(middleware)
|
|
||||||
|
|
||||||
def __call__(
|
|
||||||
self,
|
|
||||||
middleware: Optional[MiddlewareType[TelegramObject]] = None,
|
|
||||||
) -> Union[
|
|
||||||
Callable[[MiddlewareType[TelegramObject]], MiddlewareType[TelegramObject]],
|
|
||||||
MiddlewareType[TelegramObject],
|
|
||||||
]:
|
|
||||||
if middleware is None:
|
|
||||||
return self.register
|
|
||||||
return self.register(middleware)
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __getitem__(self, item: int) -> MiddlewareType[TelegramObject]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __getitem__(self, item: slice) -> Sequence[MiddlewareType[TelegramObject]]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __getitem__(
|
|
||||||
self, item: Union[int, slice]
|
|
||||||
) -> Union[MiddlewareType[TelegramObject], Sequence[MiddlewareType[TelegramObject]]]:
|
|
||||||
return self._middlewares[item]
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._middlewares)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def wrap_middlewares(
|
|
||||||
middlewares: Sequence[MiddlewareType[MiddlewareEventType]], handler: CallbackType
|
|
||||||
) -> NextMiddlewareType[MiddlewareEventType]:
|
|
||||||
@functools.wraps(handler)
|
|
||||||
def handler_wrapper(event: TelegramObject, kwargs: Dict[str, Any]) -> Any:
|
|
||||||
return handler(event, **kwargs)
|
|
||||||
|
|
||||||
middleware = handler_wrapper
|
|
||||||
for m in reversed(middlewares):
|
|
||||||
middleware = functools.partial(m, middleware) # type: ignore[assignment]
|
|
||||||
return middleware
|
|
@@ -1,182 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
||||||
|
|
||||||
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
|
||||||
from aiogram.types import (
|
|
||||||
Chat,
|
|
||||||
ChatBoostSourcePremium,
|
|
||||||
InaccessibleMessage,
|
|
||||||
TelegramObject,
|
|
||||||
Update,
|
|
||||||
User,
|
|
||||||
)
|
|
||||||
|
|
||||||
EVENT_CONTEXT_KEY = "event_context"
|
|
||||||
|
|
||||||
EVENT_FROM_USER_KEY = "event_from_user"
|
|
||||||
EVENT_CHAT_KEY = "event_chat"
|
|
||||||
EVENT_THREAD_ID_KEY = "event_thread_id"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class EventContext:
|
|
||||||
chat: Optional[Chat] = None
|
|
||||||
user: Optional[User] = None
|
|
||||||
thread_id: Optional[int] = None
|
|
||||||
business_connection_id: Optional[str] = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_id(self) -> Optional[int]:
|
|
||||||
return self.user.id if self.user else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chat_id(self) -> Optional[int]:
|
|
||||||
return self.chat.id if self.chat else None
|
|
||||||
|
|
||||||
|
|
||||||
class UserContextMiddleware(BaseMiddleware):
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
|
||||||
event: TelegramObject,
|
|
||||||
data: Dict[str, Any],
|
|
||||||
) -> Any:
|
|
||||||
if not isinstance(event, Update):
|
|
||||||
raise RuntimeError("UserContextMiddleware got an unexpected event type!")
|
|
||||||
event_context = data[EVENT_CONTEXT_KEY] = self.resolve_event_context(event=event)
|
|
||||||
|
|
||||||
# Backward compatibility
|
|
||||||
if event_context.user is not None:
|
|
||||||
data[EVENT_FROM_USER_KEY] = event_context.user
|
|
||||||
if event_context.chat is not None:
|
|
||||||
data[EVENT_CHAT_KEY] = event_context.chat
|
|
||||||
if event_context.thread_id is not None:
|
|
||||||
data[EVENT_THREAD_ID_KEY] = event_context.thread_id
|
|
||||||
|
|
||||||
return await handler(event, data)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def resolve_event_context(cls, event: Update) -> EventContext:
|
|
||||||
"""
|
|
||||||
Resolve chat and user instance from Update object
|
|
||||||
"""
|
|
||||||
if event.message:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.message.chat,
|
|
||||||
user=event.message.from_user,
|
|
||||||
thread_id=(
|
|
||||||
event.message.message_thread_id if event.message.is_topic_message else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if event.edited_message:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.edited_message.chat,
|
|
||||||
user=event.edited_message.from_user,
|
|
||||||
thread_id=(
|
|
||||||
event.edited_message.message_thread_id
|
|
||||||
if event.edited_message.is_topic_message
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if event.channel_post:
|
|
||||||
return EventContext(chat=event.channel_post.chat)
|
|
||||||
if event.edited_channel_post:
|
|
||||||
return EventContext(chat=event.edited_channel_post.chat)
|
|
||||||
if event.inline_query:
|
|
||||||
return EventContext(user=event.inline_query.from_user)
|
|
||||||
if event.chosen_inline_result:
|
|
||||||
return EventContext(user=event.chosen_inline_result.from_user)
|
|
||||||
if event.callback_query:
|
|
||||||
callback_query_message = event.callback_query.message
|
|
||||||
if callback_query_message:
|
|
||||||
return EventContext(
|
|
||||||
chat=callback_query_message.chat,
|
|
||||||
user=event.callback_query.from_user,
|
|
||||||
thread_id=(
|
|
||||||
callback_query_message.message_thread_id
|
|
||||||
if not isinstance(callback_query_message, InaccessibleMessage)
|
|
||||||
and callback_query_message.is_topic_message
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
business_connection_id=(
|
|
||||||
callback_query_message.business_connection_id
|
|
||||||
if not isinstance(callback_query_message, InaccessibleMessage)
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return EventContext(user=event.callback_query.from_user)
|
|
||||||
if event.shipping_query:
|
|
||||||
return EventContext(user=event.shipping_query.from_user)
|
|
||||||
if event.pre_checkout_query:
|
|
||||||
return EventContext(user=event.pre_checkout_query.from_user)
|
|
||||||
if event.poll_answer:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.poll_answer.voter_chat,
|
|
||||||
user=event.poll_answer.user,
|
|
||||||
)
|
|
||||||
if event.my_chat_member:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.my_chat_member.chat, user=event.my_chat_member.from_user
|
|
||||||
)
|
|
||||||
if event.chat_member:
|
|
||||||
return EventContext(chat=event.chat_member.chat, user=event.chat_member.from_user)
|
|
||||||
if event.chat_join_request:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.chat_join_request.chat, user=event.chat_join_request.from_user
|
|
||||||
)
|
|
||||||
if event.message_reaction:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.message_reaction.chat,
|
|
||||||
user=event.message_reaction.user,
|
|
||||||
)
|
|
||||||
if event.message_reaction_count:
|
|
||||||
return EventContext(chat=event.message_reaction_count.chat)
|
|
||||||
if event.chat_boost:
|
|
||||||
# We only check the premium source, because only it has a sender user,
|
|
||||||
# other sources have a user, but it is not the sender, but the recipient
|
|
||||||
if isinstance(event.chat_boost.boost.source, ChatBoostSourcePremium):
|
|
||||||
return EventContext(
|
|
||||||
chat=event.chat_boost.chat,
|
|
||||||
user=event.chat_boost.boost.source.user,
|
|
||||||
)
|
|
||||||
|
|
||||||
return EventContext(chat=event.chat_boost.chat)
|
|
||||||
if event.removed_chat_boost:
|
|
||||||
return EventContext(chat=event.removed_chat_boost.chat)
|
|
||||||
if event.deleted_business_messages:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.deleted_business_messages.chat,
|
|
||||||
business_connection_id=event.deleted_business_messages.business_connection_id,
|
|
||||||
)
|
|
||||||
if event.business_connection:
|
|
||||||
return EventContext(
|
|
||||||
user=event.business_connection.user,
|
|
||||||
business_connection_id=event.business_connection.id,
|
|
||||||
)
|
|
||||||
if event.business_message:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.business_message.chat,
|
|
||||||
user=event.business_message.from_user,
|
|
||||||
thread_id=(
|
|
||||||
event.business_message.message_thread_id
|
|
||||||
if event.business_message.is_topic_message
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
business_connection_id=event.business_message.business_connection_id,
|
|
||||||
)
|
|
||||||
if event.edited_business_message:
|
|
||||||
return EventContext(
|
|
||||||
chat=event.edited_business_message.chat,
|
|
||||||
user=event.edited_business_message.from_user,
|
|
||||||
thread_id=(
|
|
||||||
event.edited_business_message.message_thread_id
|
|
||||||
if event.edited_business_message.is_topic_message
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
business_connection_id=event.edited_business_message.business_connection_id,
|
|
||||||
)
|
|
||||||
if event.purchased_paid_media:
|
|
||||||
return EventContext(
|
|
||||||
user=event.purchased_paid_media.from_user,
|
|
||||||
)
|
|
||||||
return EventContext()
|
|
@@ -1,275 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Dict, Final, Generator, List, Optional, Set
|
|
||||||
|
|
||||||
from ..types import TelegramObject
|
|
||||||
from .event.bases import REJECTED, UNHANDLED
|
|
||||||
from .event.event import EventObserver
|
|
||||||
from .event.telegram import TelegramEventObserver
|
|
||||||
|
|
||||||
INTERNAL_UPDATE_TYPES: Final[frozenset[str]] = frozenset({"update", "error"})
|
|
||||||
|
|
||||||
|
|
||||||
class Router:
|
|
||||||
"""
|
|
||||||
Router can route update, and it nested update types like messages, callback query,
|
|
||||||
polls and all other event types.
|
|
||||||
|
|
||||||
Event handlers can be registered in observer by two ways:
|
|
||||||
|
|
||||||
- By observer method - :obj:`router.<event_type>.register(handler, <filters, ...>)`
|
|
||||||
- By decorator - :obj:`@router.<event_type>(<filters, ...>)`
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *, name: Optional[str] = None) -> None:
|
|
||||||
"""
|
|
||||||
:param name: Optional router name, can be useful for debugging
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.name = name or hex(id(self))
|
|
||||||
|
|
||||||
self._parent_router: Optional[Router] = None
|
|
||||||
self.sub_routers: List[Router] = []
|
|
||||||
|
|
||||||
# Observers
|
|
||||||
self.message = TelegramEventObserver(router=self, event_name="message")
|
|
||||||
self.edited_message = TelegramEventObserver(router=self, event_name="edited_message")
|
|
||||||
self.channel_post = TelegramEventObserver(router=self, event_name="channel_post")
|
|
||||||
self.edited_channel_post = TelegramEventObserver(
|
|
||||||
router=self, event_name="edited_channel_post"
|
|
||||||
)
|
|
||||||
self.inline_query = TelegramEventObserver(router=self, event_name="inline_query")
|
|
||||||
self.chosen_inline_result = TelegramEventObserver(
|
|
||||||
router=self, event_name="chosen_inline_result"
|
|
||||||
)
|
|
||||||
self.callback_query = TelegramEventObserver(router=self, event_name="callback_query")
|
|
||||||
self.shipping_query = TelegramEventObserver(router=self, event_name="shipping_query")
|
|
||||||
self.pre_checkout_query = TelegramEventObserver(
|
|
||||||
router=self, event_name="pre_checkout_query"
|
|
||||||
)
|
|
||||||
self.poll = TelegramEventObserver(router=self, event_name="poll")
|
|
||||||
self.poll_answer = TelegramEventObserver(router=self, event_name="poll_answer")
|
|
||||||
self.my_chat_member = TelegramEventObserver(router=self, event_name="my_chat_member")
|
|
||||||
self.chat_member = TelegramEventObserver(router=self, event_name="chat_member")
|
|
||||||
self.chat_join_request = TelegramEventObserver(router=self, event_name="chat_join_request")
|
|
||||||
self.message_reaction = TelegramEventObserver(router=self, event_name="message_reaction")
|
|
||||||
self.message_reaction_count = TelegramEventObserver(
|
|
||||||
router=self, event_name="message_reaction_count"
|
|
||||||
)
|
|
||||||
self.chat_boost = TelegramEventObserver(router=self, event_name="chat_boost")
|
|
||||||
self.removed_chat_boost = TelegramEventObserver(
|
|
||||||
router=self, event_name="removed_chat_boost"
|
|
||||||
)
|
|
||||||
self.deleted_business_messages = TelegramEventObserver(
|
|
||||||
router=self, event_name="deleted_business_messages"
|
|
||||||
)
|
|
||||||
self.business_connection = TelegramEventObserver(
|
|
||||||
router=self, event_name="business_connection"
|
|
||||||
)
|
|
||||||
self.edited_business_message = TelegramEventObserver(
|
|
||||||
router=self, event_name="edited_business_message"
|
|
||||||
)
|
|
||||||
self.business_message = TelegramEventObserver(router=self, event_name="business_message")
|
|
||||||
self.purchased_paid_media = TelegramEventObserver(
|
|
||||||
router=self, event_name="purchased_paid_media"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.errors = self.error = TelegramEventObserver(router=self, event_name="error")
|
|
||||||
|
|
||||||
self.startup = EventObserver()
|
|
||||||
self.shutdown = EventObserver()
|
|
||||||
|
|
||||||
self.observers: Dict[str, TelegramEventObserver] = {
|
|
||||||
"message": self.message,
|
|
||||||
"edited_message": self.edited_message,
|
|
||||||
"channel_post": self.channel_post,
|
|
||||||
"edited_channel_post": self.edited_channel_post,
|
|
||||||
"inline_query": self.inline_query,
|
|
||||||
"chosen_inline_result": self.chosen_inline_result,
|
|
||||||
"callback_query": self.callback_query,
|
|
||||||
"shipping_query": self.shipping_query,
|
|
||||||
"pre_checkout_query": self.pre_checkout_query,
|
|
||||||
"poll": self.poll,
|
|
||||||
"poll_answer": self.poll_answer,
|
|
||||||
"my_chat_member": self.my_chat_member,
|
|
||||||
"chat_member": self.chat_member,
|
|
||||||
"chat_join_request": self.chat_join_request,
|
|
||||||
"message_reaction": self.message_reaction,
|
|
||||||
"message_reaction_count": self.message_reaction_count,
|
|
||||||
"chat_boost": self.chat_boost,
|
|
||||||
"removed_chat_boost": self.removed_chat_boost,
|
|
||||||
"deleted_business_messages": self.deleted_business_messages,
|
|
||||||
"business_connection": self.business_connection,
|
|
||||||
"edited_business_message": self.edited_business_message,
|
|
||||||
"business_message": self.business_message,
|
|
||||||
"purchased_paid_media": self.purchased_paid_media,
|
|
||||||
"error": self.errors,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{type(self).__name__} {self.name!r}"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<{self}>"
|
|
||||||
|
|
||||||
def resolve_used_update_types(self, skip_events: Optional[Set[str]] = None) -> List[str]:
|
|
||||||
"""
|
|
||||||
Resolve registered event names
|
|
||||||
|
|
||||||
Is useful for getting updates only for registered event types.
|
|
||||||
|
|
||||||
:param skip_events: skip specified event names
|
|
||||||
:return: set of registered names
|
|
||||||
"""
|
|
||||||
handlers_in_use: Set[str] = set()
|
|
||||||
if skip_events is None:
|
|
||||||
skip_events = set()
|
|
||||||
skip_events = {*skip_events, *INTERNAL_UPDATE_TYPES}
|
|
||||||
|
|
||||||
for router in self.chain_tail:
|
|
||||||
for update_name, observer in router.observers.items():
|
|
||||||
if observer.handlers and update_name not in skip_events:
|
|
||||||
handlers_in_use.add(update_name)
|
|
||||||
|
|
||||||
return list(sorted(handlers_in_use)) # NOQA: C413
|
|
||||||
|
|
||||||
async def propagate_event(self, update_type: str, event: TelegramObject, **kwargs: Any) -> Any:
|
|
||||||
kwargs.update(event_router=self)
|
|
||||||
observer = self.observers.get(update_type)
|
|
||||||
|
|
||||||
async def _wrapped(telegram_event: TelegramObject, **data: Any) -> Any:
|
|
||||||
return await self._propagate_event(
|
|
||||||
observer=observer, update_type=update_type, event=telegram_event, **data
|
|
||||||
)
|
|
||||||
|
|
||||||
if observer:
|
|
||||||
return await observer.wrap_outer_middleware(_wrapped, event=event, data=kwargs)
|
|
||||||
return await _wrapped(event, **kwargs)
|
|
||||||
|
|
||||||
async def _propagate_event(
|
|
||||||
self,
|
|
||||||
observer: Optional[TelegramEventObserver],
|
|
||||||
update_type: str,
|
|
||||||
event: TelegramObject,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> Any:
|
|
||||||
response = UNHANDLED
|
|
||||||
if observer:
|
|
||||||
# Check globally defined filters before any other handler will be checked.
|
|
||||||
# This check is placed here instead of `trigger` method to add possibility
|
|
||||||
# to pass context to handlers from global filters.
|
|
||||||
result, data = await observer.check_root_filters(event, **kwargs)
|
|
||||||
if not result:
|
|
||||||
return UNHANDLED
|
|
||||||
kwargs.update(data)
|
|
||||||
|
|
||||||
response = await observer.trigger(event, **kwargs)
|
|
||||||
if response is REJECTED: # pragma: no cover
|
|
||||||
# Possible only if some handler returns REJECTED
|
|
||||||
return UNHANDLED
|
|
||||||
if response is not UNHANDLED:
|
|
||||||
return response
|
|
||||||
|
|
||||||
for router in self.sub_routers:
|
|
||||||
response = await router.propagate_event(update_type=update_type, event=event, **kwargs)
|
|
||||||
if response is not UNHANDLED:
|
|
||||||
break
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chain_head(self) -> Generator[Router, None, None]:
|
|
||||||
router: Optional[Router] = self
|
|
||||||
while router:
|
|
||||||
yield router
|
|
||||||
router = router.parent_router
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chain_tail(self) -> Generator[Router, None, None]:
|
|
||||||
yield self
|
|
||||||
for router in self.sub_routers:
|
|
||||||
yield from router.chain_tail
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parent_router(self) -> Optional[Router]:
|
|
||||||
return self._parent_router
|
|
||||||
|
|
||||||
@parent_router.setter
|
|
||||||
def parent_router(self, router: Router) -> None:
|
|
||||||
"""
|
|
||||||
Internal property setter of parent router fot this router.
|
|
||||||
Do not use this method in own code.
|
|
||||||
All routers should be included via `include_router` method.
|
|
||||||
|
|
||||||
Self- and circular- referencing are not allowed here
|
|
||||||
|
|
||||||
:param router:
|
|
||||||
"""
|
|
||||||
if not isinstance(router, Router):
|
|
||||||
raise ValueError(f"router should be instance of Router not {type(router).__name__!r}")
|
|
||||||
if self._parent_router:
|
|
||||||
raise RuntimeError(f"Router is already attached to {self._parent_router!r}")
|
|
||||||
if self == router:
|
|
||||||
raise RuntimeError("Self-referencing routers is not allowed")
|
|
||||||
|
|
||||||
parent: Optional[Router] = router
|
|
||||||
while parent is not None:
|
|
||||||
if parent == self:
|
|
||||||
raise RuntimeError("Circular referencing of Router is not allowed")
|
|
||||||
|
|
||||||
parent = parent.parent_router
|
|
||||||
|
|
||||||
self._parent_router = router
|
|
||||||
router.sub_routers.append(self)
|
|
||||||
|
|
||||||
def include_routers(self, *routers: Router) -> None:
|
|
||||||
"""
|
|
||||||
Attach multiple routers.
|
|
||||||
|
|
||||||
:param routers:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not routers:
|
|
||||||
raise ValueError("At least one router must be provided")
|
|
||||||
for router in routers:
|
|
||||||
self.include_router(router)
|
|
||||||
|
|
||||||
def include_router(self, router: Router) -> Router:
|
|
||||||
"""
|
|
||||||
Attach another router.
|
|
||||||
|
|
||||||
:param router:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not isinstance(router, Router):
|
|
||||||
raise ValueError(
|
|
||||||
f"router should be instance of Router not {type(router).__class__.__name__}"
|
|
||||||
)
|
|
||||||
router.parent_router = self
|
|
||||||
return router
|
|
||||||
|
|
||||||
async def emit_startup(self, *args: Any, **kwargs: Any) -> None:
|
|
||||||
"""
|
|
||||||
Recursively call startup callbacks
|
|
||||||
|
|
||||||
:param args:
|
|
||||||
:param kwargs:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
kwargs.update(router=self)
|
|
||||||
await self.startup.trigger(*args, **kwargs)
|
|
||||||
for router in self.sub_routers:
|
|
||||||
await router.emit_startup(*args, **kwargs)
|
|
||||||
|
|
||||||
async def emit_shutdown(self, *args: Any, **kwargs: Any) -> None:
|
|
||||||
"""
|
|
||||||
Recursively call shutdown callbacks to graceful shutdown
|
|
||||||
|
|
||||||
:param args:
|
|
||||||
:param kwargs:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
kwargs.update(router=self)
|
|
||||||
await self.shutdown.trigger(*args, **kwargs)
|
|
||||||
for router in self.sub_routers:
|
|
||||||
await router.emit_shutdown(*args, **kwargs)
|
|
@@ -1,71 +0,0 @@
|
|||||||
from .bot_command_scope_type import BotCommandScopeType
|
|
||||||
from .chat_action import ChatAction
|
|
||||||
from .chat_boost_source_type import ChatBoostSourceType
|
|
||||||
from .chat_member_status import ChatMemberStatus
|
|
||||||
from .chat_type import ChatType
|
|
||||||
from .content_type import ContentType
|
|
||||||
from .currency import Currency
|
|
||||||
from .dice_emoji import DiceEmoji
|
|
||||||
from .encrypted_passport_element import EncryptedPassportElement
|
|
||||||
from .inline_query_result_type import InlineQueryResultType
|
|
||||||
from .input_media_type import InputMediaType
|
|
||||||
from .input_paid_media_type import InputPaidMediaType
|
|
||||||
from .input_profile_photo_type import InputProfilePhotoType
|
|
||||||
from .input_story_content_type import InputStoryContentType
|
|
||||||
from .keyboard_button_poll_type_type import KeyboardButtonPollTypeType
|
|
||||||
from .mask_position_point import MaskPositionPoint
|
|
||||||
from .menu_button_type import MenuButtonType
|
|
||||||
from .message_entity_type import MessageEntityType
|
|
||||||
from .message_origin_type import MessageOriginType
|
|
||||||
from .owned_gift_type import OwnedGiftType
|
|
||||||
from .paid_media_type import PaidMediaType
|
|
||||||
from .parse_mode import ParseMode
|
|
||||||
from .passport_element_error_type import PassportElementErrorType
|
|
||||||
from .poll_type import PollType
|
|
||||||
from .reaction_type_type import ReactionTypeType
|
|
||||||
from .revenue_withdrawal_state_type import RevenueWithdrawalStateType
|
|
||||||
from .sticker_format import StickerFormat
|
|
||||||
from .sticker_type import StickerType
|
|
||||||
from .story_area_type_type import StoryAreaTypeType
|
|
||||||
from .topic_icon_color import TopicIconColor
|
|
||||||
from .transaction_partner_type import TransactionPartnerType
|
|
||||||
from .transaction_partner_user_transaction_type_enum import (
|
|
||||||
TransactionPartnerUserTransactionTypeEnum,
|
|
||||||
)
|
|
||||||
from .update_type import UpdateType
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"BotCommandScopeType",
|
|
||||||
"ChatAction",
|
|
||||||
"ChatBoostSourceType",
|
|
||||||
"ChatMemberStatus",
|
|
||||||
"ChatType",
|
|
||||||
"ContentType",
|
|
||||||
"Currency",
|
|
||||||
"DiceEmoji",
|
|
||||||
"EncryptedPassportElement",
|
|
||||||
"InlineQueryResultType",
|
|
||||||
"InputMediaType",
|
|
||||||
"InputPaidMediaType",
|
|
||||||
"InputProfilePhotoType",
|
|
||||||
"InputStoryContentType",
|
|
||||||
"KeyboardButtonPollTypeType",
|
|
||||||
"MaskPositionPoint",
|
|
||||||
"MenuButtonType",
|
|
||||||
"MessageEntityType",
|
|
||||||
"MessageOriginType",
|
|
||||||
"OwnedGiftType",
|
|
||||||
"PaidMediaType",
|
|
||||||
"ParseMode",
|
|
||||||
"PassportElementErrorType",
|
|
||||||
"PollType",
|
|
||||||
"ReactionTypeType",
|
|
||||||
"RevenueWithdrawalStateType",
|
|
||||||
"StickerFormat",
|
|
||||||
"StickerType",
|
|
||||||
"StoryAreaTypeType",
|
|
||||||
"TopicIconColor",
|
|
||||||
"TransactionPartnerType",
|
|
||||||
"TransactionPartnerUserTransactionTypeEnum",
|
|
||||||
"UpdateType",
|
|
||||||
)
|
|
@@ -1,17 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class BotCommandScopeType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents the scope to which bot commands are applied.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#botcommandscope
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEFAULT = "default"
|
|
||||||
ALL_PRIVATE_CHATS = "all_private_chats"
|
|
||||||
ALL_GROUP_CHATS = "all_group_chats"
|
|
||||||
ALL_CHAT_ADMINISTRATORS = "all_chat_administrators"
|
|
||||||
CHAT = "chat"
|
|
||||||
CHAT_ADMINISTRATORS = "chat_administrators"
|
|
||||||
CHAT_MEMBER = "chat_member"
|
|
@@ -1,32 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ChatAction(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents bot actions.
|
|
||||||
|
|
||||||
Choose one, depending on what the user is about to receive:
|
|
||||||
|
|
||||||
- typing for text messages,
|
|
||||||
- upload_photo for photos,
|
|
||||||
- record_video or upload_video for videos,
|
|
||||||
- record_voice or upload_voice for voice notes,
|
|
||||||
- upload_document for general files,
|
|
||||||
- choose_sticker for stickers,
|
|
||||||
- find_location for location data,
|
|
||||||
- record_video_note or upload_video_note for video notes.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#sendchataction
|
|
||||||
"""
|
|
||||||
|
|
||||||
TYPING = "typing"
|
|
||||||
UPLOAD_PHOTO = "upload_photo"
|
|
||||||
RECORD_VIDEO = "record_video"
|
|
||||||
UPLOAD_VIDEO = "upload_video"
|
|
||||||
RECORD_VOICE = "record_voice"
|
|
||||||
UPLOAD_VOICE = "upload_voice"
|
|
||||||
UPLOAD_DOCUMENT = "upload_document"
|
|
||||||
CHOOSE_STICKER = "choose_sticker"
|
|
||||||
FIND_LOCATION = "find_location"
|
|
||||||
RECORD_VIDEO_NOTE = "record_video_note"
|
|
||||||
UPLOAD_VIDEO_NOTE = "upload_video_note"
|
|
@@ -1,13 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ChatBoostSourceType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents a type of chat boost source.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#chatboostsource
|
|
||||||
"""
|
|
||||||
|
|
||||||
PREMIUM = "premium"
|
|
||||||
GIFT_CODE = "gift_code"
|
|
||||||
GIVEAWAY = "giveaway"
|
|
@@ -1,16 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ChatMemberStatus(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents chat member status.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#chatmember
|
|
||||||
"""
|
|
||||||
|
|
||||||
CREATOR = "creator"
|
|
||||||
ADMINISTRATOR = "administrator"
|
|
||||||
MEMBER = "member"
|
|
||||||
RESTRICTED = "restricted"
|
|
||||||
LEFT = "left"
|
|
||||||
KICKED = "kicked"
|
|
@@ -1,15 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ChatType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents a chat type
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#chat
|
|
||||||
"""
|
|
||||||
|
|
||||||
SENDER = "sender"
|
|
||||||
PRIVATE = "private"
|
|
||||||
GROUP = "group"
|
|
||||||
SUPERGROUP = "supergroup"
|
|
||||||
CHANNEL = "channel"
|
|
@@ -1,69 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ContentType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents a type of content in message
|
|
||||||
"""
|
|
||||||
|
|
||||||
UNKNOWN = "unknown"
|
|
||||||
ANY = "any"
|
|
||||||
TEXT = "text"
|
|
||||||
ANIMATION = "animation"
|
|
||||||
AUDIO = "audio"
|
|
||||||
DOCUMENT = "document"
|
|
||||||
PAID_MEDIA = "paid_media"
|
|
||||||
PHOTO = "photo"
|
|
||||||
STICKER = "sticker"
|
|
||||||
STORY = "story"
|
|
||||||
VIDEO = "video"
|
|
||||||
VIDEO_NOTE = "video_note"
|
|
||||||
VOICE = "voice"
|
|
||||||
CONTACT = "contact"
|
|
||||||
DICE = "dice"
|
|
||||||
GAME = "game"
|
|
||||||
POLL = "poll"
|
|
||||||
VENUE = "venue"
|
|
||||||
LOCATION = "location"
|
|
||||||
NEW_CHAT_MEMBERS = "new_chat_members"
|
|
||||||
LEFT_CHAT_MEMBER = "left_chat_member"
|
|
||||||
NEW_CHAT_TITLE = "new_chat_title"
|
|
||||||
NEW_CHAT_PHOTO = "new_chat_photo"
|
|
||||||
DELETE_CHAT_PHOTO = "delete_chat_photo"
|
|
||||||
GROUP_CHAT_CREATED = "group_chat_created"
|
|
||||||
SUPERGROUP_CHAT_CREATED = "supergroup_chat_created"
|
|
||||||
CHANNEL_CHAT_CREATED = "channel_chat_created"
|
|
||||||
MESSAGE_AUTO_DELETE_TIMER_CHANGED = "message_auto_delete_timer_changed"
|
|
||||||
MIGRATE_TO_CHAT_ID = "migrate_to_chat_id"
|
|
||||||
MIGRATE_FROM_CHAT_ID = "migrate_from_chat_id"
|
|
||||||
PINNED_MESSAGE = "pinned_message"
|
|
||||||
INVOICE = "invoice"
|
|
||||||
SUCCESSFUL_PAYMENT = "successful_payment"
|
|
||||||
REFUNDED_PAYMENT = "refunded_payment"
|
|
||||||
USERS_SHARED = "users_shared"
|
|
||||||
CHAT_SHARED = "chat_shared"
|
|
||||||
GIFT = "gift"
|
|
||||||
UNIQUE_GIFT = "unique_gift"
|
|
||||||
CONNECTED_WEBSITE = "connected_website"
|
|
||||||
WRITE_ACCESS_ALLOWED = "write_access_allowed"
|
|
||||||
PASSPORT_DATA = "passport_data"
|
|
||||||
PROXIMITY_ALERT_TRIGGERED = "proximity_alert_triggered"
|
|
||||||
BOOST_ADDED = "boost_added"
|
|
||||||
CHAT_BACKGROUND_SET = "chat_background_set"
|
|
||||||
FORUM_TOPIC_CREATED = "forum_topic_created"
|
|
||||||
FORUM_TOPIC_EDITED = "forum_topic_edited"
|
|
||||||
FORUM_TOPIC_CLOSED = "forum_topic_closed"
|
|
||||||
FORUM_TOPIC_REOPENED = "forum_topic_reopened"
|
|
||||||
GENERAL_FORUM_TOPIC_HIDDEN = "general_forum_topic_hidden"
|
|
||||||
GENERAL_FORUM_TOPIC_UNHIDDEN = "general_forum_topic_unhidden"
|
|
||||||
GIVEAWAY_CREATED = "giveaway_created"
|
|
||||||
GIVEAWAY = "giveaway"
|
|
||||||
GIVEAWAY_WINNERS = "giveaway_winners"
|
|
||||||
GIVEAWAY_COMPLETED = "giveaway_completed"
|
|
||||||
PAID_MESSAGE_PRICE_CHANGED = "paid_message_price_changed"
|
|
||||||
VIDEO_CHAT_SCHEDULED = "video_chat_scheduled"
|
|
||||||
VIDEO_CHAT_STARTED = "video_chat_started"
|
|
||||||
VIDEO_CHAT_ENDED = "video_chat_ended"
|
|
||||||
VIDEO_CHAT_PARTICIPANTS_INVITED = "video_chat_participants_invited"
|
|
||||||
WEB_APP_DATA = "web_app_data"
|
|
||||||
USER_SHARED = "user_shared"
|
|
@@ -1,96 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class Currency(str, Enum):
|
|
||||||
"""
|
|
||||||
Currencies supported by Telegram Bot API
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/payments#supported-currencies
|
|
||||||
"""
|
|
||||||
|
|
||||||
AED = "AED"
|
|
||||||
AFN = "AFN"
|
|
||||||
ALL = "ALL"
|
|
||||||
AMD = "AMD"
|
|
||||||
ARS = "ARS"
|
|
||||||
AUD = "AUD"
|
|
||||||
AZN = "AZN"
|
|
||||||
BAM = "BAM"
|
|
||||||
BDT = "BDT"
|
|
||||||
BGN = "BGN"
|
|
||||||
BND = "BND"
|
|
||||||
BOB = "BOB"
|
|
||||||
BRL = "BRL"
|
|
||||||
BYN = "BYN"
|
|
||||||
CAD = "CAD"
|
|
||||||
CHF = "CHF"
|
|
||||||
CLP = "CLP"
|
|
||||||
CNY = "CNY"
|
|
||||||
COP = "COP"
|
|
||||||
CRC = "CRC"
|
|
||||||
CZK = "CZK"
|
|
||||||
DKK = "DKK"
|
|
||||||
DOP = "DOP"
|
|
||||||
DZD = "DZD"
|
|
||||||
EGP = "EGP"
|
|
||||||
ETB = "ETB"
|
|
||||||
EUR = "EUR"
|
|
||||||
GBP = "GBP"
|
|
||||||
GEL = "GEL"
|
|
||||||
GTQ = "GTQ"
|
|
||||||
HKD = "HKD"
|
|
||||||
HNL = "HNL"
|
|
||||||
HRK = "HRK"
|
|
||||||
HUF = "HUF"
|
|
||||||
IDR = "IDR"
|
|
||||||
ILS = "ILS"
|
|
||||||
INR = "INR"
|
|
||||||
ISK = "ISK"
|
|
||||||
JMD = "JMD"
|
|
||||||
JPY = "JPY"
|
|
||||||
KES = "KES"
|
|
||||||
KGS = "KGS"
|
|
||||||
KRW = "KRW"
|
|
||||||
KZT = "KZT"
|
|
||||||
LBP = "LBP"
|
|
||||||
LKR = "LKR"
|
|
||||||
MAD = "MAD"
|
|
||||||
MDL = "MDL"
|
|
||||||
MNT = "MNT"
|
|
||||||
MUR = "MUR"
|
|
||||||
MVR = "MVR"
|
|
||||||
MXN = "MXN"
|
|
||||||
MYR = "MYR"
|
|
||||||
MZN = "MZN"
|
|
||||||
NGN = "NGN"
|
|
||||||
NIO = "NIO"
|
|
||||||
NOK = "NOK"
|
|
||||||
NPR = "NPR"
|
|
||||||
NZD = "NZD"
|
|
||||||
PAB = "PAB"
|
|
||||||
PEN = "PEN"
|
|
||||||
PHP = "PHP"
|
|
||||||
PKR = "PKR"
|
|
||||||
PLN = "PLN"
|
|
||||||
PYG = "PYG"
|
|
||||||
QAR = "QAR"
|
|
||||||
RON = "RON"
|
|
||||||
RSD = "RSD"
|
|
||||||
RUB = "RUB"
|
|
||||||
SAR = "SAR"
|
|
||||||
SEK = "SEK"
|
|
||||||
SGD = "SGD"
|
|
||||||
THB = "THB"
|
|
||||||
TJS = "TJS"
|
|
||||||
TRY = "TRY"
|
|
||||||
TTD = "TTD"
|
|
||||||
TWD = "TWD"
|
|
||||||
TZS = "TZS"
|
|
||||||
UAH = "UAH"
|
|
||||||
UGX = "UGX"
|
|
||||||
USD = "USD"
|
|
||||||
UYU = "UYU"
|
|
||||||
UZS = "UZS"
|
|
||||||
VND = "VND"
|
|
||||||
YER = "YER"
|
|
||||||
ZAR = "ZAR"
|
|
@@ -1,16 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class DiceEmoji(str, Enum):
|
|
||||||
"""
|
|
||||||
Emoji on which the dice throw animation is based
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#dice
|
|
||||||
"""
|
|
||||||
|
|
||||||
DICE = "🎲"
|
|
||||||
DART = "🎯"
|
|
||||||
BASKETBALL = "🏀"
|
|
||||||
FOOTBALL = "⚽"
|
|
||||||
SLOT_MACHINE = "🎰"
|
|
||||||
BOWLING = "🎳"
|
|
@@ -1,23 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class EncryptedPassportElement(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents type of encrypted passport element.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#encryptedpassportelement
|
|
||||||
"""
|
|
||||||
|
|
||||||
PERSONAL_DETAILS = "personal_details"
|
|
||||||
PASSPORT = "passport"
|
|
||||||
DRIVER_LICENSE = "driver_license"
|
|
||||||
IDENTITY_CARD = "identity_card"
|
|
||||||
INTERNAL_PASSPORT = "internal_passport"
|
|
||||||
ADDRESS = "address"
|
|
||||||
UTILITY_BILL = "utility_bill"
|
|
||||||
BANK_STATEMENT = "bank_statement"
|
|
||||||
RENTAL_AGREEMENT = "rental_agreement"
|
|
||||||
PASSPORT_REGISTRATION = "passport_registration"
|
|
||||||
TEMPORARY_REGISTRATION = "temporary_registration"
|
|
||||||
PHONE_NUMBER = "phone_number"
|
|
||||||
EMAIL = "email"
|
|
@@ -1,23 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class InlineQueryResultType(str, Enum):
|
|
||||||
"""
|
|
||||||
Type of inline query result
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#inlinequeryresult
|
|
||||||
"""
|
|
||||||
|
|
||||||
AUDIO = "audio"
|
|
||||||
DOCUMENT = "document"
|
|
||||||
GIF = "gif"
|
|
||||||
MPEG4_GIF = "mpeg4_gif"
|
|
||||||
PHOTO = "photo"
|
|
||||||
STICKER = "sticker"
|
|
||||||
VIDEO = "video"
|
|
||||||
VOICE = "voice"
|
|
||||||
ARTICLE = "article"
|
|
||||||
CONTACT = "contact"
|
|
||||||
GAME = "game"
|
|
||||||
LOCATION = "location"
|
|
||||||
VENUE = "venue"
|
|
@@ -1,15 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class InputMediaType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents input media type
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#inputmedia
|
|
||||||
"""
|
|
||||||
|
|
||||||
ANIMATION = "animation"
|
|
||||||
AUDIO = "audio"
|
|
||||||
DOCUMENT = "document"
|
|
||||||
PHOTO = "photo"
|
|
||||||
VIDEO = "video"
|
|
@@ -1,12 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class InputPaidMediaType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents the type of a media in a paid message.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#inputpaidmedia
|
|
||||||
"""
|
|
||||||
|
|
||||||
PHOTO = "photo"
|
|
||||||
VIDEO = "video"
|
|
@@ -1,12 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class InputProfilePhotoType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents input profile photo type
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#inputprofilephoto
|
|
||||||
"""
|
|
||||||
|
|
||||||
STATIC = "static"
|
|
||||||
ANIMATED = "animated"
|
|
@@ -1,12 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class InputStoryContentType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents input story content photo type.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#inputstorycontentphoto
|
|
||||||
"""
|
|
||||||
|
|
||||||
PHOTO = "photo"
|
|
||||||
VIDEO = "video"
|
|
@@ -1,12 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class KeyboardButtonPollTypeType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents type of a poll, which is allowed to be created and sent when the corresponding button is pressed.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#keyboardbuttonpolltype
|
|
||||||
"""
|
|
||||||
|
|
||||||
QUIZ = "quiz"
|
|
||||||
REGULAR = "regular"
|
|
@@ -1,14 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class MaskPositionPoint(str, Enum):
|
|
||||||
"""
|
|
||||||
The part of the face relative to which the mask should be placed.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#maskposition
|
|
||||||
"""
|
|
||||||
|
|
||||||
FOREHEAD = "forehead"
|
|
||||||
EYES = "eyes"
|
|
||||||
MOUTH = "mouth"
|
|
||||||
CHIN = "chin"
|
|
@@ -1,13 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class MenuButtonType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents an type of Menu button
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#menubuttondefault
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEFAULT = "default"
|
|
||||||
COMMANDS = "commands"
|
|
||||||
WEB_APP = "web_app"
|
|
@@ -1,29 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class MessageEntityType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents type of message entity
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#messageentity
|
|
||||||
"""
|
|
||||||
|
|
||||||
MENTION = "mention"
|
|
||||||
HASHTAG = "hashtag"
|
|
||||||
CASHTAG = "cashtag"
|
|
||||||
BOT_COMMAND = "bot_command"
|
|
||||||
URL = "url"
|
|
||||||
EMAIL = "email"
|
|
||||||
PHONE_NUMBER = "phone_number"
|
|
||||||
BOLD = "bold"
|
|
||||||
ITALIC = "italic"
|
|
||||||
UNDERLINE = "underline"
|
|
||||||
STRIKETHROUGH = "strikethrough"
|
|
||||||
SPOILER = "spoiler"
|
|
||||||
BLOCKQUOTE = "blockquote"
|
|
||||||
EXPANDABLE_BLOCKQUOTE = "expandable_blockquote"
|
|
||||||
CODE = "code"
|
|
||||||
PRE = "pre"
|
|
||||||
TEXT_LINK = "text_link"
|
|
||||||
TEXT_MENTION = "text_mention"
|
|
||||||
CUSTOM_EMOJI = "custom_emoji"
|
|
@@ -1,14 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class MessageOriginType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents origin of a message.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#messageorigin
|
|
||||||
"""
|
|
||||||
|
|
||||||
USER = "user"
|
|
||||||
HIDDEN_USER = "hidden_user"
|
|
||||||
CHAT = "chat"
|
|
||||||
CHANNEL = "channel"
|
|
@@ -1,12 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class OwnedGiftType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents owned gift type
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#ownedgift
|
|
||||||
"""
|
|
||||||
|
|
||||||
REGULAR = "regular"
|
|
||||||
UNIQUE = "unique"
|
|
@@ -1,13 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class PaidMediaType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents the type of a media in a paid message.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#paidmedia
|
|
||||||
"""
|
|
||||||
|
|
||||||
PHOTO = "photo"
|
|
||||||
PREVIEW = "preview"
|
|
||||||
VIDEO = "video"
|
|
@@ -1,13 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ParseMode(str, Enum):
|
|
||||||
"""
|
|
||||||
Formatting options
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#formatting-options
|
|
||||||
"""
|
|
||||||
|
|
||||||
MARKDOWN_V2 = "MarkdownV2"
|
|
||||||
MARKDOWN = "Markdown"
|
|
||||||
HTML = "HTML"
|
|
@@ -1,19 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class PassportElementErrorType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents a passport element error type.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#passportelementerror
|
|
||||||
"""
|
|
||||||
|
|
||||||
DATA = "data"
|
|
||||||
FRONT_SIDE = "front_side"
|
|
||||||
REVERSE_SIDE = "reverse_side"
|
|
||||||
SELFIE = "selfie"
|
|
||||||
FILE = "file"
|
|
||||||
FILES = "files"
|
|
||||||
TRANSLATION_FILE = "translation_file"
|
|
||||||
TRANSLATION_FILES = "translation_files"
|
|
||||||
UNSPECIFIED = "unspecified"
|
|
@@ -1,12 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class PollType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents poll type
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#poll
|
|
||||||
"""
|
|
||||||
|
|
||||||
REGULAR = "regular"
|
|
||||||
QUIZ = "quiz"
|
|
@@ -1,13 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ReactionTypeType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents reaction type.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#reactiontype
|
|
||||||
"""
|
|
||||||
|
|
||||||
EMOJI = "emoji"
|
|
||||||
CUSTOM_EMOJI = "custom_emoji"
|
|
||||||
PAID = "paid"
|
|
@@ -1,13 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class RevenueWithdrawalStateType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents a revenue withdrawal state type
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#revenuewithdrawalstate
|
|
||||||
"""
|
|
||||||
|
|
||||||
FAILED = "failed"
|
|
||||||
PENDING = "pending"
|
|
||||||
SUCCEEDED = "succeeded"
|
|
@@ -1,13 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class StickerFormat(str, Enum):
|
|
||||||
"""
|
|
||||||
Format of the sticker
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#createnewstickerset
|
|
||||||
"""
|
|
||||||
|
|
||||||
STATIC = "static"
|
|
||||||
ANIMATED = "animated"
|
|
||||||
VIDEO = "video"
|
|
@@ -1,13 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class StickerType(str, Enum):
|
|
||||||
"""
|
|
||||||
The part of the face relative to which the mask should be placed.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#maskposition
|
|
||||||
"""
|
|
||||||
|
|
||||||
REGULAR = "regular"
|
|
||||||
MASK = "mask"
|
|
||||||
CUSTOM_EMOJI = "custom_emoji"
|
|
@@ -1,15 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class StoryAreaTypeType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents input profile photo type
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#storyareatype
|
|
||||||
"""
|
|
||||||
|
|
||||||
LOCATION = "location"
|
|
||||||
SUGGESTED_REACTION = "suggested_reaction"
|
|
||||||
LINK = "link"
|
|
||||||
WEATHER = "weather"
|
|
||||||
UNIQUE_GIFT = "unique_gift"
|
|
@@ -1,16 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class TopicIconColor(int, Enum):
|
|
||||||
"""
|
|
||||||
Color of the topic icon in RGB format.
|
|
||||||
|
|
||||||
Source: https://github.com/telegramdesktop/tdesktop/blob/991fe491c5ae62705d77aa8fdd44a79caf639c45/Telegram/SourceFiles/data/data_forum_topic.cpp#L51-L56
|
|
||||||
"""
|
|
||||||
|
|
||||||
BLUE = 0x6FB9F0
|
|
||||||
YELLOW = 0xFFD67E
|
|
||||||
VIOLET = 0xCB86DB
|
|
||||||
GREEN = 0x8EEE98
|
|
||||||
ROSE = 0xFF93B2
|
|
||||||
RED = 0xFB6F5F
|
|
@@ -1,17 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionPartnerType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents a type of transaction partner.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#transactionpartner
|
|
||||||
"""
|
|
||||||
|
|
||||||
FRAGMENT = "fragment"
|
|
||||||
OTHER = "other"
|
|
||||||
USER = "user"
|
|
||||||
TELEGRAM_ADS = "telegram_ads"
|
|
||||||
TELEGRAM_API = "telegram_api"
|
|
||||||
AFFILIATE_PROGRAM = "affiliate_program"
|
|
||||||
CHAT = "chat"
|
|
@@ -1,15 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionPartnerUserTransactionTypeEnum(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents type of the transaction that were made by partner user.
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#transactionpartneruser
|
|
||||||
"""
|
|
||||||
|
|
||||||
INVOICE_PAYMENT = "invoice_payment"
|
|
||||||
PAID_MEDIA_PAYMENT = "paid_media_payment"
|
|
||||||
GIFT_PURCHASE = "gift_purchase"
|
|
||||||
PREMIUM_PURCHASE = "premium_purchase"
|
|
||||||
BUSINESS_ACCOUNT_TRANSFER = "business_account_transfer"
|
|
@@ -1,33 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateType(str, Enum):
|
|
||||||
"""
|
|
||||||
This object represents the complete list of allowed update types
|
|
||||||
|
|
||||||
Source: https://core.telegram.org/bots/api#update
|
|
||||||
"""
|
|
||||||
|
|
||||||
MESSAGE = "message"
|
|
||||||
EDITED_MESSAGE = "edited_message"
|
|
||||||
CHANNEL_POST = "channel_post"
|
|
||||||
EDITED_CHANNEL_POST = "edited_channel_post"
|
|
||||||
BUSINESS_CONNECTION = "business_connection"
|
|
||||||
BUSINESS_MESSAGE = "business_message"
|
|
||||||
EDITED_BUSINESS_MESSAGE = "edited_business_message"
|
|
||||||
DELETED_BUSINESS_MESSAGES = "deleted_business_messages"
|
|
||||||
MESSAGE_REACTION = "message_reaction"
|
|
||||||
MESSAGE_REACTION_COUNT = "message_reaction_count"
|
|
||||||
INLINE_QUERY = "inline_query"
|
|
||||||
CHOSEN_INLINE_RESULT = "chosen_inline_result"
|
|
||||||
CALLBACK_QUERY = "callback_query"
|
|
||||||
SHIPPING_QUERY = "shipping_query"
|
|
||||||
PRE_CHECKOUT_QUERY = "pre_checkout_query"
|
|
||||||
PURCHASED_PAID_MEDIA = "purchased_paid_media"
|
|
||||||
POLL = "poll"
|
|
||||||
POLL_ANSWER = "poll_answer"
|
|
||||||
MY_CHAT_MEMBER = "my_chat_member"
|
|
||||||
CHAT_MEMBER = "chat_member"
|
|
||||||
CHAT_JOIN_REQUEST = "chat_join_request"
|
|
||||||
CHAT_BOOST = "chat_boost"
|
|
||||||
REMOVED_CHAT_BOOST = "removed_chat_boost"
|
|
@@ -1,199 +0,0 @@
|
|||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from aiogram.methods import TelegramMethod
|
|
||||||
from aiogram.methods.base import TelegramType
|
|
||||||
from aiogram.utils.link import docs_url
|
|
||||||
|
|
||||||
|
|
||||||
class AiogramError(Exception):
|
|
||||||
"""
|
|
||||||
Base exception for all aiogram errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DetailedAiogramError(AiogramError):
|
|
||||||
"""
|
|
||||||
Base exception for all aiogram errors with detailed message.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url: Optional[str] = None
|
|
||||||
|
|
||||||
def __init__(self, message: str) -> None:
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
message = self.message
|
|
||||||
if self.url:
|
|
||||||
message += f"\n(background on this error at: {self.url})"
|
|
||||||
return message
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{type(self).__name__}('{self}')"
|
|
||||||
|
|
||||||
|
|
||||||
class CallbackAnswerException(AiogramError):
|
|
||||||
"""
|
|
||||||
Exception for callback answer.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class SceneException(AiogramError):
|
|
||||||
"""
|
|
||||||
Exception for scenes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedKeywordArgument(DetailedAiogramError):
|
|
||||||
"""
|
|
||||||
Exception raised when a keyword argument is passed as filter.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = docs_url("migration_2_to_3.html", fragment_="filtering-events")
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramAPIError(DetailedAiogramError):
|
|
||||||
"""
|
|
||||||
Base exception for all Telegram API errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
label: str = "Telegram server says"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
message: str,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(message=message)
|
|
||||||
self.method = method
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
original_message = super().__str__()
|
|
||||||
return f"{self.label} - {original_message}"
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramNetworkError(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Base exception for all Telegram network errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
label = "HTTP Client says"
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramRetryAfter(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Exception raised when flood control exceeds.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = "https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
message: str,
|
|
||||||
retry_after: int,
|
|
||||||
) -> None:
|
|
||||||
description = f"Flood control exceeded on method {type(method).__name__!r}"
|
|
||||||
if chat_id := getattr(method, "chat_id", None):
|
|
||||||
description += f" in chat {chat_id}"
|
|
||||||
description += f". Retry in {retry_after} seconds."
|
|
||||||
description += f"\nOriginal description: {message}"
|
|
||||||
|
|
||||||
super().__init__(method=method, message=description)
|
|
||||||
self.retry_after = retry_after
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramMigrateToChat(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Exception raised when chat has been migrated to a supergroup.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = "https://core.telegram.org/bots/api#responseparameters"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
method: TelegramMethod[TelegramType],
|
|
||||||
message: str,
|
|
||||||
migrate_to_chat_id: int,
|
|
||||||
) -> None:
|
|
||||||
description = f"The group has been migrated to a supergroup with id {migrate_to_chat_id}"
|
|
||||||
if chat_id := getattr(method, "chat_id", None):
|
|
||||||
description += f" from {chat_id}"
|
|
||||||
description += f"\nOriginal description: {message}"
|
|
||||||
super().__init__(method=method, message=message)
|
|
||||||
self.migrate_to_chat_id = migrate_to_chat_id
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramBadRequest(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Exception raised when request is malformed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramNotFound(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Exception raised when chat, message, user, etc. not found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramConflictError(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Exception raised when bot token is already used by another application in polling mode.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramUnauthorizedError(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Exception raised when bot token is invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramForbiddenError(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Exception raised when bot is kicked from chat or etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramServerError(TelegramAPIError):
|
|
||||||
"""
|
|
||||||
Exception raised when Telegram server returns 5xx error.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class RestartingTelegram(TelegramServerError):
|
|
||||||
"""
|
|
||||||
Exception raised when Telegram server is restarting.
|
|
||||||
|
|
||||||
It seems like this error is not used by Telegram anymore,
|
|
||||||
but it's still here for backward compatibility.
|
|
||||||
|
|
||||||
Currently, you should expect that Telegram can raise RetryAfter (with timeout 5 seconds)
|
|
||||||
error instead of this one.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramEntityTooLarge(TelegramNetworkError):
|
|
||||||
"""
|
|
||||||
Exception raised when you are trying to send a file that is too large.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = "https://core.telegram.org/bots/api#sending-files"
|
|
||||||
|
|
||||||
|
|
||||||
class ClientDecodeError(AiogramError):
|
|
||||||
"""
|
|
||||||
Exception raised when client can't decode response. (Malformed response, etc.)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message: str, original: Exception, data: Any) -> None:
|
|
||||||
self.message = message
|
|
||||||
self.original = original
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
original_type = type(self.original)
|
|
||||||
return (
|
|
||||||
f"{self.message}\n"
|
|
||||||
f"Caused from error: "
|
|
||||||
f"{original_type.__module__}.{original_type.__name__}: {self.original}\n"
|
|
||||||
f"Content: {self.data}"
|
|
||||||
)
|
|
@@ -1,51 +0,0 @@
|
|||||||
from .base import Filter
|
|
||||||
from .chat_member_updated import (
|
|
||||||
ADMINISTRATOR,
|
|
||||||
CREATOR,
|
|
||||||
IS_ADMIN,
|
|
||||||
IS_MEMBER,
|
|
||||||
IS_NOT_MEMBER,
|
|
||||||
JOIN_TRANSITION,
|
|
||||||
KICKED,
|
|
||||||
LEAVE_TRANSITION,
|
|
||||||
LEFT,
|
|
||||||
MEMBER,
|
|
||||||
PROMOTED_TRANSITION,
|
|
||||||
RESTRICTED,
|
|
||||||
ChatMemberUpdatedFilter,
|
|
||||||
)
|
|
||||||
from .command import Command, CommandObject, CommandStart
|
|
||||||
from .exception import ExceptionMessageFilter, ExceptionTypeFilter
|
|
||||||
from .logic import and_f, invert_f, or_f
|
|
||||||
from .magic_data import MagicData
|
|
||||||
from .state import StateFilter
|
|
||||||
|
|
||||||
BaseFilter = Filter
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"Filter",
|
|
||||||
"BaseFilter",
|
|
||||||
"Command",
|
|
||||||
"CommandObject",
|
|
||||||
"CommandStart",
|
|
||||||
"ExceptionMessageFilter",
|
|
||||||
"ExceptionTypeFilter",
|
|
||||||
"StateFilter",
|
|
||||||
"MagicData",
|
|
||||||
"ChatMemberUpdatedFilter",
|
|
||||||
"CREATOR",
|
|
||||||
"ADMINISTRATOR",
|
|
||||||
"MEMBER",
|
|
||||||
"RESTRICTED",
|
|
||||||
"LEFT",
|
|
||||||
"KICKED",
|
|
||||||
"IS_MEMBER",
|
|
||||||
"IS_ADMIN",
|
|
||||||
"PROMOTED_TRANSITION",
|
|
||||||
"IS_NOT_MEMBER",
|
|
||||||
"JOIN_TRANSITION",
|
|
||||||
"LEAVE_TRANSITION",
|
|
||||||
"and_f",
|
|
||||||
"or_f",
|
|
||||||
"invert_f",
|
|
||||||
)
|
|
@@ -1,55 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Union
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from aiogram.filters.logic import _InvertFilter
|
|
||||||
|
|
||||||
|
|
||||||
class Filter(ABC):
|
|
||||||
"""
|
|
||||||
If you want to register own filters like builtin filters you will need to write subclass
|
|
||||||
of this class with overriding the :code:`__call__`
|
|
||||||
method and adding filter attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
# This checking type-hint is needed because mypy checks validity of overrides and raises:
|
|
||||||
# error: Signature of "__call__" incompatible with supertype "BaseFilter" [override]
|
|
||||||
# https://mypy.readthedocs.io/en/latest/error_code_list.html#check-validity-of-overrides-override
|
|
||||||
__call__: Callable[..., Awaitable[Union[bool, Dict[str, Any]]]]
|
|
||||||
else: # pragma: no cover
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
This method should be overridden.
|
|
||||||
|
|
||||||
Accepts incoming event and should return boolean or dict.
|
|
||||||
|
|
||||||
:return: :class:`bool` or :class:`Dict[str, Any]`
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __invert__(self) -> "_InvertFilter":
|
|
||||||
from aiogram.filters.logic import invert_f
|
|
||||||
|
|
||||||
return invert_f(self)
|
|
||||||
|
|
||||||
def update_handler_flags(self, flags: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Also if you want to extend handler flags with using this filter
|
|
||||||
you should implement this method
|
|
||||||
|
|
||||||
:param flags: existing flags, can be updated directly
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _signature_to_string(self, *args: Any, **kwargs: Any) -> str:
|
|
||||||
items = [repr(arg) for arg in args]
|
|
||||||
items.extend([f"{k}={v!r}" for k, v in kwargs.items() if v is not None])
|
|
||||||
|
|
||||||
return f"{type(self).__name__}({', '.join(items)})"
|
|
||||||
|
|
||||||
def __await__(self): # type: ignore # pragma: no cover
|
|
||||||
# Is needed only for inspection and this method is never be called
|
|
||||||
return self.__call__
|
|
@@ -1,208 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import types
|
|
||||||
import typing
|
|
||||||
from decimal import Decimal
|
|
||||||
from enum import Enum
|
|
||||||
from fractions import Fraction
|
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
ClassVar,
|
|
||||||
Dict,
|
|
||||||
Literal,
|
|
||||||
Optional,
|
|
||||||
Type,
|
|
||||||
TypeVar,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from magic_filter import MagicFilter
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from pydantic.fields import FieldInfo
|
|
||||||
from pydantic_core import PydanticUndefined
|
|
||||||
|
|
||||||
from aiogram.filters.base import Filter
|
|
||||||
from aiogram.types import CallbackQuery
|
|
||||||
|
|
||||||
T = TypeVar("T", bound="CallbackData")
|
|
||||||
|
|
||||||
MAX_CALLBACK_LENGTH: int = 64
|
|
||||||
|
|
||||||
|
|
||||||
_UNION_TYPES = {typing.Union}
|
|
||||||
if sys.version_info >= (3, 10): # pragma: no cover
|
|
||||||
_UNION_TYPES.add(types.UnionType)
|
|
||||||
|
|
||||||
|
|
||||||
class CallbackDataException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CallbackData(BaseModel):
|
|
||||||
"""
|
|
||||||
Base class for callback data wrapper
|
|
||||||
|
|
||||||
This class should be used as super-class of user-defined callbacks.
|
|
||||||
|
|
||||||
The class-keyword :code:`prefix` is required to define prefix
|
|
||||||
and also the argument :code:`sep` can be passed to define separator (default is :code:`:`).
|
|
||||||
"""
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
__separator__: ClassVar[str]
|
|
||||||
"""Data separator (default is :code:`:`)"""
|
|
||||||
__prefix__: ClassVar[str]
|
|
||||||
"""Callback prefix"""
|
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
||||||
if "prefix" not in kwargs:
|
|
||||||
raise ValueError(
|
|
||||||
f"prefix required, usage example: "
|
|
||||||
f"`class {cls.__name__}(CallbackData, prefix='my_callback'): ...`"
|
|
||||||
)
|
|
||||||
cls.__separator__ = kwargs.pop("sep", ":")
|
|
||||||
cls.__prefix__ = kwargs.pop("prefix")
|
|
||||||
if cls.__separator__ in cls.__prefix__:
|
|
||||||
raise ValueError(
|
|
||||||
f"Separator symbol {cls.__separator__!r} can not be used "
|
|
||||||
f"inside prefix {cls.__prefix__!r}"
|
|
||||||
)
|
|
||||||
super().__init_subclass__(**kwargs)
|
|
||||||
|
|
||||||
def _encode_value(self, key: str, value: Any) -> str:
|
|
||||||
if value is None:
|
|
||||||
return ""
|
|
||||||
if isinstance(value, Enum):
|
|
||||||
return str(value.value)
|
|
||||||
if isinstance(value, UUID):
|
|
||||||
return value.hex
|
|
||||||
if isinstance(value, bool):
|
|
||||||
return str(int(value))
|
|
||||||
if isinstance(value, (int, str, float, Decimal, Fraction)):
|
|
||||||
return str(value)
|
|
||||||
raise ValueError(
|
|
||||||
f"Attribute {key}={value!r} of type {type(value).__name__!r}"
|
|
||||||
f" can not be packed to callback data"
|
|
||||||
)
|
|
||||||
|
|
||||||
def pack(self) -> str:
|
|
||||||
"""
|
|
||||||
Generate callback data string
|
|
||||||
|
|
||||||
:return: valid callback data for Telegram Bot API
|
|
||||||
"""
|
|
||||||
result = [self.__prefix__]
|
|
||||||
for key, value in self.model_dump(mode="python").items():
|
|
||||||
encoded = self._encode_value(key, value)
|
|
||||||
if self.__separator__ in encoded:
|
|
||||||
raise ValueError(
|
|
||||||
f"Separator symbol {self.__separator__!r} can not be used "
|
|
||||||
f"in value {key}={encoded!r}"
|
|
||||||
)
|
|
||||||
result.append(encoded)
|
|
||||||
callback_data = self.__separator__.join(result)
|
|
||||||
if len(callback_data.encode()) > MAX_CALLBACK_LENGTH:
|
|
||||||
raise ValueError(
|
|
||||||
f"Resulted callback data is too long! "
|
|
||||||
f"len({callback_data!r}.encode()) > {MAX_CALLBACK_LENGTH}"
|
|
||||||
)
|
|
||||||
return callback_data
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def unpack(cls: Type[T], value: str) -> T:
|
|
||||||
"""
|
|
||||||
Parse callback data string
|
|
||||||
|
|
||||||
:param value: value from Telegram
|
|
||||||
:return: instance of CallbackData
|
|
||||||
"""
|
|
||||||
prefix, *parts = value.split(cls.__separator__)
|
|
||||||
names = cls.model_fields.keys()
|
|
||||||
if len(parts) != len(names):
|
|
||||||
raise TypeError(
|
|
||||||
f"Callback data {cls.__name__!r} takes {len(names)} arguments "
|
|
||||||
f"but {len(parts)} were given"
|
|
||||||
)
|
|
||||||
if prefix != cls.__prefix__:
|
|
||||||
raise ValueError(f"Bad prefix ({prefix!r} != {cls.__prefix__!r})")
|
|
||||||
payload = {}
|
|
||||||
for k, v in zip(names, parts): # type: str, Optional[str]
|
|
||||||
if field := cls.model_fields.get(k):
|
|
||||||
if v == "" and _check_field_is_nullable(field) and field.default != "":
|
|
||||||
v = field.default if field.default is not PydanticUndefined else None
|
|
||||||
payload[k] = v
|
|
||||||
return cls(**payload)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def filter(cls, rule: Optional[MagicFilter] = None) -> CallbackQueryFilter:
|
|
||||||
"""
|
|
||||||
Generates a filter for callback query with rule
|
|
||||||
|
|
||||||
:param rule: magic rule
|
|
||||||
:return: instance of filter
|
|
||||||
"""
|
|
||||||
return CallbackQueryFilter(callback_data=cls, rule=rule)
|
|
||||||
|
|
||||||
|
|
||||||
class CallbackQueryFilter(Filter):
|
|
||||||
"""
|
|
||||||
This filter helps to handle callback query.
|
|
||||||
|
|
||||||
Should not be used directly, you should create the instance of this filter
|
|
||||||
via callback data instance
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"callback_data",
|
|
||||||
"rule",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
callback_data: Type[CallbackData],
|
|
||||||
rule: Optional[MagicFilter] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:param callback_data: Expected type of callback data
|
|
||||||
:param rule: Magic rule
|
|
||||||
"""
|
|
||||||
self.callback_data = callback_data
|
|
||||||
self.rule = rule
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self._signature_to_string(
|
|
||||||
callback_data=self.callback_data,
|
|
||||||
rule=self.rule,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def __call__(self, query: CallbackQuery) -> Union[Literal[False], Dict[str, Any]]:
|
|
||||||
if not isinstance(query, CallbackQuery) or not query.data:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
callback_data = self.callback_data.unpack(query.data)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.rule is None or self.rule.resolve(callback_data):
|
|
||||||
return {"callback_data": callback_data}
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _check_field_is_nullable(field: FieldInfo) -> bool:
|
|
||||||
"""
|
|
||||||
Check if the given field is nullable.
|
|
||||||
|
|
||||||
:param field: The FieldInfo object representing the field to check.
|
|
||||||
:return: True if the field is nullable, False otherwise.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not field.is_required():
|
|
||||||
return True
|
|
||||||
|
|
||||||
return typing.get_origin(field.annotation) in _UNION_TYPES and type(None) in typing.get_args(
|
|
||||||
field.annotation
|
|
||||||
)
|
|
@@ -1,204 +0,0 @@
|
|||||||
from typing import Any, Dict, Optional, TypeVar, Union
|
|
||||||
|
|
||||||
from aiogram.filters.base import Filter
|
|
||||||
from aiogram.types import ChatMember, ChatMemberUpdated
|
|
||||||
|
|
||||||
MarkerT = TypeVar("MarkerT", bound="_MemberStatusMarker")
|
|
||||||
MarkerGroupT = TypeVar("MarkerGroupT", bound="_MemberStatusGroupMarker")
|
|
||||||
TransitionT = TypeVar("TransitionT", bound="_MemberStatusTransition")
|
|
||||||
|
|
||||||
|
|
||||||
class _MemberStatusMarker:
|
|
||||||
__slots__ = (
|
|
||||||
"name",
|
|
||||||
"is_member",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, name: str, *, is_member: Optional[bool] = None) -> None:
|
|
||||||
self.name = name
|
|
||||||
self.is_member = is_member
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
result = self.name.upper()
|
|
||||||
if self.is_member is not None:
|
|
||||||
result = ("+" if self.is_member else "-") + result
|
|
||||||
return result # noqa: RET504
|
|
||||||
|
|
||||||
def __pos__(self: MarkerT) -> MarkerT:
|
|
||||||
return type(self)(name=self.name, is_member=True)
|
|
||||||
|
|
||||||
def __neg__(self: MarkerT) -> MarkerT:
|
|
||||||
return type(self)(name=self.name, is_member=False)
|
|
||||||
|
|
||||||
def __or__(
|
|
||||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
|
||||||
) -> "_MemberStatusGroupMarker":
|
|
||||||
if isinstance(other, _MemberStatusMarker):
|
|
||||||
return _MemberStatusGroupMarker(self, other)
|
|
||||||
if isinstance(other, _MemberStatusGroupMarker):
|
|
||||||
return other | self
|
|
||||||
raise TypeError(
|
|
||||||
f"unsupported operand type(s) for |: "
|
|
||||||
f"{type(self).__name__!r} and {type(other).__name__!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
__ror__ = __or__
|
|
||||||
|
|
||||||
def __rshift__(
|
|
||||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
|
||||||
) -> "_MemberStatusTransition":
|
|
||||||
old = _MemberStatusGroupMarker(self)
|
|
||||||
if isinstance(other, _MemberStatusMarker):
|
|
||||||
return _MemberStatusTransition(old=old, new=_MemberStatusGroupMarker(other))
|
|
||||||
if isinstance(other, _MemberStatusGroupMarker):
|
|
||||||
return _MemberStatusTransition(old=old, new=other)
|
|
||||||
raise TypeError(
|
|
||||||
f"unsupported operand type(s) for >>: "
|
|
||||||
f"{type(self).__name__!r} and {type(other).__name__!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __lshift__(
|
|
||||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
|
||||||
) -> "_MemberStatusTransition":
|
|
||||||
new = _MemberStatusGroupMarker(self)
|
|
||||||
if isinstance(other, _MemberStatusMarker):
|
|
||||||
return _MemberStatusTransition(old=_MemberStatusGroupMarker(other), new=new)
|
|
||||||
if isinstance(other, _MemberStatusGroupMarker):
|
|
||||||
return _MemberStatusTransition(old=other, new=new)
|
|
||||||
raise TypeError(
|
|
||||||
f"unsupported operand type(s) for <<: "
|
|
||||||
f"{type(self).__name__!r} and {type(other).__name__!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash((self.name, self.is_member))
|
|
||||||
|
|
||||||
def check(self, *, member: ChatMember) -> bool:
|
|
||||||
# Not all member types have `is_member` attribute
|
|
||||||
is_member = getattr(member, "is_member", None)
|
|
||||||
status = getattr(member, "status", None)
|
|
||||||
if self.is_member is not None and is_member != self.is_member:
|
|
||||||
return False
|
|
||||||
return self.name == status
|
|
||||||
|
|
||||||
|
|
||||||
class _MemberStatusGroupMarker:
|
|
||||||
__slots__ = ("statuses",)
|
|
||||||
|
|
||||||
def __init__(self, *statuses: _MemberStatusMarker) -> None:
|
|
||||||
if not statuses:
|
|
||||||
raise ValueError("Member status group should have at least one status included")
|
|
||||||
self.statuses = frozenset(statuses)
|
|
||||||
|
|
||||||
def __or__(
|
|
||||||
self: MarkerGroupT, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
|
||||||
) -> MarkerGroupT:
|
|
||||||
if isinstance(other, _MemberStatusMarker):
|
|
||||||
return type(self)(*self.statuses, other)
|
|
||||||
if isinstance(other, _MemberStatusGroupMarker):
|
|
||||||
return type(self)(*self.statuses, *other.statuses)
|
|
||||||
raise TypeError(
|
|
||||||
f"unsupported operand type(s) for |: "
|
|
||||||
f"{type(self).__name__!r} and {type(other).__name__!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __rshift__(
|
|
||||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
|
||||||
) -> "_MemberStatusTransition":
|
|
||||||
if isinstance(other, _MemberStatusMarker):
|
|
||||||
return _MemberStatusTransition(old=self, new=_MemberStatusGroupMarker(other))
|
|
||||||
if isinstance(other, _MemberStatusGroupMarker):
|
|
||||||
return _MemberStatusTransition(old=self, new=other)
|
|
||||||
raise TypeError(
|
|
||||||
f"unsupported operand type(s) for >>: "
|
|
||||||
f"{type(self).__name__!r} and {type(other).__name__!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __lshift__(
|
|
||||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
|
||||||
) -> "_MemberStatusTransition":
|
|
||||||
if isinstance(other, _MemberStatusMarker):
|
|
||||||
return _MemberStatusTransition(old=_MemberStatusGroupMarker(other), new=self)
|
|
||||||
if isinstance(other, _MemberStatusGroupMarker):
|
|
||||||
return _MemberStatusTransition(old=other, new=self)
|
|
||||||
raise TypeError(
|
|
||||||
f"unsupported operand type(s) for <<: "
|
|
||||||
f"{type(self).__name__!r} and {type(other).__name__!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
result = " | ".join(map(str, sorted(self.statuses, key=str)))
|
|
||||||
if len(self.statuses) != 1:
|
|
||||||
return f"({result})"
|
|
||||||
return result
|
|
||||||
|
|
||||||
def check(self, *, member: ChatMember) -> bool:
|
|
||||||
return any(status.check(member=member) for status in self.statuses)
|
|
||||||
|
|
||||||
|
|
||||||
class _MemberStatusTransition:
|
|
||||||
__slots__ = (
|
|
||||||
"old",
|
|
||||||
"new",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *, old: _MemberStatusGroupMarker, new: _MemberStatusGroupMarker) -> None:
|
|
||||||
self.old = old
|
|
||||||
self.new = new
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.old} >> {self.new}"
|
|
||||||
|
|
||||||
def __invert__(self: TransitionT) -> TransitionT:
|
|
||||||
return type(self)(old=self.new, new=self.old)
|
|
||||||
|
|
||||||
def check(self, *, old: ChatMember, new: ChatMember) -> bool:
|
|
||||||
return self.old.check(member=old) and self.new.check(member=new)
|
|
||||||
|
|
||||||
|
|
||||||
CREATOR = _MemberStatusMarker("creator")
|
|
||||||
ADMINISTRATOR = _MemberStatusMarker("administrator")
|
|
||||||
MEMBER = _MemberStatusMarker("member")
|
|
||||||
RESTRICTED = _MemberStatusMarker("restricted")
|
|
||||||
LEFT = _MemberStatusMarker("left")
|
|
||||||
KICKED = _MemberStatusMarker("kicked")
|
|
||||||
|
|
||||||
IS_MEMBER = CREATOR | ADMINISTRATOR | MEMBER | +RESTRICTED
|
|
||||||
IS_ADMIN = CREATOR | ADMINISTRATOR
|
|
||||||
IS_NOT_MEMBER = LEFT | KICKED | -RESTRICTED
|
|
||||||
|
|
||||||
JOIN_TRANSITION = IS_NOT_MEMBER >> IS_MEMBER
|
|
||||||
LEAVE_TRANSITION = ~JOIN_TRANSITION
|
|
||||||
PROMOTED_TRANSITION = (MEMBER | RESTRICTED | LEFT | KICKED) >> ADMINISTRATOR
|
|
||||||
|
|
||||||
|
|
||||||
class ChatMemberUpdatedFilter(Filter):
|
|
||||||
__slots__ = ("member_status_changed",)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
member_status_changed: Union[
|
|
||||||
_MemberStatusMarker,
|
|
||||||
_MemberStatusGroupMarker,
|
|
||||||
_MemberStatusTransition,
|
|
||||||
],
|
|
||||||
):
|
|
||||||
self.member_status_changed = member_status_changed
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self._signature_to_string(
|
|
||||||
member_status_changed=self.member_status_changed,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def __call__(self, member_updated: ChatMemberUpdated) -> Union[bool, Dict[str, Any]]:
|
|
||||||
old = member_updated.old_chat_member
|
|
||||||
new = member_updated.new_chat_member
|
|
||||||
rule = self.member_status_changed
|
|
||||||
|
|
||||||
if isinstance(rule, (_MemberStatusMarker, _MemberStatusGroupMarker)):
|
|
||||||
return rule.check(member=new)
|
|
||||||
if isinstance(rule, _MemberStatusTransition):
|
|
||||||
return rule.check(old=old, new=new)
|
|
||||||
|
|
||||||
# Impossible variant in due to pydantic validation
|
|
||||||
return False # pragma: no cover
|
|
@@ -1,298 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from dataclasses import dataclass, field, replace
|
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
Match,
|
|
||||||
Optional,
|
|
||||||
Pattern,
|
|
||||||
Sequence,
|
|
||||||
Union,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
from magic_filter import MagicFilter
|
|
||||||
|
|
||||||
from aiogram.filters.base import Filter
|
|
||||||
from aiogram.types import BotCommand, Message
|
|
||||||
from aiogram.utils.deep_linking import decode_payload
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from aiogram import Bot
|
|
||||||
|
|
||||||
# TODO: rm type ignore after py3.8 support expiration or mypy bug fix
|
|
||||||
CommandPatternType = Union[str, re.Pattern, BotCommand] # type: ignore[type-arg]
|
|
||||||
|
|
||||||
|
|
||||||
class CommandException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Command(Filter):
|
|
||||||
"""
|
|
||||||
This filter can be helpful for handling commands from the text messages.
|
|
||||||
|
|
||||||
Works only with :class:`aiogram.types.message.Message` events which have the :code:`text`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"commands",
|
|
||||||
"prefix",
|
|
||||||
"ignore_case",
|
|
||||||
"ignore_mention",
|
|
||||||
"magic",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*values: CommandPatternType,
|
|
||||||
commands: Optional[Union[Sequence[CommandPatternType], CommandPatternType]] = None,
|
|
||||||
prefix: str = "/",
|
|
||||||
ignore_case: bool = False,
|
|
||||||
ignore_mention: bool = False,
|
|
||||||
magic: Optional[MagicFilter] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
List of commands (string or compiled regexp patterns)
|
|
||||||
|
|
||||||
:param prefix: Prefix for command.
|
|
||||||
Prefix is always a single char but here you can pass all of allowed prefixes,
|
|
||||||
for example: :code:`"/!"` will work with commands prefixed
|
|
||||||
by :code:`"/"` or :code:`"!"`.
|
|
||||||
:param ignore_case: Ignore case (Does not work with regexp, use flags instead)
|
|
||||||
:param ignore_mention: Ignore bot mention. By default,
|
|
||||||
bot can not handle commands intended for other bots
|
|
||||||
:param magic: Validate command object via Magic filter after all checks done
|
|
||||||
"""
|
|
||||||
if commands is None:
|
|
||||||
commands = []
|
|
||||||
if isinstance(commands, (str, re.Pattern, BotCommand)):
|
|
||||||
commands = [commands]
|
|
||||||
|
|
||||||
if not isinstance(commands, Iterable):
|
|
||||||
raise ValueError(
|
|
||||||
"Command filter only supports str, re.Pattern, BotCommand object"
|
|
||||||
" or their Iterable"
|
|
||||||
)
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for command in (*values, *commands):
|
|
||||||
if isinstance(command, BotCommand):
|
|
||||||
command = command.command
|
|
||||||
if not isinstance(command, (str, re.Pattern)):
|
|
||||||
raise ValueError(
|
|
||||||
"Command filter only supports str, re.Pattern, BotCommand object"
|
|
||||||
" or their Iterable"
|
|
||||||
)
|
|
||||||
if ignore_case and isinstance(command, str):
|
|
||||||
command = command.casefold()
|
|
||||||
items.append(command)
|
|
||||||
|
|
||||||
if not items:
|
|
||||||
raise ValueError("At least one command should be specified")
|
|
||||||
|
|
||||||
self.commands = tuple(items)
|
|
||||||
self.prefix = prefix
|
|
||||||
self.ignore_case = ignore_case
|
|
||||||
self.ignore_mention = ignore_mention
|
|
||||||
self.magic = magic
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self._signature_to_string(
|
|
||||||
*self.commands,
|
|
||||||
prefix=self.prefix,
|
|
||||||
ignore_case=self.ignore_case,
|
|
||||||
ignore_mention=self.ignore_mention,
|
|
||||||
magic=self.magic,
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_handler_flags(self, flags: Dict[str, Any]) -> None:
|
|
||||||
commands = flags.setdefault("commands", [])
|
|
||||||
commands.append(self)
|
|
||||||
|
|
||||||
async def __call__(self, message: Message, bot: Bot) -> Union[bool, Dict[str, Any]]:
|
|
||||||
if not isinstance(message, Message):
|
|
||||||
return False
|
|
||||||
|
|
||||||
text = message.text or message.caption
|
|
||||||
if not text:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
command = await self.parse_command(text=text, bot=bot)
|
|
||||||
except CommandException:
|
|
||||||
return False
|
|
||||||
result = {"command": command}
|
|
||||||
if command.magic_result and isinstance(command.magic_result, dict):
|
|
||||||
result.update(command.magic_result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def extract_command(self, text: str) -> CommandObject:
|
|
||||||
# First step: separate command with arguments
|
|
||||||
# "/command@mention arg1 arg2" -> "/command@mention", ["arg1 arg2"]
|
|
||||||
try:
|
|
||||||
full_command, *args = text.split(maxsplit=1)
|
|
||||||
except ValueError:
|
|
||||||
raise CommandException("not enough values to unpack")
|
|
||||||
|
|
||||||
# Separate command into valuable parts
|
|
||||||
# "/command@mention" -> "/", ("command", "@", "mention")
|
|
||||||
prefix, (command, _, mention) = full_command[0], full_command[1:].partition("@")
|
|
||||||
return CommandObject(
|
|
||||||
prefix=prefix,
|
|
||||||
command=command,
|
|
||||||
mention=mention or None,
|
|
||||||
args=args[0] if args else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_prefix(self, command: CommandObject) -> None:
|
|
||||||
if command.prefix not in self.prefix:
|
|
||||||
raise CommandException("Invalid command prefix")
|
|
||||||
|
|
||||||
async def validate_mention(self, bot: Bot, command: CommandObject) -> None:
|
|
||||||
if command.mention and not self.ignore_mention:
|
|
||||||
me = await bot.me()
|
|
||||||
if me.username and command.mention.lower() != me.username.lower():
|
|
||||||
raise CommandException("Mention did not match")
|
|
||||||
|
|
||||||
def validate_command(self, command: CommandObject) -> CommandObject:
|
|
||||||
for allowed_command in cast(Sequence[CommandPatternType], self.commands):
|
|
||||||
# Command can be presented as regexp pattern or raw string
|
|
||||||
# then need to validate that in different ways
|
|
||||||
if isinstance(allowed_command, Pattern): # Regexp
|
|
||||||
result = allowed_command.match(command.command)
|
|
||||||
if result:
|
|
||||||
return replace(command, regexp_match=result)
|
|
||||||
|
|
||||||
command_name = command.command
|
|
||||||
if self.ignore_case:
|
|
||||||
command_name = command_name.casefold()
|
|
||||||
|
|
||||||
if command_name == allowed_command: # String
|
|
||||||
return command
|
|
||||||
raise CommandException("Command did not match pattern")
|
|
||||||
|
|
||||||
async def parse_command(self, text: str, bot: Bot) -> CommandObject:
|
|
||||||
"""
|
|
||||||
Extract command from the text and validate
|
|
||||||
|
|
||||||
:param text:
|
|
||||||
:param bot:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
command = self.extract_command(text)
|
|
||||||
self.validate_prefix(command=command)
|
|
||||||
await self.validate_mention(bot=bot, command=command)
|
|
||||||
command = self.validate_command(command)
|
|
||||||
command = self.do_magic(command=command)
|
|
||||||
return command # noqa: RET504
|
|
||||||
|
|
||||||
def do_magic(self, command: CommandObject) -> Any:
|
|
||||||
if self.magic is None:
|
|
||||||
return command
|
|
||||||
result = self.magic.resolve(command)
|
|
||||||
if not result:
|
|
||||||
raise CommandException("Rejected via magic filter")
|
|
||||||
return replace(command, magic_result=result)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CommandObject:
|
|
||||||
"""
|
|
||||||
Instance of this object is always has command and it prefix.
|
|
||||||
Can be passed as keyword argument **command** to the handler
|
|
||||||
"""
|
|
||||||
|
|
||||||
prefix: str = "/"
|
|
||||||
"""Command prefix"""
|
|
||||||
command: str = ""
|
|
||||||
"""Command without prefix and mention"""
|
|
||||||
mention: Optional[str] = None
|
|
||||||
"""Mention (if available)"""
|
|
||||||
args: Optional[str] = field(repr=False, default=None)
|
|
||||||
"""Command argument"""
|
|
||||||
regexp_match: Optional[Match[str]] = field(repr=False, default=None)
|
|
||||||
"""Will be presented match result if the command is presented as regexp in filter"""
|
|
||||||
magic_result: Optional[Any] = field(repr=False, default=None)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mentioned(self) -> bool:
|
|
||||||
"""
|
|
||||||
This command has mention?
|
|
||||||
"""
|
|
||||||
return bool(self.mention)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text(self) -> str:
|
|
||||||
"""
|
|
||||||
Generate original text from object
|
|
||||||
"""
|
|
||||||
line = self.prefix + self.command
|
|
||||||
if self.mention:
|
|
||||||
line += "@" + self.mention
|
|
||||||
if self.args:
|
|
||||||
line += " " + self.args
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
class CommandStart(Command):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
deep_link: bool = False,
|
|
||||||
deep_link_encoded: bool = False,
|
|
||||||
ignore_case: bool = False,
|
|
||||||
ignore_mention: bool = False,
|
|
||||||
magic: Optional[MagicFilter] = None,
|
|
||||||
):
|
|
||||||
super().__init__(
|
|
||||||
"start",
|
|
||||||
prefix="/",
|
|
||||||
ignore_case=ignore_case,
|
|
||||||
ignore_mention=ignore_mention,
|
|
||||||
magic=magic,
|
|
||||||
)
|
|
||||||
self.deep_link = deep_link
|
|
||||||
self.deep_link_encoded = deep_link_encoded
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self._signature_to_string(
|
|
||||||
ignore_case=self.ignore_case,
|
|
||||||
ignore_mention=self.ignore_mention,
|
|
||||||
magic=self.magic,
|
|
||||||
deep_link=self.deep_link,
|
|
||||||
deep_link_encoded=self.deep_link_encoded,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def parse_command(self, text: str, bot: Bot) -> CommandObject:
|
|
||||||
"""
|
|
||||||
Extract command from the text and validate
|
|
||||||
|
|
||||||
:param text:
|
|
||||||
:param bot:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
command = self.extract_command(text)
|
|
||||||
self.validate_prefix(command=command)
|
|
||||||
await self.validate_mention(bot=bot, command=command)
|
|
||||||
command = self.validate_command(command)
|
|
||||||
command = self.validate_deeplink(command=command)
|
|
||||||
command = self.do_magic(command=command)
|
|
||||||
return command # noqa: RET504
|
|
||||||
|
|
||||||
def validate_deeplink(self, command: CommandObject) -> CommandObject:
|
|
||||||
if not self.deep_link:
|
|
||||||
return command
|
|
||||||
if not command.args:
|
|
||||||
raise CommandException("Deep-link was missing")
|
|
||||||
args = command.args
|
|
||||||
if self.deep_link_encoded:
|
|
||||||
try:
|
|
||||||
args = decode_payload(args)
|
|
||||||
except UnicodeDecodeError as e:
|
|
||||||
raise CommandException(f"Failed to decode Base64: {e}")
|
|
||||||
return replace(command, args=args)
|
|
||||||
return command
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user