timoniq / telegrinder Goto Github PK
View Code? Open in Web Editor NEWModern visionary telegram bot framework
License: MIT License
Modern visionary telegram bot framework
License: MIT License
It's cool that using the Token
class we can load tokens from environment variables, but if the environment variables are loaded elsewhere, then passing a simple string to the API
constructor without mypy
errors is impossible.
After the program exits warning messages appear: coroutine 'coro_name' was never awaited. The reason for this bug is the function 'to_coroutine_task'.
from telegrinder import LoopWrapper
async def create_tables() -> None:
...
lw = LoopWrapper()
lw.lifespan.on_startup(create_tables())
lw.run_event_loop()
RuntimeWarning: coroutine 'create_tables' was never awaited
.
Option monad is either variant Some contains a some value, variant Nothing which contains nothing.
Option = Some[Value] | Nothing
For example:
from telegrinder.option import Option, Nothing
from telegrinder.model import Model
class User(Model):
id: int
is_bot: bool
first_name: str
last_name: Option[str] = Nothing
def repr_user(user: User) -> str:
# returns first_name + last_name
return user.first_name + user.last_name.map(lambda value: " " + value).unwrap_or("")
class MessageView(ABCView[MessageCute]):
async def check(self, event: Update) -> bool:
return bool(event.message)
async def process(self, event: Update, api: ABCAPI):
# event.message.unwrap() is guaranteed to return variant Some after check method, which is good for logic and linter
msg = MessageCute(**event.message.unwrap().to_dict(), api=api)
return await process_inner(msg, event, self.middlewares, self.handlers)
fntypes.error.UnwrapError: [400] Bad Request: field "allow_sending_without_reply" must be of type Boolean
Reason: request receives set optional parameters with None (derived from Nothing) values
Maybe use msgspec.json module as a standard dependency because it will be faster than std json module?
@node
def/async def db_connection() -> Connection:
conn = db.connect()
yield conn
conn.close()
The problem of shared mutable state, which is not good.
For example, a user created his own rule:
from telegrinder import API, Telegrinder, Token, Message
from telegrinder.rules import MessageRule, Markup
api = API(Token.from_env())
bot = Telegrinder(api)
class WithReply(MessageRule):
async def check(self, message: Message, ctx: dict) -> bool:
ctx.clear() # accidental context deletion
return message.reply_to_message is not None
@bot.on.message(Markup(["/give <item> <count:int>", "/give <item>"]), WithReply())
async def give_item(message: Message, item: str, count: int = 1):
# an exception will be raised because the ctx was
# empty and there is nothing pass to the handler
...
✏️ Feature description
Compatible with the latest version of Telegram Bot API.
🪄 Provide a minimal example 🪄
No response
✨ Teachability, Documentation, Adoption ✨
No response
discussion is open
For example:
from telegrinder import InlineKeyboardEnum, KeyboardEnum, InlineButton, Button
class FoodKeyboard(InlineKeyboardEnum):
PRETZEL = InlineButton("pretzel", callback_data="food/pretzel")
PIZZA = InlineButton("pizza", callback_data="food/pizza")
PASTA = InlineButton("pasta", callback_data="food/pasta")
RICE = InlineButton("rice", callback_data="food/rice")
DUMPLINGS = InlineButton("dumplings", callback_data="food/dumplings")
class MenuKeyboard(KeyboardEnum):
START = Button("start")
EAT = Button("eat")
SEND_CONTACT = Button("send_contact", request_contact=True)
food_kb_markup = FoodKeyboard.get_markup()
menu_kb_markup = MenuKeyboard.get_markup()
@bot.on.callback_query(FoodKeyboard.PRETZEL)
...
@bot.on.message(MenuKeyboard.EAT)
...
example:
InlineButton("Dummy button", callback_data={"action": "buy", "quantity": 3})
example:
class MySet(RuleEnum):
CANCEL = Text("/cancel")
USERNAME = Mention() | Markup("t.me/<username>")
@dp.message(MySet())
issues/PRs
🔍Fails on any inline query
Any inline query code
msgspec.ValidationError: Expected object of type `InlineQuery`, got `dict`. - at `$[0].inline_query`
2024-08-21 11:24:00,488 - [ERROR] - root - (custom.py).listen(62) - Expected object of type `InlineQuery`, got `dict`. - at `$[0].inline_query`
Traceback (most recent call last):
File "/home/prostomarkeloff/projects/umi_game_bot/.venv/lib/python3.12/site-packages/telegrinder/msgspec_utils.py", line 210, in dec_hook
return self.dec_hooks[origin_type](tp, obj)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/prostomarkeloff/projects/umi_game_bot/.venv/lib/python3.12/site-packages/telegrinder/msgspec_utils.py", line 81, in option_dec_hook
return fntypes.option.Some(msgspec_convert(obj, value_type).unwrap())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/prostomarkeloff/projects/umi_game_bot/.venv/lib/python3.12/site-packages/fntypes/result/result.py", line 114, in unwrap
raise UnwrapError(self.error)
fntypes.error.UnwrapError: Expected object of type `InlineQuery`, got `dict`.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/prostomarkeloff/projects/umi_game_bot/bot/custom.py", line 33, in listen
updates_list: list[Update] = decoder.decode(updates, type=list[Update])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/prostomarkeloff/projects/umi_game_bot/.venv/lib/python3.12/site-packages/telegrinder/msgspec_utils.py", line 248, in decode
return msgspec.json.decode(
^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected object of type `InlineQuery`, got `dict`. - at `$[0].inline_query`
2024-08-21 11:24:00,627 - [ERROR] - root - (custom.py).listen(62) - Expected object of type `InlineQuery`, got `dict`. - at `$[0].inline_query`
Traceback (most recent call last):
File "/home/prostomarkeloff/projects/umi_game_bot/.venv/lib/python3.12/site-packages/telegrinder/msgspec_utils.py", line 210, in dec_hook
return self.dec_hooks[origin_type](tp, obj)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/prostomarkeloff/projects/umi_game_bot/.venv/lib/python3.12/site-packages/telegrinder/msgspec_utils.py", line 81, in option_dec_hook
return fntypes.option.Some(msgspec_convert(obj, value_type).unwrap())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/prostomarkeloff/projects/umi_game_bot/.venv/lib/python3.12/site-packages/fntypes/result/result.py", line 114, in unwrap
raise UnwrapError(self.error)
fntypes.error.UnwrapError: Expected object of type `InlineQuery`, got `dict`.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/prostomarkeloff/projects/umi_game_bot/bot/custom.py", line 33, in listen
updates_list: list[Update] = decoder.decode(updates, type=list[Update])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/prostomarkeloff/projects/umi_game_bot/.venv/lib/python3.12/site-packages/telegrinder/msgspec_utils.py", line 248, in decode
return msgspec.json.decode(
^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected object of type `InlineQuery`, got `dict`. - at `$[0].inline_query`
Now they are defined like this:
class Dispatch(ABCDispatch):
# ...
message = MessageView()
views = {"message"}
Implementation using dataclass:
import dataclasses
import typing_extensions as typing
from telegrinder.bot.dispatch.view import ABCView, MessageView
MessageViewT = typing.TypeVar("MessageViewT", bound=ABCView, default=MessageView)
@dataclasses.dataclass(kw_only=True)
class Views(typing.Generic[MessageViewT]):
message: MessageViewT = dataclasses.field(default_factory=lambda: MessageView())
class Dispatch(ABCDispatch, Views[MessageViewT]):
...
dp = Dispatch()
dp.message # default instance 'MessageView'
dp = Dispatch(message=CustomMessageView())
dp.message # custom instance 'CustomMessageView'
i had a problem building simple bot consumer with some extra data passed from webhook router
bot = Telegrinder(...)
await bot.dispatch.feed(update, api)
i need to pass extra context
or create a dependency system
feed(update, api, context={"shop_id": shop_id})
# or
bot.dependency["shop_id"] = Value(shop_id)
# ^ value wrapper is needed to separate values from async getters
Update: use compositions
It is necessary to fix the problem with processing the asyncio event loop and launching tasks.
telegrinder uses schema at https://github.com/ark0f/tg-bot-api to generate types. its super old and not getting updates :c
probably new schema must be applied and *unfortunately* most likely type generator will have to be revised for the found new schema specifications
In python3.10 the get_event_loop
function from asyncio is deprecated and should be replaced by get_running_loop
, but it should only be called inside asynchronous functions, so I recommend removing all .loop
attributes and using get_running_loop
instead, or new_event_loop
in case of run_forever
function, because it makes no sense to cache the loop object - it can lead to a lot of problems in future and doesn't improve performance.
<!-- Please write boba, aboba ... -->
^^
As you can see in the screenshot, formatting via placeholders doesn't work in loguru, to solve this problem, you can use an adapter for logging (like this) or make loguru a required dependency.
media which is uploaded once and the sending data is saved for next uploads
kitten = CachedMedia(photo="assets/kitten.jpg")
async def runtime(...):
await api.send_photo(message.chat.id, caption="you lol", photo=await kitten.get())
ctx is rarely needed therefore it can be omitted by default
message = await wm.wait(view, event, Rule())
message, ctx = await wm.wait(view, event, Rule(), return_ctx=True)
IsPrivate
rule (and some other) do not support CallbackQueryCute
even when the latter must be supported.
IsPrivate
rule checks if chat_type
is private
. Message
does support it and IsPrivate
inherits from MessageRule
that doesn't let anything but Message
go there. There should be something like CallbackQueryOrMessageRule
, adapter of which supports both Message
and CallbackQuery
to improve typing guarantees, it is suggested to set the 'strict' mode for pyright. it is expected that there are over a thousand type errors. it will take some time to fix them
[tool.pyright]
typeCheckingMode = "strict"
maybe middlewares also but they prob need adaptor refactoring
implementation in progress
syntax proposed:
class Rational(MessageRule, composite=True, requires=[HasText()]):
nom: int
denom: int
async def check(self, message: Message) -> Result["Rational", str]:
rational = parse_rational(message.text, raise_error=False)
if rational is None:
return Error("Text is not a rational number")
return Ok(
self.context(rational.nominator, rational.denominator)
)
perhaps other rule can be treated as adaptor to reach event-independent handling for primitive datatypes like text
values
class Text(MessageRule):
async def check(self, message: Message) -> Result[str, str]:
if not message.text:
return Error("Message has no text")
return Ok(message.text)
class Rational(ABCRule, composite=True, adaptor=Text):
async def check(self, text: str) -> Result["Rational", str]:
...
for release: v5
✏️ New feature description ✏️
Please add FSM-like Rule
🪄 Provide a minimal example 🪄
https://vkbottle.readthedocs.io/ru/latest/high-level/handling/state-dispenser/
from telegrinder.rules import CallbackDataMap
@bot.on.callback_query(CallbackDataMap({"count": int}))
async def cb_handler(cb: CallbackQuery, count: int):
await cb.answer(str(count))
bot.run_forever()
to make it intuitive for contributors to use ruff formatter
issues/PRs
🔍The bot task is closed if there are issues with the internet connection on the client's side.
from envparse import Env
from telegrinder import Telegrinder, API, Token
from telegrinder import logger
from telegrinder.client import AiohttpClient
import asyncio
from app.handlers import dps
api = API(Token.from_env())
bot = Telegrinder(api)
env = Env()
logger.set_level(env.str("LOGGING"))
SUPPLIERS_URL_API = env.str("SUPPLIERS_URL_API")
DISCOUNT_URL_API = env.str("DISCOUNT_URL_API")
@bot.loop_wrapper.interval(minutes=10)
async def update():
client = AiohttpClient()
headers = {}
response = await client.request_json(
SUPPLIERS_URL_API + "/api/v3/orders/new", "GET", headers=headers
)
# other things
if __name__ == "__main__":
for dp in dps:
bot.dispatch.load(dp)
bot.run_forever()
Traceback (most recent call last):
File "main.py", line 1, in <module>
...
telegrinder | ERROR | 2024-07-15 00:50:52 | telegrinder.bot.polling.polling:listen:119 > Client connection failed, attempted to reconnect...
telegrinder | ERROR | 2024-07-15 14:18:57 | telegrinder.bot.polling.polling:listen:119 > Client connection failed, attempted to reconnect...
telegrinder | ERROR | 2024-07-15 21:03:56 | telegrinder.tools.loop_wrapper.loop_wrapper:run_event_loop:117 > Server disconnected
File "/project/venv/lib/python3.12/site-packages/telegrinder/tools/loop_wrapper/loop_wrapper.py", line 115, in run_event_loop
task_result.result()
File "/project/venv/lib/python3.12/site-packages/telegrinder/tools/loop_wrapper/loop_wrapper.py", line 51, in __call__
await self.handler(*args, **kwargs)
File "/project/app/__main__.py", line 27, in update
response = await client.request_json(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/project/venv/lib/python3.12/site-packages/telegrinder/client/aiohttp.py", line 66, in request_json
response = await self.request_raw(url, method, data, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/project/venv/lib/python3.12/site-packages/telegrinder/client/aiohttp.py", line 49, in request_raw
async with self.session.request(
File "/project/venv/lib/python3.12/site-packages/aiohttp/client.py", line 1197, in __aenter__
self._resp = await self._coro
^^^^^^^^^^^^^^^^
File "/project/venv/lib/python3.12/site-packages/aiohttp/client.py", line 608, in _request
await resp.start(conn)
File "/project/venv/lib/python3.12/site-packages/aiohttp/client_reqrep.py", line 976, in start
message, payload = await protocol.read() # type: ignore[union-attr]
^^^^^^^^^^^^^^^^^^^^^
File "/project/venv/lib/python3.12/site-packages/aiohttp/streams.py", line 640, in read
await self._waiter
aiohttp.client_exceptions.ServerDisconnectedError: Server disconnected
Task was destroyed but it is pending!
task: <Task pending name='Task-35647' coro=<Dispatch.feed() done, defined at /project/venv/lib/python3.12/site-packages/telegrinder/bot/dispatch/dispatch.py:136> wait_for=<Future pending cb=[Task.task_wakeup()]>>
telegrinder | ERROR | 2024-07-16 14:37:55 | telegrinder.bot.polling.polling:listen:119 > Client connection failed, attempted to reconnect...
✏️ New feature description ✏️
Could you add a default value for the parse_mode parameter? This will reduce lines of code for developers.
🪄 Provide a minimal example 🪄
api = API(token=token, prase_mode=ParseMode.HTML)
async def handler(m):
await m.answer(
HTMLFormatter(bold(italic("bold italic text!"))),
)
# instead of
async def handler(m):
await m.answer(
HTMLFormatter(bold(italic("bold italic text!"))),
parse_mode=HTMLFormatter.PARSE_MODE,
)
✏️ New feature description ✏️
Changeable base lifetime of all waiting tasks, instead of limiting their number (but they can be combined) or specifying the lifetime of each specific task.
🪄 Provide a minimal example 🪄
class WaiterMachine:
def __init__(self, *, max_storage_size: int = 1000, base_task_lifetime: float | timedelta | None = None) -> None:
self.max_storage_size = max_storage_size
self.base_task_lifetime = base_task_lifetime
self.storage: Storage = {}
✨ Teachability, Documentation, Adoption ✨
In order to avoid storing dead waiter states it is required to implement limited size dict to store short waiters that will delete old records on exceeding maximum size limit
aiosonic faster than aiohttp. also aiosonic is similar aiohttp, which means that the ABCClient interface class will be easier to use with him
Proposed solution:
Use mock
_: KW_ONLY = ...
a node must allow adding multiple compose implementations
for different sets of child nodes. (this may have a lot of test cases to handle)
class Text(ScalarNode, str):
@composer
async def compose_message(cls, message: MessageNode) -> typing.Self:
...
return cls(message.text.unwrap())
@composer
async def compose_caption(cls, attachment: Attachment) -> typing.Self:
# works with attachments with caption
...
return cls(attachment.caption.unwrap())
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.