Utilities for streamlined aiogram bot development.
Project description
gram_tools
Utilities for streamlined aiogram 3 bot development.
Features
- 📦 Message Packing: Serialize and deserialize Telegram messages with support for all media types
- 🗄️ Asset Management: Efficient handling of media files with caching and file_id reuse
- 📚 CRUD Operations: Generic async CRUD operations for SQLAlchemy models
- ⌨️ Pagination: Flexible inline keyboard pagination with search capability
- 🔧 Template System: Simple template system for message text formatting
Installation
pip install gram-tools
Components
Message Packer
Pack and unpack Telegram messages with all their content for easy storage and reuse.
from gram_tools.packer import pack_message, unpack_message, send_packed_message, answer_packed_message
# Pack a message
packed = await pack_message(message)
# Send packed message
await send_packed_message(bot, chat_id, packed)
# Answer with packed message
await answer_packed_message(message, packed)
Whole example
import asyncio
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import Message
from aiogram.filters import Command
from gram_tools.packer import (
pack_message,
send_packed_message,
)
API_TOKEN = "BOT TOKEN"
bot = Bot(token=API_TOKEN)
dp = Dispatcher()
router = Router()
# In-memory storage for packed messages
packed_messages = {}
@router.message(Command("save"))
async def save_message(message: Message):
if not message.reply_to_message:
await message.answer("Please reply to the message you want to save.")
return
# Pack the replied message
packed = await pack_message(message.reply_to_message)
# Store the packed message using user's chat ID
packed_messages[message.from_user.id] = packed
await message.answer("Message saved!")
@router.message(Command("send"))
async def send_saved_message(message: Message):
packed = packed_messages.get(message.from_user.id)
if not packed:
await message.answer("No message saved. Use /save to save a message.")
return
# Send the packed message back to the user
await send_packed_message(bot, message.chat.id, packed)
async def main():
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Asset Management
Efficiently handle media files with automatic file_id caching.
from gram_tools.assets import answer_with_asset, send_with_asset
# Send media file
await send_with_asset(bot, chat_id, "path/to/file.jpg", caption="My photo") # also u can without caption, and also you can use reply_markup and more
# Answer with media file
await answer_with_asset(message, "path/to/file.mp4")
Whole example how to use
import asyncio
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import Message
from aiogram.filters import Command
from gram_tools.assets import send_with_asset, answer_with_asset
API_TOKEN = "BOT TOKEN"
bot = Bot(token=API_TOKEN)
dp = Dispatcher()
router = Router()
@router.message(Command("photo"))
async def send_photo(message: Message):
await send_with_asset(
bot,
chat_id=message.chat.id,
file_path=r"D:\Dokumenti\Pictures\photo_2024-08-11_18-25-01.jpg",
caption="Here is a photo!"
)
@router.message(Command("video"))
async def send_video(message: Message):
await answer_with_asset(
message=message,
file_path=r"D:\Dokumenti\Documents\Untitled.mp4",
caption="Here is a video!"
)
async def main():
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Supported media types:
- Photos: jpg, jpeg, png, svg, webp, bmp, jfif, heic, heif
- Videos: mp4, mov, avi, mkv, m4v, 3gp
- Audio: mp3, wav, flac, m4a, ogg, aac
- Voice: ogg, oga
- Animations: gif
- Any other file type as document
CRUD Operations
Generic CRUD operations for SQLAlchemy models.
from gram_tools.crud import get_crud
from sqlalchemy import Column, Integer, String
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
telegram_id = Column(Integer, unique=True, index=True)
name = Column(String)
user_crud = get_crud(User)
# Add new instance
await user_crud.add(session, user)
# Get instance
user = await user_crud.get(session, id=1)
# Get all instances
active_users = await user_crud.get_all(session, status="active")
# Get count of instances
active_users_count = await user_crud.get_all_count(session, status="active")
# Update instance
await user_crud.update(session, user, name="New Name")
# Delete instance
await user_crud.delete(session, user)
Pagination Keyboards
Create paginated inline keyboards with optional search functionality.
from gram_tools.keyboards import InlineBuilder, SearchButton
# Create pagination with search
builder = InlineBuilder(
per_page=5,
layout=1,
search_button=SearchButton("🔍")
)
items = ["Item 1", "Item 2", "Item 3", ...]
keyboard = builder.get_paginated(items, page=1)
# With search
keyboard = builder.get_paginated(items, page=1, search_term="search query")
Customization options:
per_page
: Items per pagelayout
: Number of buttons per rownext_button_text
: Next page button textprev_button_text
: Previous page button textpage_callback_prefix
: Optional callback prefix of core paginator and item selectionignore_callback_prefix
: Optional callback prefox on click ignore itemsnot_exist_page
: Text for disabled navigation buttonssearch_button
: Optional search button configuration
Whole example:
import asyncio
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from gram_tools.keyboards import (
InlineBuilder,
SearchButton
)
API_TOKEN = "7350010616:AAHQXoWzmMuffBkv5mPAI2-cBziZBkvoTI8"
bot = Bot(token=API_TOKEN)
dp = Dispatcher()
router = Router()
items = [f"Item {i}" for i in range(1, 21)]
search_button = SearchButton(button_text="🔍 Search")
inline_builder = InlineBuilder(per_page=5, search_button=search_button)
class Form(StatesGroup):
waiting_for_search_term = State()
@router.message(Command("start"))
async def show_inline_keyboard(message: Message, state: FSMContext):
await state.clear()
keyboard = inline_builder.get_paginated(items, page=1)
await message.answer("Select an item:", reply_markup=keyboard)
@router.callback_query(inline_builder.page_callback.filter())
async def handle_callback(callback: CallbackQuery, callback_data: inline_builder.page_callback, state: FSMContext):
action = callback_data.action
value = callback_data.value
data = await state.get_data()
search_term = data.get('search_term')
if action in ("next", "prev"):
page = value
keyboard = inline_builder.get_paginated(items, page=page, search_term=search_term)
await callback.message.edit_reply_markup(reply_markup=keyboard)
await callback.answer()
elif action == "sel":
item_index = value
if search_term:
filtered_items = [item for item in items if search_term.lower() in str(item).lower()]
else:
filtered_items = items
if 0 <= item_index < len(filtered_items):
selected_item = filtered_items[item_index]
await callback.message.answer(f"You selected: {selected_item}")
else:
await callback.message.answer("Element not found.")
await callback.answer()
@router.callback_query(search_button.search_callback.filter())
async def handle_search_callback(callback: CallbackQuery, state: FSMContext):
print(callback.data)
await state.update_data(search_term=None)
await callback.message.answer("Type text to search:")
await state.set_state(Form.waiting_for_search_term)
await callback.answer()
@router.message(Form.waiting_for_search_term)
async def perform_search(message: Message, state: FSMContext):
search_term = message.text
await state.update_data(search_term=search_term)
await state.set_state(None)
keyboard = inline_builder.get_paginated(items, page=1, search_term=search_term)
if keyboard.inline_keyboard:
await message.answer("Results:", reply_markup=keyboard)
else:
await message.answer("Nothing found.")
@router.callback_query(inline_builder.ignore_callback.filter())
async def handle_ignore_callback(callback: CallbackQuery):
await callback.answer()
async def main():
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Template System
Simple template system for message text formatting.
from gram_tools.messages import T
template = T("Hello, ${name}!")
text = template.text(name="User")
Whole example
import asyncio
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import Message
from aiogram.filters import Command
from gram_tools.messages import T
API_TOKEN = "BOT TOKEN"
bot = Bot(token=API_TOKEN)
dp = Dispatcher()
router = Router()
# Define a template
welcome_template = T("Hello, ${name}! Welcome to our bot.")
@router.message(Command("start"))
async def send_welcome(message: Message):
# Render the template with the user's name
text = welcome_template.text(name=message.from_user.full_name)
await message.answer(text)
@router.message(Command("info"))
async def send_info(message: Message):
info_template = T(
"User Info:\n"
"- ID: ${id}\n"
"- Username: ${username}\n"
"- First Name: ${first_name}\n"
"- Last Name: ${last_name}"
)
text = info_template.text(
id=message.from_user.id,
username=message.from_user.username or "N/A",
first_name=message.from_user.first_name or "N/A",
last_name=message.from_user.last_name or "N/A"
)
await message.answer(text)
async def main():
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
gram_tools-1.0.0.tar.gz
(11.4 kB
view details)
Built Distribution
File details
Details for the file gram_tools-1.0.0.tar.gz
.
File metadata
- Download URL: gram_tools-1.0.0.tar.gz
- Upload date:
- Size: 11.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.1.1 CPython/3.10.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0715a30eb0e05768a2ab3cd277fb6868b883a64958231e8c98534e8cd313a34f |
|
MD5 | 4903c886e7209ece194b795c70043f7e |
|
BLAKE2b-256 | a7281a3ee380faf5c484d8136ef2c879ac56e19ae1d6c48fb0d552068b4e7de3 |
File details
Details for the file gram_tools-1.0.0-py3-none-any.whl
.
File metadata
- Download URL: gram_tools-1.0.0-py3-none-any.whl
- Upload date:
- Size: 9.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.1.1 CPython/3.10.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 561b9f65e1d8df0a0ed0a9662828e9df9efa5615a97f69083fdc2824b7462b59 |
|
MD5 | 656a6a368ba8c8ff8e7a81905b680885 |
|
BLAKE2b-256 | e5401a5e826be29cf1a187bda10e491d39b34bdc9621e6ac763d8a75931cb4ef |