Compare commits

..

11 Commits

Author SHA1 Message Date
Bibo-Joshi c062712472 Bump Version to v22.3 (#4870) 2025-07-20 22:01:41 +02:00
Abdelrahman Elkheir 15ae1eac89 Documentation Improvements (#4839)
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2025-07-20 21:42:24 +02:00
Bibo-Joshi 1111d342d6 Full Support for Bot API 9.1 (#4847)
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
Co-authored-by: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com>
2025-07-20 21:39:21 +02:00
Xiaohu e9dd490b2c Make Gender Input Case-Insensitive in `conversationbot.py` (#4855) 2025-07-15 17:00:54 +02:00
pre-commit-ci[bot] 957345f6d9 Bump pre-commit Hooks to Latest Versions (#4858)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2025-07-08 22:22:20 +02:00
Abdelrahman Elkheir cedfc99e24 Remove Functionality Deprecated in API 9.0 (#4852) 2025-07-05 13:05:52 +02:00
dependabot[bot] 3f5f3a6888 Bump sigstore/gh-action-sigstore-python from 3.0.0 to 3.0.1 (#4843)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2025-07-02 20:36:55 +02:00
dependabot[bot] ea967b5e71 Bump astral-sh/setup-uv from 5.4.1 to 6.3.1 (#4842)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2025-07-02 19:52:12 +02:00
dependabot[bot] f1d4264f68 Bump github/codeql-action from 3.28.18 to 3.29.2 (#4841)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2025-07-02 19:24:19 +02:00
dependabot[bot] f55a4c24b6 Bump stefanzweifel/git-auto-commit-action from 5.2.0 to 6.0.1 (#4840)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2025-07-02 19:24:02 +02:00
Bibo-Joshi 661045f962 Update API Token for Local Testing Bot (#4837) 2025-07-01 11:02:16 +02:00
80 changed files with 2710 additions and 309 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
- name: Commit & Push
if: steps.check_title.outputs.IS_RELEASE_PR == 'true'
uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0
uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1
with:
commit_message: "Do chango Release"
repository: ./target-repo
+2 -2
View File
@@ -21,13 +21,13 @@ jobs:
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Run zizmor
run: uvx zizmor --persona=pedantic --format sarif . > results.sarif
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
sarif_file: results.sarif
category: zizmor
+1 -1
View File
@@ -86,7 +86,7 @@ jobs:
sha1sum $file > $file.sha1
done
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0
uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # v3.0.1
with:
inputs: >-
./dist/*.tar.gz
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
sha1sum $file > $file.sha1
done
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0
uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # v3.0.1
with:
inputs: >-
./dist/*.tar.gz
+5 -5
View File
@@ -7,7 +7,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.11.9'
rev: 'v0.12.2'
hooks:
- id: ruff
name: ruff
@@ -25,11 +25,11 @@ repos:
- --diff
- --check
- repo: https://github.com/PyCQA/flake8
rev: 7.2.0
rev: 7.3.0
hooks:
- id: flake8
- repo: https://github.com/PyCQA/pylint
rev: v3.3.6
rev: v3.3.7
hooks:
- id: pylint
files: ^(?!(tests|docs)).*\.py$
@@ -41,7 +41,7 @@ repos:
- aiolimiter~=1.1,<1.3
- . # this basically does `pip install -e .`
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
rev: v1.16.1
hooks:
- id: mypy
name: mypy-ptb
@@ -68,7 +68,7 @@ repos:
- cachetools>=5.3.3,<5.5.0
- . # this basically does `pip install -e .`
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
rev: v3.20.0
hooks:
- id: pyupgrade
args:
+2 -2
View File
@@ -11,7 +11,7 @@
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-9.0-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-9.1-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API version
@@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with
Telegram API support
~~~~~~~~~~~~~~~~~~~~
All types and methods of the Telegram Bot API **9.0** are natively supported by this library.
All types and methods of the Telegram Bot API **9.1** are natively supported by this library.
In addition, Bot API functionality not yet natively included can still be used as described `in our wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Bot-API-Forward-Compatibility>`_.
Notable Features
@@ -0,0 +1,6 @@
internal = "Update API Token for Local Testing Bot"
[[pull_requests]]
uid = "4837"
author_uid = "Bibo-Joshi"
closes_threads = []
@@ -0,0 +1,5 @@
documentation = "Documentation Improvements. Among others, fix links to source code."
[[pull_requests]]
uid = "4839"
author_uid = "aelkheir"
closes_threads = ["4838"]
@@ -0,0 +1,5 @@
internal = "Bump stefanzweifel/git-auto-commit-action from 5.2.0 to 6.0.1"
[[pull_requests]]
uid = "4840"
author_uid = "dependabot"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Bump github/codeql-action from 3.28.18 to 3.29.2"
[[pull_requests]]
uid = "4841"
author_uid = "dependabot"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Bump astral-sh/setup-uv from 5.4.1 to 6.3.1"
[[pull_requests]]
uid = "4842"
author_uid = "dependabot"
closes_threads = []
@@ -0,0 +1,5 @@
internal = "Bump sigstore/gh-action-sigstore-python from 3.0.0 to 3.0.1"
[[pull_requests]]
uid = "4843"
author_uid = "dependabot"
closes_threads = []
@@ -0,0 +1,18 @@
highlights = "Full Support for Bot API 9.1"
features = """
New filters based on Bot API 9.1:
* ``filters.StatusUpdate.DIRECT_MESSAGE_PRICE_CHANGED`` for ``Message.direct_message_price_changed``
* ``filters.StatusUpdate.CHECKLIST_TASKS_ADDED`` for ``Message.checklist_tasks_added``
* ``filters.StatusUpdate.CHECKLIST_TASKS_DONE`` for ``Message.checklist_tasks_done``
* ``filters.CHECKLIST`` for ``Message.checklist``
"""
pull_requests = [
{ uid = "4847", author_uid = "Bibo-Joshi", closes_threads = ["4845"] },
{ uid = "4848", author_uid = "Bibo-Joshi" },
{ uid = "4849", author_uid = "harshil21" },
{ uid = "4851", author_uid = "harshil21" },
{ uid = "4857", author_uid = "aelkheir" },
]
@@ -0,0 +1,11 @@
breaking = """Remove Functionality Deprecated in API 9.0
* Remove deprecated argument and attribute ``BusinessConnection.can_reply``.
* Remove deprecated argument and attribute ``ChatFullInfo.can_send_gift``
* Remove deprecated class ``constants.StarTransactions``. Please instead use :attr:`telegram.constants.Nanostar.VALUE`.
* Remove deprecated attributes ``constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT`` and ``constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT``. Please instead use :attr:`telegram.constants.NanostarLimit.MIN_AMOUNT` and :attr:`telegram.constants.NanostarLimit.MAX_AMOUNT`.
"""
[[pull_requests]]
uid = "4852"
author_uid = "aelkheir"
closes_threads = []
@@ -0,0 +1,5 @@
other= "Make Gender Input Case-Insensitive in ``conversationbot.py``"
[[pull_requests]]
uid = "4855"
author_uid = "fengxiaohu"
closes_threads = ["4846"]
@@ -0,0 +1,5 @@
internal = "Bump `pre-commit` Hooks to Latest Versions"
[[pull_requests]]
uid = "4858"
author_uid = "pre-commit-ci"
closes_threads = []
@@ -0,0 +1,5 @@
other = "Bump Version to v22.3"
[[pull_requests]]
uid = "4870"
author_uid = "Bibo-Joshi"
closes_threads = []
+1 -1
View File
@@ -80,7 +80,7 @@ class CustomChango(DirectoryChanGo):
"""replace "14.5" with version.uid except in the contrib guide
then call super
"""
root = Path(__file__).parent.parent / "telegram"
root = Path(__file__).parent.parent / "src"
python_files = root.rglob("*.py")
pattern = re.compile(r"NEXT\.VERSION")
excluded_paths = {root / "docs/source/contribute.rst"}
+3 -1
View File
@@ -55,6 +55,8 @@ PRIVATE_BASE_CLASSES = {
}
# Resolves to the parent directory of `telegram/`, depending on installation setup,
# could either be `<absolute_path>/src` or `<absolute_path>/site-packages`
FILE_ROOT = Path(inspect.getsourcefile(telegram)).parent.parent.resolve()
@@ -161,7 +163,7 @@ def autodoc_process_docstring(
with contextlib.suppress(Exception):
source_lines, start_line = inspect.getsourcelines(obj)
end_line = start_line + len(source_lines)
file = Path(inspect.getsourcefile(obj)).relative_to(FILE_ROOT)
file = Path("src") / Path(inspect.getsourcefile(obj)).relative_to(FILE_ROOT)
LINE_NUMBERS[name] = (file, start_line, end_line)
# Since we don't document the `__init__`, we call this manually to have it available for
+6
View File
@@ -390,6 +390,8 @@
- Used to generate an HTTP link for an invoice
* - :meth:`~telegram.Bot.edit_user_star_subscription`
- Used for editing a user's star subscription
* - :meth:`~telegram.Bot.get_my_star_balance`
- Used for obtaining the bot's Telegram Stars balance
* - :meth:`~telegram.Bot.get_star_transactions`
- Used for obtaining the bot's Telegram Stars transactions
* - :meth:`~telegram.Bot.refund_star_payment`
@@ -447,6 +449,10 @@
- Used for transferring owned unique gifts to another user.
* - :meth:`~telegram.Bot.transfer_business_account_stars`
- Used for transfering Stars from the business account balance to the bot's balance.
* - :meth:`~telegram.Bot.send_checklist`
- Used for sending a checklist on behalf of the business account.
* - :meth:`~telegram.Bot.edit_message_checklist`
- Used for editing a checklist on behalf of the business account.
.. raw:: html
+7
View File
@@ -31,6 +31,10 @@ Available Types
telegram.chat
telegram.chatadministratorrights
telegram.chatbackground
telegram.checklist
telegram.checklisttask
telegram.checklisttasksadded
telegram.checklisttasksdone
telegram.copytextbutton
telegram.backgroundtype
telegram.backgroundtypefill
@@ -66,6 +70,7 @@ Available Types
telegram.chatshared
telegram.contact
telegram.dice
telegram.directmessagepricechanged
telegram.document
telegram.externalreplyinfo
telegram.file
@@ -85,6 +90,8 @@ Available Types
telegram.inaccessiblemessage
telegram.inlinekeyboardbutton
telegram.inlinekeyboardmarkup
telegram.inputchecklist
telegram.inputchecklisttask
telegram.inputfile
telegram.inputmedia
telegram.inputmediaanimation
+6
View File
@@ -0,0 +1,6 @@
Checklist
=========
.. autoclass:: telegram.Checklist
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
ChecklistTask
=============
.. autoclass:: telegram.ChecklistTask
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
ChecklistTasksAdded
===================
.. autoclass:: telegram.ChecklistTasksAdded
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
ChecklistTasksDone
==================
.. autoclass:: telegram.ChecklistTasksDone
:members:
:show-inheritance:
-1
View File
@@ -5,5 +5,4 @@ telegram.constants Module
:members:
:show-inheritance:
:no-undoc-members:
:inherited-members: Enum, EnumMeta, str, int, float
:exclude-members: __format__, __new__, __repr__, __str__
@@ -0,0 +1,6 @@
DirectMessagePriceChanged
=========================
.. autoclass:: telegram.DirectMessagePriceChanged
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
InputChecklist
==============
.. autoclass:: telegram.InputChecklist
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
InputChecklistTask
==================
.. autoclass:: telegram.InputChecklistTask
:members:
:show-inheritance:
+3 -1
View File
@@ -145,7 +145,9 @@ def main() -> None:
conv_handler = ConversationHandler(
entry_points=[CommandHandler("start", start)],
states={
GENDER: [MessageHandler(filters.Regex("^(Boy|Girl|Other)$"), gender)],
# Use case-insensitive regex to accept gender input regardless of letter casing,
# e.g., "boy", "BOY", "Girl", etc., will all be matched
GENDER: [MessageHandler(filters.Regex("(?i)^(Boy|Girl|Other)$"), gender)],
PHOTO: [MessageHandler(filters.PHOTO, photo), CommandHandler("skip", skip_photo)],
LOCATION: [
MessageHandler(filters.LOCATION, location),
+10
View File
@@ -82,12 +82,17 @@ __all__ = (
"ChatPermissions",
"ChatPhoto",
"ChatShared",
"Checklist",
"ChecklistTask",
"ChecklistTasksAdded",
"ChecklistTasksDone",
"ChosenInlineResult",
"Contact",
"CopyTextButton",
"Credentials",
"DataCredentials",
"Dice",
"DirectMessagePriceChanged",
"Document",
"EncryptedCredentials",
"EncryptedPassportElement",
@@ -138,6 +143,8 @@ __all__ = (
"InlineQueryResultVideo",
"InlineQueryResultVoice",
"InlineQueryResultsButton",
"InputChecklist",
"InputChecklistTask",
"InputContactMessageContent",
"InputFile",
"InputInvoiceMessageContent",
@@ -302,6 +309,7 @@ __all__ = (
"warnings",
)
from telegram._inputchecklist import InputChecklist, InputChecklistTask
from telegram._payment.stars.staramount import StarAmount
from telegram._payment.stars.startransactions import StarTransaction, StarTransactions
from telegram._payment.stars.transactionpartner import (
@@ -381,9 +389,11 @@ from ._chatmember import (
)
from ._chatmemberupdated import ChatMemberUpdated
from ._chatpermissions import ChatPermissions
from ._checklists import Checklist, ChecklistTask, ChecklistTasksAdded, ChecklistTasksDone
from ._choseninlineresult import ChosenInlineResult
from ._copytextbutton import CopyTextButton
from ._dice import Dice
from ._directmessagepricechanged import DirectMessagePriceChanged
from ._files._inputstorycontent import (
InputStoryContent,
InputStoryContentPhoto,
+1 -1
View File
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=missing-module-docstring
# ruff: noqa: T201, D100, S603, S607
# ruff: noqa: T201, D100, S607
import subprocess
import sys
from typing import Optional
+174 -1
View File
@@ -78,6 +78,7 @@ from telegram._games.gamehighscore import GameHighScore
from telegram._gifts import AcceptedGiftTypes, Gift, Gifts
from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton
from telegram._inline.preparedinlinemessage import PreparedInlineMessage
from telegram._inputchecklist import InputChecklist
from telegram._menubutton import MenuButton
from telegram._message import Message
from telegram._messageid import MessageId
@@ -3176,7 +3177,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
google_place_id (:obj:`str`, optional): Google Places identifier of the venue.
google_place_type (:obj:`str`, optional): Google Places type of the venue. (See
`supported types \
<https://developers.google.com/maps/documentation/places/web-service/supported_types>`_.)
<https://developers.google.com/maps/documentation/places/web-service/place-types>`_.)
disable_notification (:obj:`bool`, optional): |disable_notification|
protect_content (:obj:`bool`, optional): |protect_content|
@@ -7555,6 +7556,142 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
)
return Poll.de_json(result, self)
async def send_checklist(
self,
business_connection_id: str,
chat_id: int,
checklist: InputChecklist,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_effect_id: Optional[str] = None,
reply_parameters: Optional["ReplyParameters"] = None,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Message:
"""
Use this method to send a checklist on behalf of a connected business account.
.. versionadded:: 22.3
Args:
business_connection_id (:obj:`str`):
|business_id_str|
chat_id (:obj:`int`):
Unique identifier for the target chat.
checklist (:class:`telegram.InputChecklist`):
The checklist to send.
disable_notification (:obj:`bool`, optional):
|disable_notification|
protect_content (:obj:`bool`, optional):
|protect_content|
message_effect_id (:obj:`str`, optional):
|message_effect_id|
reply_parameters (:class:`telegram.ReplyParameters`, optional):
|reply_parameters|
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional):
An object for an inline keyboard
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience
parameter for
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience
parameter for
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"chat_id": chat_id,
"checklist": checklist,
}
return await self._send_message(
"sendChecklist",
data,
disable_notification=disable_notification,
reply_markup=reply_markup,
protect_content=protect_content,
reply_parameters=reply_parameters,
message_effect_id=message_effect_id,
business_connection_id=business_connection_id,
allow_sending_without_reply=allow_sending_without_reply,
reply_to_message_id=reply_to_message_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def edit_message_checklist(
self,
business_connection_id: str,
chat_id: int,
message_id: int,
checklist: InputChecklist,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Message:
"""
Use this method to edit a checklist on behalf of a connected business account.
.. versionadded:: 22.3
Args:
business_connection_id (:obj:`str`):
|business_id_str|
chat_id (:obj:`int`):
Unique identifier for the target chat.
message_id (:obj:`int`):
Unique identifier for the target message.
checklist (:class:`telegram.InputChecklist`):
The new checklist.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional):
An object for the new inline keyboard for the message.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"chat_id": chat_id,
"message_id": message_id,
"checklist": checklist,
}
return await self._send_message(
"editMessageChecklist",
data,
reply_markup=reply_markup,
business_connection_id=business_connection_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def send_dice(
self,
chat_id: Union[int, str],
@@ -11072,6 +11209,36 @@ CHAT_ACTIVITY_TIMEOUT` seconds.
api_kwargs=api_kwargs,
)
async def get_my_star_balance(
self,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> StarAmount:
"""A method to get the current Telegram Stars balance of the bot. Requires no parameters.
.. versionadded:: 22.3
Returns:
:class:`telegram.StarAmount`
Raises:
:class:`telegram.error.TelegramError`
"""
return StarAmount.de_json(
await self._post(
"getMyStarBalance",
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
)
def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002
"""See :meth:`telegram.TelegramObject.to_dict`."""
data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name}
@@ -11244,6 +11411,10 @@ CHAT_ACTIVITY_TIMEOUT` seconds.
"""Alias for :meth:`send_poll`"""
stopPoll = stop_poll
"""Alias for :meth:`stop_poll`"""
sendChecklist = send_checklist
"""Alias for :meth:`send_checklist`"""
editMessageChecklist = edit_message_checklist
"""Alias for :meth:`edit_message_checklist`"""
sendDice = send_dice
"""Alias for :meth:`send_dice`"""
getMyCommands = get_my_commands
@@ -11386,3 +11557,5 @@ CHAT_ACTIVITY_TIMEOUT` seconds.
"""Alias for :meth:`remove_chat_verification`"""
removeUserVerification = remove_user_verification
"""Alias for :meth:`remove_user_verification`"""
getMyStarBalance = get_my_star_balance
"""Alias for :meth:`get_my_star_balance`"""
+5 -51
View File
@@ -30,12 +30,6 @@ from telegram._user import User
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
from telegram._utils.warnings import warn
from telegram._utils.warnings_transition import (
build_deprecation_warning_message,
warn_about_deprecated_attr_in_property,
)
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import Bot
@@ -195,7 +189,10 @@ class BusinessConnection(TelegramObject):
.. versionadded:: 21.1
.. versionchanged:: 22.1
Equality comparison now considers :attr:`rights` instead of :attr:`can_reply`.
Equality comparison now considers :attr:`rights` instead of ``can_reply``.
.. versionremoved:: 22.3
Removed argument and attribute ``can_reply`` deprecated by API 9.0.
Args:
id (:obj:`str`): Unique identifier of the business connection.
@@ -203,11 +200,6 @@ class BusinessConnection(TelegramObject):
user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the
business connection.
date (:obj:`datetime.datetime`): Date the connection was established in Unix time.
can_reply (:obj:`bool`, optional): True, if the bot can act on behalf of the business
account in chats that were active in the last 24 hours.
.. deprecated:: 22.1
Bot API 9.0 deprecated this argument in favor of :paramref:`rights`.
is_enabled (:obj:`bool`): True, if the connection is active.
rights (:class:`BusinessBotRights`, optional): Rights of the business bot.
@@ -226,7 +218,6 @@ class BusinessConnection(TelegramObject):
"""
__slots__ = (
"_can_reply",
"date",
"id",
"is_enabled",
@@ -241,37 +232,16 @@ class BusinessConnection(TelegramObject):
user: "User",
user_chat_id: int,
date: dtm.datetime,
can_reply: Optional[bool] = None,
# temporarily optional to account for changed signature
# tags: deprecated 22.1; bot api 9.0
is_enabled: Optional[bool] = None,
is_enabled: bool,
rights: Optional[BusinessBotRights] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
if is_enabled is None:
raise TypeError("Missing required argument `is_enabled`")
if can_reply is not None:
warn(
PTBDeprecationWarning(
version="22.1",
message=build_deprecation_warning_message(
deprecated_name="can_reply",
new_name="rights",
bot_api_version="9.0",
object_type="parameter",
),
),
stacklevel=2,
)
super().__init__(api_kwargs=api_kwargs)
self.id: str = id
self.user: User = user
self.user_chat_id: int = user_chat_id
self.date: dtm.datetime = date
self._can_reply: Optional[bool] = can_reply
self.is_enabled: bool = is_enabled
self.rights: Optional[BusinessBotRights] = rights
@@ -286,22 +256,6 @@ class BusinessConnection(TelegramObject):
self._freeze()
@property
def can_reply(self) -> Optional[bool]:
""":obj:`bool`: Optional. True, if the bot can act on behalf of the business account in
chats that were active in the last 24 hours.
.. deprecated:: 22.1
Bot API 9.0 deprecated this argument in favor of :attr:`rights`
"""
warn_about_deprecated_attr_in_property(
deprecated_attr_name="can_reply",
new_attr_name="rights",
bot_api_version="9.0",
ptb_version="22.1",
)
return self._can_reply
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessConnection":
"""See :meth:`telegram.TelegramObject.de_json`."""
+38
View File
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Final, Optional, Union
from telegram import constants
from telegram._files.location import Location
from telegram._inputchecklist import InputChecklist
from telegram._message import MaybeInaccessibleMessage, Message
from telegram._telegramobject import TelegramObject
from telegram._user import User
@@ -345,6 +346,43 @@ class CallbackQuery(TelegramObject):
show_caption_above_media=show_caption_above_media,
)
async def edit_message_checklist(
self,
checklist: InputChecklist,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Union[Message, bool]:
"""Shortcut for::
await update.callback_query.message.edit_checklist(*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Message.edit_checklist`.
.. versionadded:: 22.3
Returns:
:class:`telegram.Message`: On success, the edited Message is returned.
Raises:
:exc:`TypeError` if :attr:`message` is not accessible.
"""
return await self._get_message().edit_checklist(
checklist=checklist,
reply_markup=reply_markup,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def edit_message_reply_markup(
self,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
+49
View File
@@ -53,6 +53,7 @@ if TYPE_CHECKING:
Document,
Gift,
InlineKeyboardMarkup,
InputChecklist,
InputMediaAudio,
InputMediaDocument,
InputMediaPhoto,
@@ -1471,6 +1472,54 @@ class _ChatBase(TelegramObject):
allow_paid_broadcast=allow_paid_broadcast,
)
async def send_checklist(
self,
business_connection_id: str,
checklist: "InputChecklist",
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
reply_parameters: Optional["ReplyParameters"] = None,
message_effect_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> "Message":
"""Shortcut for::
await bot.send_checklist(chat_id=update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_checklist`.
.. versionadded:: 22.3
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return await self.get_bot().send_checklist(
chat_id=self.id,
business_connection_id=business_connection_id,
checklist=checklist,
disable_notification=disable_notification,
protect_content=protect_content,
message_effect_id=message_effect_id,
reply_parameters=reply_parameters,
reply_markup=reply_markup,
reply_to_message_id=reply_to_message_id,
allow_sending_without_reply=allow_sending_without_reply,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def send_dice(
self,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
+5 -56
View File
@@ -41,12 +41,6 @@ from telegram._utils.datetime import (
get_timedelta_value,
)
from telegram._utils.types import JSONDict, TimePeriod
from telegram._utils.warnings import warn
from telegram._utils.warnings_transition import (
build_deprecation_warning_message,
warn_about_deprecated_attr_in_property,
)
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import Bot, BusinessIntro, BusinessLocation, BusinessOpeningHours, Message
@@ -66,6 +60,9 @@ class ChatFullInfo(_ChatBase):
object. Previously those were only available because this class inherited from
:class:`telegram.Chat`.
.. versionremoved:: 22.3
Removed argument and attribute ``can_send_gift`` deprecated by API 9.0.
Args:
id (:obj:`int`): Unique identifier for this chat.
type (:obj:`str`): Type of chat, can be either :attr:`PRIVATE`, :attr:`GROUP`,
@@ -226,13 +223,6 @@ class ChatFullInfo(_ChatBase):
sent or forwarded to the channel chat. The field is available only for channel chats.
.. versionadded:: 21.4
can_send_gift (:obj:`bool`, optional): :obj:`True`, if gifts can be sent to the chat.
.. versionadded:: 21.11
.. deprecated:: 22.1
Bot API 9.0 introduced :paramref:`accepted_gift_types`, replacing this argument.
Hence, this argument will be removed in future versions.
Attributes:
id (:obj:`int`): Unique identifier for this chat.
@@ -403,7 +393,6 @@ class ChatFullInfo(_ChatBase):
"""
__slots__ = (
"_can_send_gift",
"_message_auto_delete_time",
"_slow_mode_delay",
"accent_color_id",
@@ -450,6 +439,7 @@ class ChatFullInfo(_ChatBase):
type: str,
accent_color_id: int,
max_reaction_count: int,
accepted_gift_types: AcceptedGiftTypes,
title: Optional[str] = None,
username: Optional[str] = None,
first_name: Optional[str] = None,
@@ -490,10 +480,6 @@ class ChatFullInfo(_ChatBase):
linked_chat_id: Optional[int] = None,
location: Optional[ChatLocation] = None,
can_send_paid_media: Optional[bool] = None,
# tags: deprecated 22.1; bot api 9.0
can_send_gift: Optional[bool] = None,
# temporarily optional to account for changed signature
accepted_gift_types: Optional[AcceptedGiftTypes] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -507,23 +493,6 @@ class ChatFullInfo(_ChatBase):
is_forum=is_forum,
api_kwargs=api_kwargs,
)
if accepted_gift_types is None:
raise TypeError("`accepted_gift_type` is a required argument since Bot API 9.0")
if can_send_gift is not None:
warn(
PTBDeprecationWarning(
"22.1",
build_deprecation_warning_message(
deprecated_name="can_send_gift",
new_name="accepted_gift_types",
object_type="parameter",
bot_api_version="9.0",
),
),
stacklevel=2,
)
# Required and unique to this class-
with self._unfrozen():
self.max_reaction_count: int = max_reaction_count
@@ -575,28 +544,8 @@ class ChatFullInfo(_ChatBase):
self.business_location: Optional[BusinessLocation] = business_location
self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours
self.can_send_paid_media: Optional[bool] = can_send_paid_media
self._can_send_gift: Optional[bool] = can_send_gift
self.accepted_gift_types: AcceptedGiftTypes = accepted_gift_types
@property
def can_send_gift(self) -> Optional[bool]:
"""
:obj:`bool`: Optional. :obj:`True`, if gifts can be sent to the chat.
.. deprecated:: 22.1
As Bot API 9.0 replaces this attribute with :attr:`accepted_gift_types`, this attribute
will be removed in future versions.
"""
warn_about_deprecated_attr_in_property(
deprecated_attr_name="can_send_gift",
new_attr_name="accepted_gift_types",
bot_api_version="9.0",
ptb_version="22.1",
stacklevel=2,
)
return self._can_send_gift
@property
def slow_mode_delay(self) -> Optional[Union[int, dtm.timedelta]]:
return get_timedelta_value(self._slow_mode_delay, attribute="slow_mode_delay")
@@ -624,7 +573,7 @@ class ChatFullInfo(_ChatBase):
data.get("accepted_gift_types"), AcceptedGiftTypes, bot
)
from telegram import ( # pylint: disable=import-outside-toplevel
from telegram import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
BusinessIntro,
BusinessLocation,
BusinessOpeningHours,
+392
View File
@@ -0,0 +1,392 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an objects related to Telegram checklists."""
import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional
from telegram._messageentity import MessageEntity
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.entities import parse_message_entities, parse_message_entity
from telegram._utils.types import JSONDict
from telegram.constants import ZERO_DATE
if TYPE_CHECKING:
from telegram import Bot, Message
class ChecklistTask(TelegramObject):
"""
Describes a task in a checklist.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if all their :attr:`id` is equal.
.. versionadded:: 22.3
Args:
id (:obj:`int`): Unique identifier of the task.
text (:obj:`str`): Text of the task.
text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special
entities that appear in the task text.
completed_by_user (:class:`telegram.User`, optional): User that completed the task; omitted
if the task wasn't completed
completion_date (:class:`datetime.datetime`, optional): Point in time when
the task was completed; :attr:`~telegram.constants.ZERO_DATE` if the task wasn't
completed
|datetime_localization|
Attributes:
id (:obj:`int`): Unique identifier of the task.
text (:obj:`str`): Text of the task.
text_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special
entities that appear in the task text.
completed_by_user (:class:`telegram.User`): Optional. User that completed the task; omitted
if the task wasn't completed
completion_date (:class:`datetime.datetime`): Optional. Point in time when
the task was completed; :attr:`~telegram.constants.ZERO_DATE` if the task wasn't
completed
|datetime_localization|
"""
__slots__ = (
"completed_by_user",
"completion_date",
"id",
"text",
"text_entities",
)
def __init__(
self,
id: int, # pylint: disable=redefined-builtin
text: str,
text_entities: Optional[Sequence[MessageEntity]] = None,
completed_by_user: Optional[User] = None,
completion_date: Optional[dtm.datetime] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.id: int = id
self.text: str = text
self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
self.completed_by_user: Optional[User] = completed_by_user
self.completion_date: Optional[dtm.datetime] = completion_date
self._id_attrs = (self.id,)
self._freeze()
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTask":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
if (date := data.get("completion_date")) == 0:
data["completion_date"] = ZERO_DATE
else:
data["completion_date"] = from_timestamp(date, tzinfo=loc_tzinfo)
data["completed_by_user"] = de_json_optional(data.get("completed_by_user"), User, bot)
data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot)
return super().de_json(data=data, bot=bot)
def parse_entity(self, entity: MessageEntity) -> str:
"""Returns the text in :attr:`text`
from a given :class:`telegram.MessageEntity` of :attr:`text_entities`.
Note:
This method is present because Telegram calculates the offset and length in
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
(That is, you can't just slice ``ChecklistTask.text`` with the offset and length.)
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to :attr:`text_entities`.
Returns:
:obj:`str`: The text of the given entity.
"""
return parse_message_entity(self.text, entity)
def parse_entities(self, types: Optional[list[str]] = None) -> dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this checklist task filtered by their ``type`` attribute as
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
Note:
This method should always be used instead of the :attr:`text_entities`
attribute, since it calculates the correct substring from the message text based on
UTF-16 codepoints. See :attr:`parse_entity` for more info.
Args:
types (list[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
``type`` attribute of an entity is contained in this list, it will be returned.
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
Returns:
dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
return parse_message_entities(self.text, self.text_entities, types)
class Checklist(TelegramObject):
"""
Describes a checklist.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if all their :attr:`tasks` are equal.
.. versionadded:: 22.3
Args:
title (:obj:`str`): Title of the checklist.
title_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special
entities that appear in the checklist title.
tasks (Sequence[:class:`telegram.ChecklistTask`]): List of tasks in the checklist.
others_can_add_tasks (:obj:`bool`, optional): :obj:`True` if users other than the creator
of the list can add tasks to the list
others_can_mark_tasks_as_done (:obj:`bool`, optional): :obj:`True` if users other than the
creator of the list can mark tasks as done or not done
Attributes:
title (:obj:`str`): Title of the checklist.
title_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special
entities that appear in the checklist title.
tasks (Tuple[:class:`telegram.ChecklistTask`]): List of tasks in the checklist.
others_can_add_tasks (:obj:`bool`): Optional. :obj:`True` if users other than the creator
of the list can add tasks to the list
others_can_mark_tasks_as_done (:obj:`bool`): Optional. :obj:`True` if users other than the
creator of the list can mark tasks as done or not done
"""
__slots__ = (
"others_can_add_tasks",
"others_can_mark_tasks_as_done",
"tasks",
"title",
"title_entities",
)
def __init__(
self,
title: str,
tasks: Sequence[ChecklistTask],
title_entities: Optional[Sequence[MessageEntity]] = None,
others_can_add_tasks: Optional[bool] = None,
others_can_mark_tasks_as_done: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.title: str = title
self.title_entities: tuple[MessageEntity, ...] = parse_sequence_arg(title_entities)
self.tasks: tuple[ChecklistTask, ...] = parse_sequence_arg(tasks)
self.others_can_add_tasks: Optional[bool] = others_can_add_tasks
self.others_can_mark_tasks_as_done: Optional[bool] = others_can_mark_tasks_as_done
self._id_attrs = (self.tasks,)
self._freeze()
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Checklist":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
data["title_entities"] = de_list_optional(data.get("title_entities"), MessageEntity, bot)
data["tasks"] = de_list_optional(data.get("tasks"), ChecklistTask, bot)
return super().de_json(data=data, bot=bot)
def parse_entity(self, entity: MessageEntity) -> str:
"""Returns the text in :attr:`title`
from a given :class:`telegram.MessageEntity` of :attr:`title_entities`.
Note:
This method is present because Telegram calculates the offset and length in
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
(That is, you can't just slice :attr:`title` with the offset and length.)
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to :attr:`title_entities`.
Returns:
:obj:`str`: The text of the given entity.
"""
return parse_message_entity(self.title, entity)
def parse_entities(self, types: Optional[list[str]] = None) -> dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this checklist's title filtered by their ``type`` attribute as
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
Note:
This method should always be used instead of the :attr:`title_entities`
attribute, since it calculates the correct substring from the message text based on
UTF-16 codepoints. See :attr:`parse_entity` for more info.
Args:
types (list[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
``type`` attribute of an entity is contained in this list, it will be returned.
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
Returns:
dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
return parse_message_entities(self.title, self.title_entities, types)
class ChecklistTasksDone(TelegramObject):
"""
Describes a service message about checklist tasks marked as done or not done.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their :attr:`marked_as_done_task_ids` and
:attr:`marked_as_not_done_task_ids` are equal.
.. versionadded:: 22.3
Args:
checklist_message (:class:`telegram.Message`, optional): Message containing the checklist
whose tasks were marked as done or not done. Note that the ~:class:`telegram.Message`
object in this field will not contain the :attr:`~telegram.Message.reply_to_message`
field even if it itself is a reply.
marked_as_done_task_ids (Sequence[:obj:`int`], optional): Identifiers of the tasks that
were marked as done
marked_as_not_done_task_ids (Sequence[:obj:`int`], optional): Identifiers of the tasks that
were marked as not done
Attributes:
checklist_message (:class:`telegram.Message`): Optional. Message containing the checklist
whose tasks were marked as done or not done. Note that the ~:class:`telegram.Message`
object in this field will not contain the :attr:`~telegram.Message.reply_to_message`
field even if it itself is a reply.
marked_as_done_task_ids (Tuple[:obj:`int`]): Optional. Identifiers of the tasks that were
marked as done
marked_as_not_done_task_ids (Tuple[:obj:`int`]): Optional. Identifiers of the tasks that
were marked as not done
"""
__slots__ = (
"checklist_message",
"marked_as_done_task_ids",
"marked_as_not_done_task_ids",
)
def __init__(
self,
checklist_message: Optional["Message"] = None,
marked_as_done_task_ids: Optional[Sequence[int]] = None,
marked_as_not_done_task_ids: Optional[Sequence[int]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.checklist_message: Optional[Message] = checklist_message
self.marked_as_done_task_ids: tuple[int, ...] = parse_sequence_arg(marked_as_done_task_ids)
self.marked_as_not_done_task_ids: tuple[int, ...] = parse_sequence_arg(
marked_as_not_done_task_ids
)
self._id_attrs = (self.marked_as_done_task_ids, self.marked_as_not_done_task_ids)
self._freeze()
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTasksDone":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
# needs to be imported here to avoid circular import issues
from telegram import Message # pylint: disable=import-outside-toplevel # noqa: PLC0415
data["checklist_message"] = de_json_optional(data.get("checklist_message"), Message, bot)
return super().de_json(data=data, bot=bot)
class ChecklistTasksAdded(TelegramObject):
"""
Describes a service message about tasks added to a checklist.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their :attr:`tasks` are equal.
.. versionadded:: 22.3
Args:
checklist_message (:class:`telegram.Message`, optional): Message containing the checklist
to which tasks were added. Note that the ~:class:`telegram.Message`
object in this field will not contain the :attr:`~telegram.Message.reply_to_message`
field even if it itself is a reply.
tasks (Sequence[:class:`telegram.ChecklistTask`]): List of tasks added to the checklist
Attributes:
checklist_message (:class:`telegram.Message`): Optional. Message containing the checklist
to which tasks were added. Note that the ~:class:`telegram.Message`
object in this field will not contain the :attr:`~telegram.Message.reply_to_message`
field even if it itself is a reply.
tasks (Tuple[:class:`telegram.ChecklistTask`]): List of tasks added to the checklist
"""
__slots__ = ("checklist_message", "tasks")
def __init__(
self,
tasks: Sequence[ChecklistTask],
checklist_message: Optional["Message"] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.checklist_message: Optional[Message] = checklist_message
self.tasks: tuple[ChecklistTask, ...] = parse_sequence_arg(tasks)
self._id_attrs = (self.tasks,)
self._freeze()
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTasksAdded":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
# needs to be imported here to avoid circular import issues
from telegram import Message # pylint: disable=import-outside-toplevel # noqa: PLC0415
data["checklist_message"] = de_json_optional(data.get("checklist_message"), Message, bot)
data["tasks"] = ChecklistTask.de_list(data.get("tasks", []), bot)
return super().de_json(data=data, bot=bot)
@@ -0,0 +1,73 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Direct Message Price."""
from typing import Optional
from telegram._telegramobject import TelegramObject
from telegram._utils.types import JSONDict
class DirectMessagePriceChanged(TelegramObject):
"""
Describes a service message about a change in the price of direct messages sent to a channel
chat.
.. versionadded:: 22.3
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`are_direct_messages_enabled`, and
:attr:`direct_message_star_count` are equal.
Args:
are_direct_messages_enabled (:obj:`bool`):
:obj:`True`, if direct messages are enabled for the channel chat; :obj:`False`
otherwise.
direct_message_star_count (:obj:`int`, optional):
The new number of Telegram Stars that must be paid by users for each direct message
sent to the channel. Does not apply to users who have been exempted by administrators.
Defaults to ``0``.
Attributes:
are_direct_messages_enabled (:obj:`bool`):
:obj:`True`, if direct messages are enabled for the channel chat; :obj:`False`
otherwise.
direct_message_star_count (:obj:`int`):
Optional. The new number of Telegram Stars that must be paid by users for each direct
message sent to the channel. Does not apply to users who have been exempted by
administrators. Defaults to ``0``.
"""
__slots__ = ("are_direct_messages_enabled", "direct_message_star_count")
def __init__(
self,
are_direct_messages_enabled: bool,
direct_message_star_count: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.are_direct_messages_enabled: bool = are_direct_messages_enabled
self.direct_message_star_count: Optional[int] = direct_message_star_count
self._id_attrs = (self.are_direct_messages_enabled, self.direct_message_star_count)
self._freeze()
+3 -1
View File
@@ -371,7 +371,9 @@ class GiveawayCompleted(TelegramObject):
data = cls._parse_data(data)
# Unfortunately, this needs to be here due to cyclic imports
from telegram._message import Message # pylint: disable=import-outside-toplevel
from telegram._message import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel
Message,
)
data["giveaway_message"] = de_json_optional(data.get("giveaway_message"), Message, bot)
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an objects that are related to Telegram input checklists."""
from collections.abc import Sequence
from typing import Optional
from telegram._messageentity import MessageEntity
from telegram._telegramobject import TelegramObject
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.types import JSONDict, ODVInput
class InputChecklistTask(TelegramObject):
"""
Describes a task to add to a checklist.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal if their :attr:`id` is equal.
.. versionadded:: 22.3
Args:
id (:obj:`int`):
Unique identifier of the task; must be positive and unique among all task identifiers
currently present in the checklist.
text (:obj:`str`):
Text of the task;
:tg-const:`telegram.constants.InputChecklistLimit.MIN_TEXT_LENGTH`\
-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TEXT_LENGTH` characters after
entities parsing.
parse_mode (:obj:`str`, optional):
|parse_mode|
text_entities (Sequence[:class:`telegram.MessageEntity`], optional):
List of special entities that appear in the text, which can be specified instead of
parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler, and
custom_emoji entities are allowed.
Attributes:
id (:obj:`int`):
Unique identifier of the task; must be positive and unique among all task identifiers
currently present in the checklist.
text (:obj:`str`):
Text of the task;
:tg-const:`telegram.constants.InputChecklistLimit.MIN_TEXT_LENGTH`\
-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TEXT_LENGTH` characters after
entities parsing.
parse_mode (:obj:`str`):
Optional. |parse_mode|
text_entities (Sequence[:class:`telegram.MessageEntity`]):
Optional. List of special entities that appear in the text, which can be specified
instead of parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler,
and custom_emoji entities are allowed.
"""
__slots__ = (
"id",
"parse_mode",
"text",
"text_entities",
)
def __init__(
self,
id: int, # pylint: disable=redefined-builtin
text: str,
parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence[MessageEntity]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.id: int = id
self.text: str = text
self.parse_mode: ODVInput[str] = parse_mode
self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
self._id_attrs = (self.id,)
self._freeze()
class InputChecklist(TelegramObject):
"""
Describes a checklist to create.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal if their :attr:`tasks` is equal.
.. versionadded:: 22.3
Args:
title (:obj:`str`):
Title of the checklist;
:tg-const:`telegram.constants.InputChecklistLimit.MIN_TITLE_LENGTH`\
-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TITLE_LENGTH` characters after
entities parsing.
parse_mode (:obj:`str`, optional):
|parse_mode|
title_entities (Sequence[:class:`telegram.MessageEntity`], optional):
List of special entities that appear in the title, which
can be specified instead of :paramref:`parse_mode`. Currently, only bold, italic,
underline, strikethrough, spoiler, and custom_emoji entities are allowed.
tasks (Sequence[:class:`telegram.InputChecklistTask`]):
List of
:tg-const:`telegram.constants.InputChecklistLimit.MIN_TASK_NUMBER`\
-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TASK_NUMBER` tasks in
the checklist.
others_can_add_tasks (:obj:`bool`, optional):
Pass :obj:`True` if other users can add tasks to the checklist.
others_can_mark_tasks_as_done (:obj:`bool`, optional):
Pass :obj:`True` if other users can mark tasks as done or not done in the checklist.
Attributes:
title (:obj:`str`):
Title of the checklist;
:tg-const:`telegram.constants.InputChecklistLimit.MIN_TITLE_LENGTH`\
-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TITLE_LENGTH` characters after
entities parsing.
parse_mode (:obj:`str`):
Optional. |parse_mode|
title_entities (Sequence[:class:`telegram.MessageEntity`]):
Optional. List of special entities that appear in the title, which
can be specified instead of :paramref:`parse_mode`. Currently, only bold, italic,
underline, strikethrough, spoiler, and custom_emoji entities are allowed.
tasks (Sequence[:class:`telegram.InputChecklistTask`]):
List of
:tg-const:`telegram.constants.InputChecklistLimit.MIN_TASK_NUMBER`\
-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TASK_NUMBER` tasks in
the checklist.
others_can_add_tasks (:obj:`bool`):
Optional. Pass :obj:`True` if other users can add tasks to the checklist.
others_can_mark_tasks_as_done (:obj:`bool`):
Optional. Pass :obj:`True` if other users can mark tasks as done or not done in
the checklist.
"""
__slots__ = (
"others_can_add_tasks",
"others_can_mark_tasks_as_done",
"parse_mode",
"tasks",
"title",
"title_entities",
)
def __init__(
self,
title: str,
tasks: Sequence[InputChecklistTask],
parse_mode: ODVInput[str] = DEFAULT_NONE,
title_entities: Optional[Sequence[MessageEntity]] = None,
others_can_add_tasks: Optional[bool] = None,
others_can_mark_tasks_as_done: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.title: str = title
self.tasks: tuple[InputChecklistTask, ...] = parse_sequence_arg(tasks)
self.parse_mode: ODVInput[str] = parse_mode
self.title_entities: tuple[MessageEntity, ...] = parse_sequence_arg(title_entities)
self.others_can_add_tasks: Optional[bool] = others_can_add_tasks
self.others_can_mark_tasks_as_done: Optional[bool] = others_can_mark_tasks_as_done
self._id_attrs = (self.tasks,)
self._freeze()
+166 -3
View File
@@ -28,7 +28,9 @@ from typing import TYPE_CHECKING, Optional, TypedDict, Union
from telegram._chat import Chat
from telegram._chatbackground import ChatBackground
from telegram._chatboost import ChatBoostAdded
from telegram._checklists import Checklist, ChecklistTasksAdded, ChecklistTasksDone
from telegram._dice import Dice
from telegram._directmessagepricechanged import DirectMessagePriceChanged
from telegram._files.animation import Animation
from telegram._files.audio import Audio
from telegram._files.contact import Contact
@@ -51,6 +53,7 @@ from telegram._forumtopic import (
from telegram._games.game import Game
from telegram._gifts import GiftInfo
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inputchecklist import InputChecklist
from telegram._linkpreviewoptions import LinkPreviewOptions
from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged
from telegram._messageentity import MessageEntity
@@ -524,6 +527,9 @@ class Message(MaybeInaccessibleMessage):
by a spoiler animation.
.. versionadded:: 20.0
checklist (:class:`telegram.Checklist`, optional): Message is a checklist
.. versionadded:: 22.3
users_shared (:class:`telegram.UsersShared`, optional): Service message: users were shared
with the bot
@@ -601,6 +607,14 @@ class Message(MaybeInaccessibleMessage):
background set.
.. versionadded:: 21.2
checklist_tasks_done (:class:`telegram.ChecklistTasksDone`, optional): Service message:
some tasks in a checklist were marked as done or not done
.. versionadded:: 22.3
checklist_tasks_added (:class:`telegram.ChecklistTasksAdded`, optional): Service message:
tasks were added to a checklist
.. versionadded:: 22.3
paid_media (:class:`telegram.PaidMediaInfo`, optional): Message contains paid media;
information about the paid media.
@@ -609,6 +623,11 @@ class Message(MaybeInaccessibleMessage):
message about a refunded payment, information about the payment.
.. versionadded:: 21.4
direct_message_price_changed (:class:`telegram.DirectMessagePriceChanged`, optional):
Service message: the price for paid messages in the corresponding direct messages chat
of a channel has changed.
.. versionadded:: 22.3
Attributes:
message_id (:obj:`int`): Unique message identifier inside this chat. In specific instances
@@ -868,6 +887,9 @@ class Message(MaybeInaccessibleMessage):
by a spoiler animation.
.. versionadded:: 20.0
checklist (:class:`telegram.Checklist`): Optional. Message is a checklist
.. versionadded:: 22.3
users_shared (:class:`telegram.UsersShared`): Optional. Service message: users were shared
with the bot
@@ -946,6 +968,14 @@ class Message(MaybeInaccessibleMessage):
background set
.. versionadded:: 21.2
checklist_tasks_done (:class:`telegram.ChecklistTasksDone`): Optional. Service message:
some tasks in a checklist were marked as done or not done
.. versionadded:: 22.3
checklist_tasks_added (:class:`telegram.ChecklistTasksAdded`): Optional. Service message:
tasks were added to a checklist
.. versionadded:: 22.3
paid_media (:class:`telegram.PaidMediaInfo`): Optional. Message contains paid media;
information about the paid media.
@@ -954,6 +984,11 @@ class Message(MaybeInaccessibleMessage):
message about a refunded payment, information about the payment.
.. versionadded:: 21.4
direct_message_price_changed (:class:`telegram.DirectMessagePriceChanged`):
Optional. Service message: the price for paid messages in the corresponding direct
messages chat of a channel has changed.
.. versionadded:: 22.3
.. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by
:attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a
@@ -983,10 +1018,14 @@ class Message(MaybeInaccessibleMessage):
"channel_chat_created",
"chat_background_set",
"chat_shared",
"checklist",
"checklist_tasks_added",
"checklist_tasks_done",
"connected_website",
"contact",
"delete_chat_photo",
"dice",
"direct_message_price_changed",
"document",
"edit_date",
"effect_id",
@@ -1152,6 +1191,10 @@ class Message(MaybeInaccessibleMessage):
unique_gift: Optional[UniqueGiftInfo] = None,
paid_message_price_changed: Optional[PaidMessagePriceChanged] = None,
paid_star_count: Optional[int] = None,
direct_message_price_changed: Optional[DirectMessagePriceChanged] = None,
checklist: Optional[Checklist] = None,
checklist_tasks_done: Optional[ChecklistTasksDone] = None,
checklist_tasks_added: Optional[ChecklistTasksAdded] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -1233,6 +1276,7 @@ class Message(MaybeInaccessibleMessage):
)
self.write_access_allowed: Optional[WriteAccessAllowed] = write_access_allowed
self.has_media_spoiler: Optional[bool] = has_media_spoiler
self.checklist: Optional[Checklist] = checklist
self.users_shared: Optional[UsersShared] = users_shared
self.chat_shared: Optional[ChatShared] = chat_shared
self.story: Optional[Story] = story
@@ -1251,6 +1295,8 @@ class Message(MaybeInaccessibleMessage):
self.sender_business_bot: Optional[User] = sender_business_bot
self.is_from_offline: Optional[bool] = is_from_offline
self.chat_background_set: Optional[ChatBackground] = chat_background_set
self.checklist_tasks_done: Optional[ChecklistTasksDone] = checklist_tasks_done
self.checklist_tasks_added: Optional[ChecklistTasksAdded] = checklist_tasks_added
self.effect_id: Optional[str] = effect_id
self.show_caption_above_media: Optional[bool] = show_caption_above_media
self.paid_media: Optional[PaidMediaInfo] = paid_media
@@ -1261,6 +1307,9 @@ class Message(MaybeInaccessibleMessage):
paid_message_price_changed
)
self.paid_star_count: Optional[int] = paid_star_count
self.direct_message_price_changed: Optional[DirectMessagePriceChanged] = (
direct_message_price_changed
)
self._effective_attachment = DEFAULT_NONE
@@ -1402,16 +1451,16 @@ class Message(MaybeInaccessibleMessage):
)
# Unfortunately, this needs to be here due to cyclic imports
from telegram._giveaway import ( # pylint: disable=import-outside-toplevel
from telegram._giveaway import ( # pylint: disable=C0415 # noqa: PLC0415
Giveaway,
GiveawayCompleted,
GiveawayCreated,
GiveawayWinners,
)
from telegram._messageorigin import ( # pylint: disable=import-outside-toplevel
from telegram._messageorigin import ( # pylint: disable=C0415 # noqa: PLC0415
MessageOrigin,
)
from telegram._reply import ( # pylint: disable=import-outside-toplevel
from telegram._reply import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
ExternalReplyInfo,
TextQuote,
)
@@ -1437,6 +1486,16 @@ class Message(MaybeInaccessibleMessage):
data["reply_to_story"] = de_json_optional(data.get("reply_to_story"), Story, bot)
data["boost_added"] = de_json_optional(data.get("boost_added"), ChatBoostAdded, bot)
data["sender_business_bot"] = de_json_optional(data.get("sender_business_bot"), User, bot)
data["direct_message_price_changed"] = de_json_optional(
data.get("direct_message_price_changed"), DirectMessagePriceChanged, bot
)
data["checklist"] = de_json_optional(data.get("checklist"), Checklist, bot)
data["checklist_tasks_done"] = de_json_optional(
data.get("checklist_tasks_done"), ChecklistTasksDone, bot
)
data["checklist_tasks_added"] = de_json_optional(
data.get("checklist_tasks_added"), ChecklistTasksAdded, bot
)
api_kwargs = {}
# This is a deprecated field that TG still returns for backwards compatibility
@@ -3250,6 +3309,63 @@ class Message(MaybeInaccessibleMessage):
allow_paid_broadcast=allow_paid_broadcast,
)
async def reply_checklist(
self,
checklist: InputChecklist,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_effect_id: Optional[str] = None,
reply_parameters: Optional["ReplyParameters"] = None,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> "Message":
"""Shortcut for::
await bot.send_checklist(
business_connection_id=self.business_connection_id,
chat_id=update.effective_message.chat_id,
*args,
**kwargs,
)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_checklist`.
.. versionadded:: 22.3
Keyword Args:
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters, allow_sending_without_reply
)
return await self.get_bot().send_checklist(
business_connection_id=self.business_connection_id,
chat_id=chat_id, # type: ignore[arg-type]
checklist=checklist,
disable_notification=disable_notification,
reply_parameters=effective_reply_parameters,
reply_markup=reply_markup,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
message_effect_id=message_effect_id,
)
async def reply_chat_action(
self,
action: str,
@@ -3859,6 +3975,53 @@ class Message(MaybeInaccessibleMessage):
business_connection_id=self.business_connection_id,
)
async def edit_checklist(
self,
checklist: InputChecklist,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> "Message":
"""Shortcut for::
await bot.edit_message_checklist(
business_connection_id=message.business_connection_id,
chat_id=message.chat_id,
message_id=message.message_id,
*args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.edit_message_checklist`.
.. versionadded:: 22.3
Note:
You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family
of methods) or channel posts, if the bot is an admin in that channel. However, this
behaviour is undocumented and might be changed by Telegram.
Returns:
:class:`telegram.Message`: On success, the edited Message is returned.
"""
return await self.get_bot().edit_message_checklist(
business_connection_id=self.business_connection_id,
chat_id=self.chat_id,
message_id=self.message_id,
checklist=checklist,
reply_markup=reply_markup,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def edit_media(
self,
media: "InputMedia",
+16 -2
View File
@@ -347,13 +347,17 @@ class OwnedGiftUnique(OwnedGift):
bot; for gifts received on behalf of business accounts only.
sender_user (:class:`telegram.User`, optional): Sender of the gift if it is a known user.
send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`.
|datetime_localization|.
|datetime_localization|
is_saved (:obj:`bool`, optional): :obj:`True`, if the gift is displayed on the account's
profile page; for gifts received on behalf of business accounts only.
can_be_transferred (:obj:`bool`, optional): :obj:`True`, if the gift can be transferred to
another owner; for gifts received on behalf of business accounts only.
transfer_star_count (:obj:`int`, optional): Number of Telegram Stars that must be paid
to transfer the gift; omitted if the bot cannot transfer the gift.
next_transfer_date (:obj:`datetime.datetime`, optional): Date when the gift can be
transferred. If it's in the past, then the gift can be transferred now.
|datetime_localization|
.. versionadded:: 22.3
Attributes:
type (:obj:`str`): Type of the owned gift, always :tg-const:`~telegram.OwnedGift.UNIQUE`.
@@ -362,19 +366,24 @@ class OwnedGiftUnique(OwnedGift):
bot; for gifts received on behalf of business accounts only.
sender_user (:class:`telegram.User`): Optional. Sender of the gift if it is a known user.
send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`.
|datetime_localization|.
|datetime_localization|
is_saved (:obj:`bool`): Optional. :obj:`True`, if the gift is displayed on the account's
profile page; for gifts received on behalf of business accounts only.
can_be_transferred (:obj:`bool`): Optional. :obj:`True`, if the gift can be transferred to
another owner; for gifts received on behalf of business accounts only.
transfer_star_count (:obj:`int`): Optional. Number of Telegram Stars that must be paid
to transfer the gift; omitted if the bot cannot transfer the gift.
next_transfer_date (:obj:`datetime.datetime`): Optional. Date when the gift can be
transferred. If it's in the past, then the gift can be transferred now.
|datetime_localization|
.. versionadded:: 22.3
"""
__slots__ = (
"can_be_transferred",
"gift",
"is_saved",
"next_transfer_date",
"owned_gift_id",
"send_date",
"sender_user",
@@ -390,6 +399,7 @@ class OwnedGiftUnique(OwnedGift):
is_saved: Optional[bool] = None,
can_be_transferred: Optional[bool] = None,
transfer_star_count: Optional[int] = None,
next_transfer_date: Optional[dtm.datetime] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
@@ -403,6 +413,7 @@ class OwnedGiftUnique(OwnedGift):
self.is_saved: Optional[bool] = is_saved
self.can_be_transferred: Optional[bool] = can_be_transferred
self.transfer_star_count: Optional[int] = transfer_star_count
self.next_transfer_date: Optional[dtm.datetime] = next_transfer_date
self._id_attrs = (self.type, self.gift, self.send_date)
@@ -415,5 +426,8 @@ class OwnedGiftUnique(OwnedGift):
data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo)
data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot)
data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot)
data["next_transfer_date"] = from_timestamp(
data.get("next_transfer_date"), tzinfo=loc_tzinfo
)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
@@ -1,5 +1,4 @@
#!/usr/bin/env python
# flake8: noqa: E501
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
@@ -416,6 +416,7 @@ class TransactionPartnerUser(TransactionPartner):
def __init__(
self,
transaction_type: str,
user: "User",
invoice_payload: Optional[str] = None,
paid_media: Optional[Sequence[PaidMedia]] = None,
@@ -424,17 +425,11 @@ class TransactionPartnerUser(TransactionPartner):
gift: Optional[Gift] = None,
affiliate: Optional[AffiliateInfo] = None,
premium_subscription_duration: Optional[int] = None,
# temporarily optional to account for changed signature
transaction_type: Optional[str] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs)
# tags: deprecated 22.1, bot api 9.0
if transaction_type is None:
raise TypeError("`transaction_type` is a required argument since Bot API 9.0")
with self._unfrozen():
self.user: User = user
self.affiliate: Optional[AffiliateInfo] = affiliate
+11
View File
@@ -21,6 +21,7 @@ from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional, Union
from telegram._chat import Chat
from telegram._checklists import Checklist
from telegram._dice import Dice
from telegram._files.animation import Animation
from telegram._files.audio import Audio
@@ -89,6 +90,9 @@ class ExternalReplyInfo(TelegramObject):
the file.
has_media_spoiler (:obj:`bool`, optional): :obj:`True`, if the message media is covered by
a spoiler animation.
checklist (:class:`telegram.Checklist`, optional): Message is a checklist
.. versionadded:: 22.3
contact (:class:`telegram.Contact`, optional): Message is a shared contact, information
about the contact.
dice (:class:`telegram.Dice`, optional): Message is a dice with random value.
@@ -138,6 +142,9 @@ class ExternalReplyInfo(TelegramObject):
the file.
has_media_spoiler (:obj:`bool`): Optional. :obj:`True`, if the message media is covered by
a spoiler animation.
checklist (:class:`telegram.Checklist`): Optional. Message is a checklist
.. versionadded:: 22.3
contact (:class:`telegram.Contact`): Optional. Message is a shared contact, information
about the contact.
dice (:class:`telegram.Dice`): Optional. Message is a dice with random value.
@@ -164,6 +171,7 @@ class ExternalReplyInfo(TelegramObject):
"animation",
"audio",
"chat",
"checklist",
"contact",
"dice",
"document",
@@ -213,6 +221,7 @@ class ExternalReplyInfo(TelegramObject):
poll: Optional[Poll] = None,
venue: Optional[Venue] = None,
paid_media: Optional[PaidMediaInfo] = None,
checklist: Optional[Checklist] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -232,6 +241,7 @@ class ExternalReplyInfo(TelegramObject):
self.video_note: Optional[VideoNote] = video_note
self.voice: Optional[Voice] = voice
self.has_media_spoiler: Optional[bool] = has_media_spoiler
self.checklist: Optional[Checklist] = checklist
self.contact: Optional[Contact] = contact
self.dice: Optional[Dice] = dice
self.game: Optional[Game] = game
@@ -278,6 +288,7 @@ class ExternalReplyInfo(TelegramObject):
data["poll"] = de_json_optional(data.get("poll"), Poll, bot)
data["venue"] = de_json_optional(data.get("venue"), Venue, bot)
data["paid_media"] = de_json_optional(data.get("paid_media"), PaidMediaInfo, bot)
data["checklist"] = de_json_optional(data.get("checklist"), Checklist, bot)
return super().de_json(data=data, bot=bot)
+46 -4
View File
@@ -18,6 +18,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]
"""This module contains classes related to unique gifs."""
import datetime as dtm
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
@@ -25,6 +26,7 @@ from telegram._files.sticker import Sticker
from telegram._telegramobject import TelegramObject
from telegram._utils import enum
from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -340,31 +342,63 @@ class UniqueGiftInfo(TelegramObject):
Args:
gift (:class:`UniqueGift`): Information about the gift.
origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE`
or :attr:`TRANSFER`.
origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE` for gifts
upgraded from regular gifts, :attr:`TRANSFER` for gifts transferred from other users
or channels, or :attr:`RESALE` for gifts bought from other users.
.. versionchanged:: 22.3
The :attr:`RESALE` origin was added.
owned_gift_id (:obj:`str`, optional) Unique identifier of the received gift for the
bot; only present for gifts received on behalf of business accounts.
transfer_star_count (:obj:`int`, optional): Number of Telegram Stars that must be paid
to transfer the gift; omitted if the bot cannot transfer the gift.
last_resale_star_count (:obj:`int`, optional): For gifts bought from other users, the price
paid for the gift.
.. versionadded:: 22.3
next_transfer_date (:obj:`datetime.datetime`, optional): Date when the gift can be
transferred. If it's in the past, then the gift can be transferred now.
|datetime_localization|
.. versionadded:: 22.3
Attributes:
gift (:class:`UniqueGift`): Information about the gift.
origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE`
or :attr:`TRANSFER`.
origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE` for gifts
upgraded from regular gifts, :attr:`TRANSFER` for gifts transferred from other users
or channels, or :attr:`RESALE` for gifts bought from other users.
.. versionchanged:: 22.3
The :attr:`RESALE` origin was added.
owned_gift_id (:obj:`str`) Optional. Unique identifier of the received gift for the
bot; only present for gifts received on behalf of business accounts.
transfer_star_count (:obj:`int`): Optional. Number of Telegram Stars that must be paid
to transfer the gift; omitted if the bot cannot transfer the gift.
last_resale_star_count (:obj:`int`): Optional. For gifts bought from other users, the price
paid for the gift.
.. versionadded:: 22.3
next_transfer_date (:obj:`datetime.datetime`): Optional. Date when the gift can be
transferred. If it's in the past, then the gift can be transferred now.
|datetime_localization|
.. versionadded:: 22.3
"""
UPGRADE: Final[str] = constants.UniqueGiftInfoOrigin.UPGRADE
""":const:`telegram.constants.UniqueGiftInfoOrigin.UPGRADE`"""
TRANSFER: Final[str] = constants.UniqueGiftInfoOrigin.TRANSFER
""":const:`telegram.constants.UniqueGiftInfoOrigin.TRANSFER`"""
RESALE: Final[str] = constants.UniqueGiftInfoOrigin.RESALE
""":const:`telegram.constants.UniqueGiftInfoOrigin.RESALE`
.. versionadded:: 22.3
"""
__slots__ = (
"gift",
"last_resale_star_count",
"next_transfer_date",
"origin",
"owned_gift_id",
"transfer_star_count",
@@ -376,6 +410,8 @@ class UniqueGiftInfo(TelegramObject):
origin: str,
owned_gift_id: Optional[str] = None,
transfer_star_count: Optional[int] = None,
last_resale_star_count: Optional[int] = None,
next_transfer_date: Optional[dtm.datetime] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -386,6 +422,8 @@ class UniqueGiftInfo(TelegramObject):
# Optional
self.owned_gift_id: Optional[str] = owned_gift_id
self.transfer_star_count: Optional[int] = transfer_star_count
self.last_resale_star_count: Optional[int] = last_resale_star_count
self.next_transfer_date: Optional[dtm.datetime] = next_transfer_date
self._id_attrs = (self.gift, self.origin)
@@ -396,6 +434,10 @@ class UniqueGiftInfo(TelegramObject):
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
loc_tzinfo = extract_tzinfo_from_defaults(bot)
data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot)
data["next_transfer_date"] = from_timestamp(
data.get("next_transfer_date"), tzinfo=loc_tzinfo
)
return super().de_json(data=data, bot=bot)
+1 -1
View File
@@ -134,7 +134,7 @@ def parse_file_input( # pylint: disable=too-many-return-statements
:attr:`file_input`, in case it's no valid file input.
"""
# Importing on file-level yields cyclic Import Errors
from telegram import InputFile # pylint: disable=import-outside-toplevel
from telegram import InputFile # pylint: disable=import-outside-toplevel # noqa: PLC0415
if isinstance(file_input, str) and file_input.startswith("file://"):
if not local_mode:
+1 -1
View File
@@ -51,6 +51,6 @@ class Version(NamedTuple):
__version_info__: Final[Version] = Version(
major=22, minor=2, micro=0, releaselevel="final", serial=0
major=22, minor=3, micro=0, releaselevel="final", serial=0
)
__version__: Final[str] = str(__version_info__)
+83 -49
View File
@@ -27,6 +27,10 @@ those classes.
.. versionchanged:: 20.0
* Most of the constants in this module are grouped into enums.
.. versionremoved:: 22.3
Removed deprecated class ``StarTransactions``. Please instead use
:attr:`telegram.constants.Nanostar.VALUE`.
"""
# TODO: Remove this when https://github.com/PyCQA/pylint/issues/6887 is resolved.
# pylint: disable=invalid-enum-extension,invalid-slots
@@ -73,6 +77,7 @@ __all__ = [
"InlineQueryResultLimit",
"InlineQueryResultType",
"InlineQueryResultsButtonLimit",
"InputChecklistLimit",
"InputMediaType",
"InputPaidMediaType",
"InputProfilePhotoType",
@@ -103,7 +108,6 @@ __all__ = [
"ReactionType",
"ReplyLimit",
"RevenueWithdrawalStateType",
"StarTransactions",
"StarTransactionsLimit",
"StickerFormat",
"StickerLimit",
@@ -169,7 +173,7 @@ class _AccentColor(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=0)
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=1)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@@ -185,6 +189,7 @@ SUPPORTED_WEBHOOK_PORTS: Final[list[int]] = [443, 80, 88, 8443]
#: :obj:`datetime.datetime`, value of unix 0.
#: This date literal is used in :class:`telegram.InaccessibleMessage`
# and :class:`telegram.ChecklistTask`.
#:
#: .. versionadded:: 20.8
ZERO_DATE: Final[dtm.datetime] = dtm.datetime(1970, 1, 1, tzinfo=UTC)
@@ -1408,6 +1413,47 @@ class InlineKeyboardMarkupLimit(IntEnum):
"""
class InputChecklistLimit(IntEnum):
"""This enum contains limitations for :class:`telegram.InputChecklist`/
:class:`telegram.InputChecklistTask`. The enum
members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 22.3
"""
__slots__ = ()
MIN_TITLE_LENGTH = 1
""":obj:`int`: Minimum number of characters in a :obj:`str` passed as
:paramref:`~telegram.InputChecklist.title` parameter of :class:`telegram.InputChecklist`
"""
MAX_TITLE_LENGTH = 255
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as
:paramref:`~telegram.InputChecklist.title` parameter of :class:`telegram.InputChecklist`
"""
MIN_TEXT_LENGTH = 1
""":obj:`int`: Minimum number of characters in a :obj:`str` passed as
:paramref:`~telegram.InputChecklistTask.text` parameter of :class:`telegram.InputChecklistTask`
"""
MAX_TEXT_LENGTH = 100
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as
:paramref:`~telegram.InputChecklistTask.text` parameter of :class:`telegram.InputChecklistTask`
"""
MIN_TASK_NUMBER = 1
""":obj:`int`: Minimum number of tasks passed as :paramref:`~telegram.InputChecklist.tasks`
parameter of :class:`telegram.InputChecklist`
"""
MAX_TASK_NUMBER = 30
""":obj:`int`: Maximum number of tasks passed as :paramref:`~telegram.InputChecklistTask.tasks`
parameter of :class:`telegram.InputChecklistTask`
"""
class InputMediaType(StringEnum):
"""This enum contains the available types of :class:`telegram.InputMedia`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
@@ -2055,6 +2101,21 @@ class MessageType(StringEnum):
.. versionadded:: 21.2
"""
CHECKLIST = "checklist"
""":obj:`str`: Messages with :attr:`telegram.Message.checklist`.
.. versionadded:: 22.3
"""
CHECKLIST_TASKS_ADDED = "checklist_tasks_added"
""":obj:`str`: Messages with :attr:`telegram.Message.checklist_tasks_added`.
.. versionadded:: 22.3
"""
CHECKLIST_TASKS_DONE = "checklist_tasks_done"
""":obj:`str`: Messages with :attr:`telegram.Message.checklist_tasks_done`.
.. versionadded:: 22.3
"""
CONNECTED_WEBSITE = "connected_website"
""":obj:`str`: Messages with :attr:`telegram.Message.connected_website`."""
CONTACT = "contact"
@@ -2063,6 +2124,11 @@ class MessageType(StringEnum):
""":obj:`str`: Messages with :attr:`telegram.Message.delete_chat_photo`."""
DICE = "dice"
""":obj:`str`: Messages with :attr:`telegram.Message.dice`."""
DIRECT_MESSAGE_PRICE_CHANGED = "direct_message_price_changed"
""":obj:`str`: Messages with :attr:`telegram.Message.direct_message_price_changed`.
.. versionadded:: 22.3
"""
DOCUMENT = "document"
""":obj:`str`: Messages with :attr:`telegram.Message.document`."""
EFFECT_ID = "effect_id"
@@ -2737,36 +2803,18 @@ class RevenueWithdrawalStateType(StringEnum):
""":obj:`str`: A withdrawal failed and the transaction was refunded."""
# tags: deprecated 22.1, bot api 9.0
class StarTransactions(FloatEnum):
"""This enum contains constants for :class:`telegram.StarTransaction`.
The enum members of this enumeration are instances of :class:`float` and can be treated as
such.
.. versionadded:: 21.9
.. deprecated:: 22.1
This class will be removed as its only member :attr:`NANOSTAR_VALUE` will be replaced
by :attr:`telegram.constants.Nanostar.VALUE`.
"""
__slots__ = ()
NANOSTAR_VALUE = Nanostar.VALUE
""":obj:`float`: The value of one nanostar as used in
:attr:`telegram.StarTransaction.nanostar_amount`.
.. deprecated:: 22.1
This member will be replaced by :attr:`telegram.constants.Nanostar.VALUE`.
"""
class StarTransactionsLimit(IntEnum):
"""This enum contains limitations for :class:`telegram.Bot.get_star_transactions` and
:class:`telegram.StarTransaction`.
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 21.4
.. versionremoved:: 22.3
Removed deprecated attributes ``StarTransactionsLimit.NANOSTAR_MIN_AMOUNT``
and ``StarTransactionsLimit.NANOSTAR_MAX_AMOUNT``. Please instead use
:attr:`telegram.constants.NanostarLimit.MIN_AMOUNT`
and :attr:`telegram.constants.NanostarLimit.MAX_AMOUNT`.
"""
__slots__ = ()
@@ -2779,28 +2827,6 @@ class StarTransactionsLimit(IntEnum):
""":obj:`int`: Maximum value allowed for the
:paramref:`~telegram.Bot.get_star_transactions.limit` parameter of
:meth:`telegram.Bot.get_star_transactions`."""
# tags: deprecated 22.1, bot api 9.0
NANOSTAR_MIN_AMOUNT = NanostarLimit.MIN_AMOUNT
""":obj:`int`: Minimum value allowed for :paramref:`~telegram.AffiliateInfo.nanostar_amount`
parameter of :class:`telegram.AffiliateInfo`.
.. versionadded:: 21.9
.. deprecated:: 22.1
This member will be replaced by :attr:`telegram.constants.NanostarLimit.MIN_AMOUNT`.
"""
# tags: deprecated 22.1, bot api 9.0
NANOSTAR_MAX_AMOUNT = NanostarLimit.MAX_AMOUNT
""":obj:`int`: Maximum value allowed for :paramref:`~telegram.StarTransaction.nanostar_amount`
parameter of :class:`telegram.StarTransaction` and
:paramref:`~telegram.AffiliateInfo.nanostar_amount` parameter of
:class:`telegram.AffiliateInfo`.
.. versionadded:: 21.9
.. deprecated:: 22.1
This member will be replaced by :attr:`telegram.constants.NanostarLimit.MAX_AMOUNT`.
"""
class StickerFormat(StringEnum):
@@ -3152,10 +3178,13 @@ class PollLimit(IntEnum):
to the :paramref:`~telegram.Bot.send_poll.options` parameter of
:meth:`telegram.Bot.send_poll`.
"""
MAX_OPTION_NUMBER = 10
MAX_OPTION_NUMBER = 12
""":obj:`int`: Maximum number of strings passed in a :obj:`list`
to the :paramref:`~telegram.Bot.send_poll.options` parameter of
:meth:`telegram.Bot.send_poll`.
.. versionchanged:: 22.3
This value was changed from ``10`` to ``12`` in accordance to Bot API 9.1.
"""
MAX_EXPLANATION_LENGTH = 200
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
@@ -3210,6 +3239,11 @@ class UniqueGiftInfoOrigin(StringEnum):
""":obj:`str` gift upgraded"""
TRANSFER = "transfer"
""":obj:`str` gift transfered"""
RESALE = "resale"
""":obj:`str` gift bought from other users
.. versionadded:: 22.3
"""
class UpdateType(StringEnum):
+6 -4
View File
@@ -455,7 +455,9 @@ class Application(
.. versionadded:: 20.0
"""
# Unfortunately this needs to be here due to cyclical imports
from telegram.ext import ApplicationBuilder # pylint: disable=import-outside-toplevel
from telegram.ext import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel
ApplicationBuilder,
)
return ApplicationBuilder()
@@ -498,7 +500,7 @@ class Application(
# Unfortunately due to circular imports this has to be here
# pylint: disable=import-outside-toplevel
from telegram.ext._handlers.conversationhandler import ConversationHandler
from telegram.ext._handlers.conversationhandler import ConversationHandler # noqa: PLC0415
# Initialize the persistent conversation handlers with the stored states
for handler in itertools.chain.from_iterable(self.handlers.values()):
@@ -1369,7 +1371,7 @@ class Application(
"""
# Unfortunately due to circular imports this has to be here
# pylint: disable=import-outside-toplevel
from telegram.ext._handlers.conversationhandler import ConversationHandler
from telegram.ext._handlers.conversationhandler import ConversationHandler # noqa: PLC0415
if not isinstance(handler, BaseHandler):
raise TypeError(f"handler is not an instance of {BaseHandler.__name__}")
@@ -1735,7 +1737,7 @@ class Application(
# Unfortunately due to circular imports this has to be here
# pylint: disable=import-outside-toplevel
from telegram.ext._handlers.conversationhandler import PendingState
from telegram.ext._handlers.conversationhandler import PendingState # noqa: PLC0415
for name, (key, new_state) in itertools.chain.from_iterable(
zip(itertools.repeat(name), states_dict.pop_accessed_write_items())
+88
View File
@@ -62,6 +62,7 @@ from telegram import (
Gifts,
InlineKeyboardMarkup,
InlineQueryResultsButton,
InputChecklist,
InputMedia,
InputPaidMedia,
InputPollOption,
@@ -2639,6 +2640,72 @@ class ExtBot(Bot, Generic[RLARGS]):
allow_paid_broadcast=allow_paid_broadcast,
)
async def send_checklist(
self,
business_connection_id: str,
chat_id: int,
checklist: InputChecklist,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_effect_id: Optional[str] = None,
reply_parameters: Optional["ReplyParameters"] = None,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> Message:
return await super().send_checklist(
business_connection_id=business_connection_id,
chat_id=chat_id,
checklist=checklist,
disable_notification=disable_notification,
protect_content=protect_content,
message_effect_id=message_effect_id,
reply_parameters=reply_parameters,
reply_markup=reply_markup,
reply_to_message_id=reply_to_message_id,
allow_sending_without_reply=allow_sending_without_reply,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def edit_message_checklist(
self,
business_connection_id: str,
chat_id: int,
message_id: int,
checklist: InputChecklist,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> Message:
return await super().edit_message_checklist(
business_connection_id=business_connection_id,
chat_id=chat_id,
message_id=message_id,
checklist=checklist,
reply_markup=reply_markup,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def send_dice(
self,
chat_id: Union[int, str],
@@ -5057,6 +5124,24 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def get_my_star_balance(
self,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> StarAmount:
return await super().get_my_star_balance(
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
# updated camelCase aliases
getMe = get_me
sendMessage = send_message
@@ -5139,6 +5224,8 @@ class ExtBot(Bot, Generic[RLARGS]):
setPassportDataErrors = set_passport_data_errors
sendPoll = send_poll
stopPoll = stop_poll
sendChecklist = send_checklist
editMessageChecklist = edit_message_checklist
sendDice = send_dice
getMyCommands = get_my_commands
setMyCommands = set_my_commands
@@ -5210,3 +5297,4 @@ class ExtBot(Bot, Generic[RLARGS]):
verifyUser = verify_user
removeChatVerification = remove_chat_verification
removeUserVerification = remove_user_verification
getMyStarBalance = get_my_star_balance
@@ -298,7 +298,7 @@ class ConversationHandler(BaseHandler[Update, CCT, object]):
block: DVType[bool] = DEFAULT_TRUE,
):
# these imports need to be here because of circular import error otherwise
from telegram.ext import ( # pylint: disable=import-outside-toplevel
from telegram.ext import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
PollAnswerHandler,
PollHandler,
PreCheckoutQueryHandler,
+58 -2
View File
@@ -46,6 +46,7 @@ __all__ = (
"AUDIO",
"BOOST_ADDED",
"CAPTION",
"CHECKLIST",
"COMMAND",
"CONTACT",
"EFFECT_ID",
@@ -920,6 +921,20 @@ class ChatType: # A convenience namespace for Chat types.
"""Updates from supergroup."""
class _Checklist(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.checklist)
CHECKLIST = _Checklist(name="filters.CHECKLIST")
"""Messages that contain :attr:`telegram.Message.checklist`.
.. versionadded:: 22.3
"""
class Command(MessageFilter):
"""
Messages with a :attr:`telegram.MessageEntity.BOT_COMMAND`. By default, only allows
@@ -1918,7 +1933,10 @@ class StatusUpdate:
StatusUpdate.CHAT_BACKGROUND_SET.check_update(update)
or StatusUpdate.CHAT_CREATED.check_update(update)
or StatusUpdate.CHAT_SHARED.check_update(update)
or StatusUpdate.CHECKLIST_TASKS_ADDED.check_update(update)
or StatusUpdate.CHECKLIST_TASKS_DONE.check_update(update)
or StatusUpdate.CONNECTED_WEBSITE.check_update(update)
or StatusUpdate.DIRECT_MESSAGE_PRICE_CHANGED.check_update(update)
or StatusUpdate.DELETE_CHAT_PHOTO.check_update(update)
or StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update)
or StatusUpdate.FORUM_TOPIC_CREATED.check_update(update)
@@ -1988,6 +2006,30 @@ class StatusUpdate:
.. versionadded:: 20.1
"""
class _ChecklistTasksAdded(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.checklist_tasks_added)
CHECKLIST_TASKS_ADDED = _ChecklistTasksAdded(name="filters.StatusUpdate.CHECKLIST_TASKS_ADDED")
"""Messages that contain :attr:`telegram.Message.checklist_tasks_added`.
.. versionadded:: 22.3
"""
class _ChecklistTasksDone(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.checklist_tasks_done)
CHECKLIST_TASKS_DONE = _ChecklistTasksDone(name="filters.StatusUpdate.CHECKLIST_TASKS_DONE")
"""Messages that contain :attr:`telegram.Message.checklist_tasks_done`.
.. versionadded:: 22.3
"""
class _ConnectedWebsite(MessageFilter):
__slots__ = ()
@@ -1997,6 +2039,20 @@ class StatusUpdate:
CONNECTED_WEBSITE = _ConnectedWebsite(name="filters.StatusUpdate.CONNECTED_WEBSITE")
"""Messages that contain :attr:`telegram.Message.connected_website`."""
class _DirectMessagePriceChanged(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.direct_message_price_changed)
DIRECT_MESSAGE_PRICE_CHANGED = _DirectMessagePriceChanged(
name="filters.StatusUpdate.DIRECT_MESSAGE_PRICE_CHANGED"
)
"""Messages that contain :attr:`telegram.Message.direct_message_price_changed`.
.. versionadded:: 22.3
"""
class _DeleteChatPhoto(MessageFilter):
__slots__ = ()
@@ -2707,7 +2763,7 @@ class User(_ChatUserBaseFilter):
@user_ids.setter
def user_ids(self, user_id: SCT[int]) -> None:
self.chat_ids = user_id # type: ignore[assignment]
self.chat_ids = user_id
def add_user_ids(self, user_id: SCT[int]) -> None:
"""
@@ -2845,7 +2901,7 @@ class ViaBot(_ChatUserBaseFilter):
@bot_ids.setter
def bot_ids(self, bot_id: SCT[int]) -> None:
self.chat_ids = bot_id # type: ignore[assignment]
self.chat_ids = bot_id
def add_bot_ids(self, bot_id: SCT[int]) -> None:
"""
+4 -1
View File
@@ -125,7 +125,10 @@ def effective_message_type(entity: Union["Message", "Update"]) -> Optional[str]:
"""
# Importing on file-level yields cyclic Import Errors
from telegram import Message, Update # pylint: disable=import-outside-toplevel
from telegram import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
Message,
Update,
)
if isinstance(entity, Message):
message = entity
@@ -322,19 +322,15 @@ class TestTransactionPartnerUserWithoutRequest(TransactionPartnerTestBase):
assert json_dict["subscription_period"] == self.subscription_period.total_seconds()
assert json_dict["premium_subscription_duration"] == self.premium_subscription_duration
def test_transaction_type_is_required_argument(self):
with pytest.raises(TypeError, match="`transaction_type` is a required argument"):
TransactionPartnerUser(user=self.user)
def test_equality(self, transaction_partner_user):
a = transaction_partner_user
b = TransactionPartnerUser(
user=self.user,
transaction_type=self.transaction_type,
user=self.user,
)
c = TransactionPartnerUser(
user=User(id=1, is_bot=False, first_name="user", last_name="user"),
transaction_type=self.transaction_type,
user=User(id=1, is_bot=False, first_name="user", last_name="user"),
)
d = User(id=1, is_bot=False, first_name="user", last_name="user")
+1 -1
View File
@@ -65,7 +65,7 @@ class TestDatetime:
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed")
def test_localize_pytz(self):
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
import pytz
import pytz # noqa: PLC0415
tzinfo = pytz.timezone("Europe/Berlin")
localized_dt = tg_dtm.localize(dt, tzinfo)
+1 -1
View File
@@ -30,7 +30,7 @@ from .envvars import GITHUB_ACTIONS
# These bots are only able to talk in our test chats, so they are quite useless for other
# purposes than testing.
FALLBACKS = (
"W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc"
"W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRmdqXzhmNFVlV1hrb3VVUnpUZThhRUY0UGNFQkRxdlY0IiwgInBheW1lbnRfc"
"HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj"
"IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTgzODA"
"wNDU3NyIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgIm5hbWUiOiAiUFRCIHRlc3RzIGZh"
+2 -1
View File
@@ -8,6 +8,7 @@ from telegram import (
BotDescription,
BotName,
BotShortDescription,
BusinessBotRights,
BusinessConnection,
Chat,
ChatAdministratorRights,
@@ -69,8 +70,8 @@ _PREPARED_DUMMY_OBJECTS: dict[str, object] = {
id="123",
user_chat_id=123456,
date=_DUMMY_DATE,
can_reply=True,
is_enabled=True,
rights=BusinessBotRights(can_reply=True),
),
"Chat": Chat(id=123456, type="dummy_type"),
"ChatAdministratorRights": ChatAdministratorRights.all_rights(),
+2 -2
View File
@@ -80,7 +80,7 @@ def caller_func():
symlink_to(symlink_file, temp_file)
sys.path.append(tmp_path.as_posix())
from caller_link import caller_func
from caller_link import caller_func # noqa: PLC0415
frame = caller_func()
assert was_called_by(frame, temp_file)
@@ -111,7 +111,7 @@ def outer_func():
symlink_to(symlink_file2, temp_file2)
sys.path.append(tmp_path.as_posix())
from outer_link import outer_func
from outer_link import outer_func # noqa: PLC0415
frame = outer_func()
assert was_called_by(frame, temp_file2)
+2 -1
View File
@@ -23,6 +23,7 @@ import pytest
from telegram import (
Bot,
BusinessBotRights,
BusinessConnection,
CallbackQuery,
Chat,
@@ -81,8 +82,8 @@ def business_connection(bot):
user_chat_id=1,
user=User(1, "name", username="user_a", is_bot=False),
date=dtm.datetime.now(tz=UTC),
can_reply=True,
is_enabled=True,
rights=BusinessBotRights(can_reply=True),
)
bc.set_bot(bot)
return bc
+1 -2
View File
@@ -20,6 +20,7 @@
import asyncio
import functools
import logging
from copy import copy
from pathlib import Path
from warnings import filterwarnings
@@ -307,8 +308,6 @@ class TestConversationHandler:
)
def test_repr_with_truncation(self):
from copy import copy
states = copy(self.drinking_states)
# there are exactly 3 drinking states. adding one more to make sure it's truncated
states["extra_to_be_truncated"] = [CommandHandler("foo", self.start)]
+1 -1
View File
@@ -42,7 +42,7 @@ class TestDefaults:
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed")
def test_pytz_deprecation(self, recwarn):
import pytz
import pytz # noqa: PLC0415
with pytest.warns(PTBDeprecationWarning, match="pytz") as record:
Defaults(tzinfo=pytz.timezone("Europe/Berlin"))
+23 -1
View File
@@ -1116,7 +1116,22 @@ class TestFilters:
assert filters.StatusUpdate.PAID_MESSAGE_PRICE_CHANGED.check_update(update)
update.message.paid_message_price_changed = None
def test_filters_forwarded(self, update, message_origin_user):
update.message.direct_message_price_changed = "direct_message_price_changed"
assert filters.StatusUpdate.ALL.check_update(update)
assert filters.StatusUpdate.DIRECT_MESSAGE_PRICE_CHANGED.check_update(update)
update.message.direct_message_price_changed = None
update.message.checklist_tasks_added = "checklist_tasks_added"
assert filters.StatusUpdate.ALL.check_update(update)
assert filters.StatusUpdate.CHECKLIST_TASKS_ADDED.check_update(update)
update.message.checklist_tasks_added = None
update.message.checklist_tasks_done = "checklist_tasks_done"
assert filters.StatusUpdate.ALL.check_update(update)
assert filters.StatusUpdate.CHECKLIST_TASKS_DONE.check_update(update)
update.message.checklist_tasks_done = None
def test_filters_forwarded(self, update):
assert filters.FORWARDED.check_update(update)
update.message.forward_origin = MessageOriginHiddenUser(dtm.datetime.utcnow(), 1)
assert filters.FORWARDED.check_update(update)
@@ -2792,3 +2807,10 @@ class TestFilters:
update.message.sender_boost_count = "test"
assert filters.SENDER_BOOST_COUNT.check_update(update)
assert str(filters.SENDER_BOOST_COUNT) == "filters.SENDER_BOOST_COUNT"
def test_filters_checklist(self, update):
assert not filters.CHECKLIST.check_update(update)
update.message.checklist = "test"
assert filters.CHECKLIST.check_update(update)
assert str(filters.CHECKLIST) == "filters.CHECKLIST"
+17
View File
@@ -79,6 +79,7 @@ from telegram import (
User,
WebAppInfo,
)
from telegram._payment.stars.staramount import StarAmount
from telegram._utils.datetime import UTC, from_timestamp, localize, to_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.strings import to_camel_case
@@ -2574,6 +2575,17 @@ class TestBotWithoutRequest:
await offline_bot.remove_chat_verification(1234)
async def test_get_my_star_balance(self, offline_bot, monkeypatch):
sa = StarAmount(1000).to_json()
async def do_request(url, request_data: RequestData, *args, **kwargs):
assert not request_data.parameters
return 200, f'{{"ok": true, "result": {sa}}}'.encode()
monkeypatch.setattr(offline_bot.request, "do_request", do_request)
obj = await offline_bot.get_my_star_balance()
assert isinstance(obj, StarAmount)
class TestBotWithRequest:
"""
@@ -4540,3 +4552,8 @@ class TestBotWithRequest:
assert edited_link.name == "sub_name_2"
assert sub_link.subscription_period == 2592000
assert sub_link.subscription_price == 13
async def test_get_my_star_balance(self, bot):
balance = await bot.get_my_star_balance()
assert isinstance(balance, StarAmount)
assert balance.amount == 0
-35
View File
@@ -34,7 +34,6 @@ from telegram import (
)
from telegram._business import BusinessBotRights
from telegram._utils.datetime import UTC, to_timestamp
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.slots import mro_slots
@@ -101,7 +100,6 @@ def business_connection(business_bot_rights):
BusinessTestBase.user,
BusinessTestBase.user_chat_id,
BusinessTestBase.date,
BusinessTestBase.can_reply,
BusinessTestBase.is_enabled,
rights=business_bot_rights,
)
@@ -246,7 +244,6 @@ class TestBusinessConnectionWithoutRequest(BusinessTestBase):
"user": self.user.to_dict(),
"user_chat_id": self.user_chat_id,
"date": to_timestamp(self.date),
"can_reply": self.can_reply,
"is_enabled": self.is_enabled,
"rights": business_bot_rights.to_dict(),
}
@@ -255,7 +252,6 @@ class TestBusinessConnectionWithoutRequest(BusinessTestBase):
assert bc.user == self.user
assert bc.user_chat_id == self.user_chat_id
assert bc.date == self.date
assert bc.can_reply == self.can_reply
assert bc.is_enabled == self.is_enabled
assert bc.rights == business_bot_rights
assert bc.api_kwargs == {}
@@ -267,7 +263,6 @@ class TestBusinessConnectionWithoutRequest(BusinessTestBase):
"user": self.user.to_dict(),
"user_chat_id": self.user_chat_id,
"date": to_timestamp(self.date),
"can_reply": self.can_reply,
"is_enabled": self.is_enabled,
"rights": business_bot_rights.to_dict(),
}
@@ -299,7 +294,6 @@ class TestBusinessConnectionWithoutRequest(BusinessTestBase):
self.user,
self.user_chat_id,
self.date,
self.can_reply,
self.is_enabled,
rights=business_bot_rights,
)
@@ -308,7 +302,6 @@ class TestBusinessConnectionWithoutRequest(BusinessTestBase):
self.user,
self.user_chat_id,
self.date,
self.can_reply,
self.is_enabled,
rights=business_bot_rights,
)
@@ -317,7 +310,6 @@ class TestBusinessConnectionWithoutRequest(BusinessTestBase):
self.user,
self.user_chat_id,
self.date,
self.can_reply,
self.is_enabled,
rights=business_bot_rights,
)
@@ -326,7 +318,6 @@ class TestBusinessConnectionWithoutRequest(BusinessTestBase):
self.user,
self.user_chat_id,
self.date,
self.can_reply,
self.is_enabled,
rights=BusinessBotRights(),
)
@@ -340,32 +331,6 @@ class TestBusinessConnectionWithoutRequest(BusinessTestBase):
assert bc1 != bc4
assert hash(bc1) != hash(bc4)
def test_can_reply_argument_property_deprecation(self, business_connection):
with pytest.warns(PTBDeprecationWarning, match=r"9\.0.*can\_reply") as record:
assert BusinessConnection(
id=self.id_,
user=self.user,
user_chat_id=self.user_chat_id,
date=self.date,
can_reply=True,
is_enabled=self.is_enabled,
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
with pytest.warns(PTBDeprecationWarning, match=r"9\.0.*can\_reply") as record:
assert business_connection.can_reply is self.can_reply
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
def test_is_enabled_remains_required(self):
with pytest.raises(TypeError):
BusinessConnection(
id=self.id_, user=self.user, user_chat_id=self.user_chat_id, date=self.date
)
class TestBusinessMessagesDeleted(BusinessTestBase):
def test_slots(self, business_messages_deleted):
+159 -2
View File
@@ -21,6 +21,7 @@ import datetime as dtm
import pytest
from telegram import (
BusinessBotRights,
BusinessConnection,
Chat,
InputProfilePhotoStatic,
@@ -35,7 +36,12 @@ from telegram import (
from telegram._files._inputstorycontent import InputStoryContentVideo
from telegram._files.sticker import Sticker
from telegram._gifts import AcceptedGiftTypes, Gift
from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inputchecklist import InputChecklist, InputChecklistTask
from telegram._message import Message
from telegram._ownedgift import OwnedGiftRegular, OwnedGifts
from telegram._reply import ReplyParameters
from telegram._utils.datetime import UTC
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram.constants import InputProfilePhotoType, InputStoryContentType
@@ -51,10 +57,15 @@ class TestBusinessMethodsWithoutRequest(BusinessMethodsTestBase):
user = User(1, "first", False)
user_chat_id = 1
date = dtm.datetime.utcnow()
can_reply = True
rights = BusinessBotRights(can_reply=True)
is_enabled = True
bc = BusinessConnection(
self.bci, user, user_chat_id, date, can_reply, is_enabled
self.bci,
user,
user_chat_id,
date,
is_enabled,
rights=rights,
).to_json()
async def do_request(*args, **kwargs):
@@ -632,3 +643,149 @@ class TestBusinessMethodsWithoutRequest(BusinessMethodsTestBase):
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.delete_story(business_connection_id=self.bci, story_id=story_id)
async def test_send_checklist_all_args(self, offline_bot, monkeypatch):
chat_id = 123
checklist = InputChecklist(
title="My Checklist",
tasks=[InputChecklistTask(1, "Task 1"), InputChecklistTask(2, "Task 2")],
)
disable_notification = True
protect_content = False
message_effect_id = 42
reply_parameters = ReplyParameters(23, chat_id, allow_sending_without_reply=True)
reply_markup = InlineKeyboardMarkup(
[[InlineKeyboardButton(text="test", callback_data="test2")]]
)
json_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="test").to_json()
async def make_assertions(*args, **kwargs):
params = kwargs.get("request_data").parameters
assert params.get("business_connection_id") == self.bci
assert params.get("chat_id") == chat_id
assert params.get("checklist") == checklist.to_dict()
assert params.get("disable_notification") is disable_notification
assert params.get("protect_content") is protect_content
assert params.get("message_effect_id") == message_effect_id
assert params.get("reply_parameters") == reply_parameters.to_dict()
assert params.get("reply_markup") == reply_markup.to_dict()
return 200, f'{{"ok": true, "result": {json_message}}}'.encode()
monkeypatch.setattr(offline_bot.request, "do_request", make_assertions)
obj = await offline_bot.send_checklist(
business_connection_id=self.bci,
chat_id=chat_id,
checklist=checklist,
disable_notification=disable_notification,
protect_content=protect_content,
message_effect_id=message_effect_id,
reply_parameters=reply_parameters,
reply_markup=reply_markup,
)
assert isinstance(obj, Message)
@pytest.mark.parametrize("default_bot", [{"disable_notification": True}], indirect=True)
@pytest.mark.parametrize(
("passed_value", "expected_value"),
[(DEFAULT_NONE, True), (False, False), (None, None)],
)
async def test_send_checklist_default_disable_notification(
self, default_bot, monkeypatch, passed_value, expected_value
):
async def make_assertion(url, request_data, *args, **kwargs):
assert request_data.parameters.get("disable_notification") is expected_value
return Message(1, dtm.datetime.now(), Chat(1, ""), text="test").to_dict()
monkeypatch.setattr(default_bot.request, "post", make_assertion)
kwargs = {
"business_connection_id": self.bci,
"chat_id": 123,
"checklist": InputChecklist(
title="My Checklist",
tasks=[InputChecklistTask(1, "Task 1")],
),
}
if passed_value is not DEFAULT_NONE:
kwargs["disable_notification"] = passed_value
await default_bot.send_checklist(**kwargs)
@pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True)
@pytest.mark.parametrize(
("passed_value", "expected_value"),
[(DEFAULT_NONE, True), (False, False), (None, None)],
)
async def test_send_checklist_default_protect_content(
self, default_bot, monkeypatch, passed_value, expected_value
):
async def make_assertion(url, request_data, *args, **kwargs):
assert request_data.parameters.get("protect_content") is expected_value
return Message(1, dtm.datetime.now(), Chat(1, ""), text="test").to_dict()
monkeypatch.setattr(default_bot.request, "post", make_assertion)
kwargs = {
"business_connection_id": self.bci,
"chat_id": 123,
"checklist": InputChecklist(
title="My Checklist",
tasks=[InputChecklistTask(1, "Task 1")],
),
}
if passed_value is not DEFAULT_NONE:
kwargs["protect_content"] = passed_value
await default_bot.send_checklist(**kwargs)
async def test_send_checklist_mutually_exclusive_reply_parameters(self, offline_bot):
"""Test that reply_to_message_id and allow_sending_without_reply are mutually exclusive
with reply_parameters."""
with pytest.raises(ValueError, match="`reply_to_message_id` and"):
await offline_bot.send_checklist(
self.bci,
123,
InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")]),
reply_to_message_id=1,
reply_parameters=True,
)
with pytest.raises(ValueError, match="`allow_sending_without_reply` and"):
await offline_bot.send_checklist(
self.bci,
123,
InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")]),
allow_sending_without_reply=True,
reply_parameters=True,
)
async def test_edit_message_checklist_all_args(self, offline_bot, monkeypatch):
chat_id = 123
message_id = 45
checklist = InputChecklist(
title="My Checklist",
tasks=[InputChecklistTask(1, "Task 1"), InputChecklistTask(2, "Task 2")],
)
reply_markup = InlineKeyboardMarkup(
[[InlineKeyboardButton(text="test", callback_data="test2")]]
)
json_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="test").to_json()
async def make_assertions(*args, **kwargs):
params = kwargs.get("request_data").parameters
assert params.get("business_connection_id") == self.bci
assert params.get("chat_id") == chat_id
assert params.get("message_id") == message_id
assert params.get("checklist") == checklist.to_dict()
assert params.get("reply_markup") == reply_markup.to_dict()
return 200, f'{{"ok": true, "result": {json_message}}}'.encode()
monkeypatch.setattr(offline_bot.request, "do_request", make_assertions)
obj = await offline_bot.edit_message_checklist(
business_connection_id=self.bci,
chat_id=chat_id,
message_id=message_id,
checklist=checklist,
reply_markup=reply_markup,
)
assert isinstance(obj, Message)
+48 -1
View File
@@ -21,7 +21,17 @@ import datetime as dtm
import pytest
from telegram import Audio, Bot, CallbackQuery, Chat, InaccessibleMessage, Message, User
from telegram import (
Audio,
Bot,
CallbackQuery,
Chat,
InaccessibleMessage,
InputChecklist,
InputChecklistTask,
Message,
User,
)
from tests.auxil.bot_method_checks import (
check_defaults_handling,
check_shortcut_call,
@@ -230,6 +240,43 @@ class TestCallbackQueryWithoutRequest(CallbackQueryTestBase):
assert await callback_query.edit_message_caption(caption="new caption")
assert await callback_query.edit_message_caption("new caption")
async def test_edit_message_checklist(self, monkeypatch, callback_query):
checklist = InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")])
if isinstance(callback_query.message, InaccessibleMessage):
with pytest.raises(TypeError, match="inaccessible message"):
await callback_query.edit_message_checklist(checklist)
return
if callback_query.inline_message_id:
pytest.skip("Can't edit inline messages")
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == callback_query.message.chat_id
message_id = kwargs["message_id"] == callback_query.message.message_id
caption = kwargs["checklist"] == checklist
return chat_id and message_id and caption
assert check_shortcut_signature(
CallbackQuery.edit_message_checklist,
Bot.edit_message_checklist,
["chat_id", "message_id", "business_connection_id"],
[],
)
assert await check_shortcut_call(
callback_query.edit_message_checklist,
callback_query.get_bot(),
"edit_message_checklist",
shortcut_kwargs=["chat_id", "message_id", "business_connection_id"],
)
assert await check_defaults_handling(
callback_query.edit_message_checklist, callback_query.get_bot()
)
monkeypatch.setattr(callback_query.get_bot(), "edit_message_checklist", make_assertion)
assert await callback_query.edit_message_checklist(checklist=checklist)
assert await callback_query.edit_message_checklist(checklist)
async def test_edit_message_reply_markup(self, monkeypatch, callback_query):
if isinstance(callback_query.message, InaccessibleMessage):
with pytest.raises(TypeError, match="inaccessible message"):
+26 -1
View File
@@ -20,7 +20,15 @@
import pytest
from telegram import Bot, Chat, ChatPermissions, ReactionTypeEmoji, User
from telegram import (
Bot,
Chat,
ChatPermissions,
InputChecklist,
InputChecklistTask,
ReactionTypeEmoji,
User,
)
from telegram.constants import ChatAction, ChatType, ReactionEmoji
from telegram.helpers import escape_markdown
from tests.auxil.bot_method_checks import (
@@ -579,6 +587,23 @@ class TestChatWithoutRequest(ChatTestBase):
monkeypatch.setattr(chat.get_bot(), "send_dice", make_assertion)
assert await chat.send_dice(emoji="test_dice")
async def test_instance_method_send_checklist(self, monkeypatch, chat):
checklist = InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")])
async def make_assertion(*_, **kwargs):
return (
kwargs["chat_id"] == chat.id
and kwargs["business_connection_id"] == "123"
and kwargs["checklist"] == checklist
)
assert check_shortcut_signature(Chat.send_checklist, Bot.send_checklist, ["chat_id"], [])
assert await check_shortcut_call(chat.send_checklist, chat.get_bot(), "send_checklist")
assert await check_defaults_handling(chat.send_checklist, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "send_checklist", make_assertion)
assert await chat.send_checklist("123", checklist)
async def test_instance_method_send_game(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return kwargs["chat_id"] == chat.id and kwargs["game_short_name"] == "test_game"
-31
View File
@@ -145,7 +145,6 @@ class ChatFullInfoTestBase:
first_name = "first_name"
last_name = "last_name"
can_send_paid_media = True
can_send_gift = True
accepted_gift_types = AcceptedGiftTypes(True, True, True, True)
@@ -166,7 +165,6 @@ class TestChatFullInfoWithoutRequest(ChatFullInfoTestBase):
"max_reaction_count": self.max_reaction_count,
"username": self.username,
"accepted_gift_types": self.accepted_gift_types.to_dict(),
"can_send_gift": self.can_send_gift,
"sticker_set_name": self.sticker_set_name,
"can_set_sticker_set": self.can_set_sticker_set,
"permissions": self.permissions.to_dict(),
@@ -334,35 +332,6 @@ class TestChatFullInfoWithoutRequest(ChatFullInfoTestBase):
assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count
def test_accepted_gift_types_is_required_argument(self):
with pytest.raises(TypeError, match="`accepted_gift_type` is a required argument"):
ChatFullInfo(
id=123,
type=Chat.PRIVATE,
accent_color_id=1,
max_reaction_count=2,
can_send_gift=True,
)
def test_can_send_gift_deprecation_warning(self):
with pytest.warns(
PTBDeprecationWarning,
match="'can_send_gift' was replaced by 'accepted_gift_types' in Bot API 9.0",
):
chat_full_info = ChatFullInfo(
id=123,
type=Chat.PRIVATE,
accent_color_id=1,
max_reaction_count=2,
accepted_gift_types=self.accepted_gift_types,
can_send_gift=self.can_send_gift,
)
with pytest.warns(
PTBDeprecationWarning,
match="Bot API 9.0 renamed the attribute 'can_send_gift' to 'accepted_gift_types'",
):
chat_full_info.can_send_gift
def test_time_period_properties(self, PTB_TIMEDELTA, chat_full_info):
cfi = chat_full_info
if PTB_TIMEDELTA:
+439
View File
@@ -0,0 +1,439 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import datetime as dtm
import pytest
from telegram import (
Checklist,
ChecklistTask,
ChecklistTasksAdded,
ChecklistTasksDone,
Dice,
MessageEntity,
User,
)
from telegram._utils.datetime import UTC, to_timestamp
from telegram.constants import ZERO_DATE
from tests.auxil.build_messages import make_message
from tests.auxil.slots import mro_slots
class ChecklistTaskTestBase:
id = 42
text = "here is a text"
text_entities = [
MessageEntity(type="bold", offset=0, length=4),
MessageEntity(type="italic", offset=5, length=2),
]
completed_by_user = User(id=1, first_name="Test", last_name="User", is_bot=False)
completion_date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
@pytest.fixture(scope="module")
def checklist_task():
return ChecklistTask(
id=ChecklistTaskTestBase.id,
text=ChecklistTaskTestBase.text,
text_entities=ChecklistTaskTestBase.text_entities,
completed_by_user=ChecklistTaskTestBase.completed_by_user,
completion_date=ChecklistTaskTestBase.completion_date,
)
class TestChecklistTaskWithoutRequest(ChecklistTaskTestBase):
def test_slot_behaviour(self, checklist_task):
for attr in checklist_task.__slots__:
assert getattr(checklist_task, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(checklist_task)) == len(
set(mro_slots(checklist_task))
), "duplicate slot"
def test_to_dict(self, checklist_task):
clt_dict = checklist_task.to_dict()
assert isinstance(clt_dict, dict)
assert clt_dict["id"] == self.id
assert clt_dict["text"] == self.text
assert clt_dict["text_entities"] == [entity.to_dict() for entity in self.text_entities]
assert clt_dict["completed_by_user"] == self.completed_by_user.to_dict()
assert clt_dict["completion_date"] == to_timestamp(self.completion_date)
def test_de_json(self, offline_bot):
json_dict = {
"id": self.id,
"text": self.text,
"text_entities": [entity.to_dict() for entity in self.text_entities],
"completed_by_user": self.completed_by_user.to_dict(),
"completion_date": to_timestamp(self.completion_date),
}
clt = ChecklistTask.de_json(json_dict, offline_bot)
assert isinstance(clt, ChecklistTask)
assert clt.id == self.id
assert clt.text == self.text
assert clt.text_entities == tuple(self.text_entities)
assert clt.completed_by_user == self.completed_by_user
assert clt.completion_date == self.completion_date
assert clt.api_kwargs == {}
def test_de_json_required_fields(self, offline_bot):
json_dict = {
"id": self.id,
"text": self.text,
}
clt = ChecklistTask.de_json(json_dict, offline_bot)
assert isinstance(clt, ChecklistTask)
assert clt.id == self.id
assert clt.text == self.text
assert clt.text_entities == ()
assert clt.completed_by_user is None
assert clt.completion_date is None
assert clt.api_kwargs == {}
def test_de_json_localization(self, offline_bot, raw_bot, tz_bot):
json_dict = {
"id": self.id,
"text": self.text,
"completion_date": to_timestamp(self.completion_date),
}
clt_bot = ChecklistTask.de_json(json_dict, offline_bot)
clt_bot_raw = ChecklistTask.de_json(json_dict, raw_bot)
clt_bot_tz = ChecklistTask.de_json(json_dict, tz_bot)
# comparing utcoffsets because comparing tzinfo objects is not reliable
completion_date_offset = clt_bot_tz.completion_date.utcoffset()
completion_date_offset_tz = tz_bot.defaults.tzinfo.utcoffset(
clt_bot_tz.completion_date.replace(tzinfo=None)
)
assert clt_bot.completion_date.tzinfo == UTC
assert clt_bot_raw.completion_date.tzinfo == UTC
assert completion_date_offset_tz == completion_date_offset
@pytest.mark.parametrize(
("completion_date", "expected"),
[
(None, None),
(0, ZERO_DATE),
(1735689600, dtm.datetime(2025, 1, 1, tzinfo=UTC)),
],
)
def test_de_json_completion_date(self, offline_bot, completion_date, expected):
json_dict = {
"id": self.id,
"text": self.text,
"completion_date": completion_date,
}
clt = ChecklistTask.de_json(json_dict, offline_bot)
assert isinstance(clt, ChecklistTask)
assert clt.completion_date == expected
def test_parse_entity(self, checklist_task):
assert checklist_task.parse_entity(checklist_task.text_entities[0]) == "here"
def test_parse_entities(self, checklist_task):
assert checklist_task.parse_entities(MessageEntity.BOLD) == {
checklist_task.text_entities[0]: "here"
}
assert checklist_task.parse_entities() == {
checklist_task.text_entities[0]: "here",
checklist_task.text_entities[1]: "is",
}
def test_equality(self, checklist_task):
clt1 = checklist_task
clt2 = ChecklistTask(
id=self.id,
text="other text",
)
clt3 = ChecklistTask(
id=self.id + 1,
text=self.text,
)
clt4 = Dice(value=1, emoji="🎲")
assert clt1 == clt2
assert hash(clt1) == hash(clt2)
assert clt1 != clt3
assert hash(clt1) != hash(clt3)
assert clt1 != clt4
assert hash(clt1) != hash(clt4)
class ChecklistTestBase:
title = "Checklist Title"
title_entities = [
MessageEntity(type="bold", offset=0, length=9),
MessageEntity(type="italic", offset=10, length=5),
]
tasks = [
ChecklistTask(
id=1,
text="Task 1",
),
ChecklistTask(
id=2,
text="Task 2",
),
]
others_can_add_tasks = True
others_can_mark_tasks_as_done = False
@pytest.fixture(scope="module")
def checklist():
return Checklist(
title=ChecklistTestBase.title,
title_entities=ChecklistTestBase.title_entities,
tasks=ChecklistTestBase.tasks,
others_can_add_tasks=ChecklistTestBase.others_can_add_tasks,
others_can_mark_tasks_as_done=ChecklistTestBase.others_can_mark_tasks_as_done,
)
class TestChecklistWithoutRequest(ChecklistTestBase):
def test_slot_behaviour(self, checklist):
for attr in checklist.__slots__:
assert getattr(checklist, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(checklist)) == len(set(mro_slots(checklist))), "duplicate slot"
def test_to_dict(self, checklist):
cl_dict = checklist.to_dict()
assert isinstance(cl_dict, dict)
assert cl_dict["title"] == self.title
assert cl_dict["title_entities"] == [entity.to_dict() for entity in self.title_entities]
assert cl_dict["tasks"] == [task.to_dict() for task in self.tasks]
assert cl_dict["others_can_add_tasks"] is self.others_can_add_tasks
assert cl_dict["others_can_mark_tasks_as_done"] is self.others_can_mark_tasks_as_done
def test_de_json(self, offline_bot):
json_dict = {
"title": self.title,
"title_entities": [entity.to_dict() for entity in self.title_entities],
"tasks": [task.to_dict() for task in self.tasks],
"others_can_add_tasks": self.others_can_add_tasks,
"others_can_mark_tasks_as_done": self.others_can_mark_tasks_as_done,
}
cl = Checklist.de_json(json_dict, offline_bot)
assert isinstance(cl, Checklist)
assert cl.title == self.title
assert cl.title_entities == tuple(self.title_entities)
assert cl.tasks == tuple(self.tasks)
assert cl.others_can_add_tasks is self.others_can_add_tasks
assert cl.others_can_mark_tasks_as_done is self.others_can_mark_tasks_as_done
assert cl.api_kwargs == {}
def test_de_json_required_fields(self, offline_bot):
json_dict = {
"title": self.title,
"tasks": [task.to_dict() for task in self.tasks],
}
cl = Checklist.de_json(json_dict, offline_bot)
assert isinstance(cl, Checklist)
assert cl.title == self.title
assert cl.title_entities == ()
assert cl.tasks == tuple(self.tasks)
assert not cl.others_can_add_tasks
assert not cl.others_can_mark_tasks_as_done
def test_parse_entity(self, checklist):
assert checklist.parse_entity(checklist.title_entities[0]) == "Checklist"
assert checklist.parse_entity(checklist.title_entities[1]) == "Title"
def test_parse_entities(self, checklist):
assert checklist.parse_entities(MessageEntity.BOLD) == {
checklist.title_entities[0]: "Checklist"
}
assert checklist.parse_entities() == {
checklist.title_entities[0]: "Checklist",
checklist.title_entities[1]: "Title",
}
def test_equality(self, checklist, checklist_task):
cl1 = checklist
cl2 = Checklist(
title=self.title + " other",
tasks=[ChecklistTask(id=1, text="something"), ChecklistTask(id=2, text="something")],
)
cl3 = Checklist(
title=self.title + " other",
tasks=[ChecklistTask(id=42, text="Task 2")],
)
cl4 = checklist_task
assert cl1 == cl2
assert hash(cl1) == hash(cl2)
assert cl1 != cl3
assert hash(cl1) != hash(cl3)
assert cl1 != cl4
assert hash(cl1) != hash(cl4)
class ChecklistTasksDoneTestBase:
checklist_message = make_message("Checklist message")
marked_as_done_task_ids = [1, 2, 3]
marked_as_not_done_task_ids = [4, 5]
@pytest.fixture(scope="module")
def checklist_tasks_done():
return ChecklistTasksDone(
checklist_message=ChecklistTasksDoneTestBase.checklist_message,
marked_as_done_task_ids=ChecklistTasksDoneTestBase.marked_as_done_task_ids,
marked_as_not_done_task_ids=ChecklistTasksDoneTestBase.marked_as_not_done_task_ids,
)
class TestChecklistTasksDoneWithoutRequest(ChecklistTasksDoneTestBase):
def test_slot_behaviour(self, checklist_tasks_done):
for attr in checklist_tasks_done.__slots__:
assert getattr(checklist_tasks_done, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(checklist_tasks_done)) == len(
set(mro_slots(checklist_tasks_done))
), "duplicate slot"
def test_to_dict(self, checklist_tasks_done):
cltd_dict = checklist_tasks_done.to_dict()
assert isinstance(cltd_dict, dict)
assert cltd_dict["checklist_message"] == self.checklist_message.to_dict()
assert cltd_dict["marked_as_done_task_ids"] == self.marked_as_done_task_ids
assert cltd_dict["marked_as_not_done_task_ids"] == self.marked_as_not_done_task_ids
def test_de_json(self, offline_bot):
json_dict = {
"checklist_message": self.checklist_message.to_dict(),
"marked_as_done_task_ids": self.marked_as_done_task_ids,
"marked_as_not_done_task_ids": self.marked_as_not_done_task_ids,
}
cltd = ChecklistTasksDone.de_json(json_dict, offline_bot)
assert isinstance(cltd, ChecklistTasksDone)
assert cltd.checklist_message == self.checklist_message
assert cltd.marked_as_done_task_ids == tuple(self.marked_as_done_task_ids)
assert cltd.marked_as_not_done_task_ids == tuple(self.marked_as_not_done_task_ids)
assert cltd.api_kwargs == {}
def test_de_json_required_fields(self, offline_bot):
cltd = ChecklistTasksDone.de_json({}, offline_bot)
assert isinstance(cltd, ChecklistTasksDone)
assert cltd.checklist_message is None
assert cltd.marked_as_done_task_ids == ()
assert cltd.marked_as_not_done_task_ids == ()
assert cltd.api_kwargs == {}
def test_equality(self, checklist_tasks_done):
cltd1 = checklist_tasks_done
cltd2 = ChecklistTasksDone(
checklist_message=None,
marked_as_done_task_ids=[1, 2, 3],
marked_as_not_done_task_ids=[4, 5],
)
cltd3 = ChecklistTasksDone(
checklist_message=make_message("Checklist message"),
marked_as_done_task_ids=[1, 2, 3],
)
cltd4 = make_message("Not a checklist tasks done")
assert cltd1 == cltd2
assert hash(cltd1) == hash(cltd2)
assert cltd1 != cltd3
assert hash(cltd1) != hash(cltd3)
assert cltd1 != cltd4
assert hash(cltd1) != hash(cltd4)
class ChecklistTasksAddedTestBase:
checklist_message = make_message("Checklist message")
tasks = [
ChecklistTask(id=1, text="Task 1"),
ChecklistTask(id=2, text="Task 2"),
ChecklistTask(id=3, text="Task 3"),
]
@pytest.fixture(scope="module")
def checklist_tasks_added():
return ChecklistTasksAdded(
checklist_message=ChecklistTasksAddedTestBase.checklist_message,
tasks=ChecklistTasksAddedTestBase.tasks,
)
class TestChecklistTasksAddedWithoutRequest(ChecklistTasksAddedTestBase):
def test_slot_behaviour(self, checklist_tasks_added):
for attr in checklist_tasks_added.__slots__:
assert getattr(checklist_tasks_added, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(checklist_tasks_added)) == len(
set(mro_slots(checklist_tasks_added))
), "duplicate slot"
def test_to_dict(self, checklist_tasks_added):
clta_dict = checklist_tasks_added.to_dict()
assert isinstance(clta_dict, dict)
assert clta_dict["checklist_message"] == self.checklist_message.to_dict()
assert clta_dict["tasks"] == [task.to_dict() for task in self.tasks]
def test_de_json(self, offline_bot):
json_dict = {
"checklist_message": self.checklist_message.to_dict(),
"tasks": [task.to_dict() for task in self.tasks],
}
clta = ChecklistTasksAdded.de_json(json_dict, offline_bot)
assert isinstance(clta, ChecklistTasksAdded)
assert clta.checklist_message == self.checklist_message
assert clta.tasks == tuple(self.tasks)
assert clta.api_kwargs == {}
def test_de_json_required_fields(self, offline_bot):
clta = ChecklistTasksAdded.de_json(
{"tasks": [task.to_dict() for task in self.tasks]}, offline_bot
)
assert isinstance(clta, ChecklistTasksAdded)
assert clta.checklist_message is None
assert clta.tasks == tuple(self.tasks)
assert clta.api_kwargs == {}
def test_equality(self, checklist_tasks_added):
clta1 = checklist_tasks_added
clta2 = ChecklistTasksAdded(
checklist_message=None,
tasks=[
ChecklistTask(id=1, text="Other Task 1"),
ChecklistTask(id=2, text="Other Task 2"),
ChecklistTask(id=3, text="Other Task 3"),
],
)
clta3 = ChecklistTasksAdded(
checklist_message=make_message("Checklist message"),
tasks=[ChecklistTask(id=1, text="Task 1")],
)
clta4 = make_message("Not a checklist tasks added")
assert clta1 == clta2
assert hash(clta1) == hash(clta2)
assert clta1 != clta3
assert hash(clta1) != hash(clta3)
assert clta1 != clta4
assert hash(clta1) != hash(clta4)
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object for testing a Direct Message Price."""
from typing import TYPE_CHECKING
import pytest
from telegram import DirectMessagePriceChanged, User
from tests.auxil.slots import mro_slots
if TYPE_CHECKING:
from telegram._utils.types import JSONDict
@pytest.fixture
def direct_message_price_changed():
return DirectMessagePriceChanged(
are_direct_messages_enabled=DirectMessagePriceChangedTestBase.are_direct_messages_enabled,
direct_message_star_count=DirectMessagePriceChangedTestBase.direct_message_star_count,
)
class DirectMessagePriceChangedTestBase:
are_direct_messages_enabled: bool = True
direct_message_star_count: int = 100
class TestDirectMessagePriceChangedWithoutRequest(DirectMessagePriceChangedTestBase):
def test_slot_behaviour(self, direct_message_price_changed):
action = direct_message_price_changed
for attr in action.__slots__:
assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict: JSONDict = {
"are_direct_messages_enabled": self.are_direct_messages_enabled,
"direct_message_star_count": self.direct_message_star_count,
}
dmpc = DirectMessagePriceChanged.de_json(json_dict, offline_bot)
assert dmpc.api_kwargs == {}
assert dmpc.are_direct_messages_enabled == self.are_direct_messages_enabled
assert dmpc.direct_message_star_count == self.direct_message_star_count
def test_to_dict(self, direct_message_price_changed):
dmpc_dict = direct_message_price_changed.to_dict()
assert dmpc_dict["are_direct_messages_enabled"] == self.are_direct_messages_enabled
assert dmpc_dict["direct_message_star_count"] == self.direct_message_star_count
def test_equality(self, direct_message_price_changed):
dmpc1 = direct_message_price_changed
dmpc2 = DirectMessagePriceChanged(
are_direct_messages_enabled=self.are_direct_messages_enabled,
direct_message_star_count=self.direct_message_star_count,
)
assert dmpc1 == dmpc2
assert hash(dmpc1) == hash(dmpc2)
dmpc3 = DirectMessagePriceChanged(
are_direct_messages_enabled=False,
direct_message_star_count=self.direct_message_star_count,
)
assert dmpc1 != dmpc3
assert hash(dmpc1) != hash(dmpc3)
not_a_dmpc = User(id=1, first_name="wrong", is_bot=False)
assert dmpc1 != not_a_dmpc
assert hash(dmpc1) != hash(not_a_dmpc)
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from telegram import Dice, InputChecklist, InputChecklistTask, MessageEntity
from tests.auxil.slots import mro_slots
@pytest.fixture(scope="module")
def input_checklist_task():
return InputChecklistTask(
id=InputChecklistTaskTestBase.id,
text=InputChecklistTaskTestBase.text,
parse_mode=InputChecklistTaskTestBase.parse_mode,
text_entities=InputChecklistTaskTestBase.text_entities,
)
class InputChecklistTaskTestBase:
id = 1
text = "buy food"
parse_mode = "MarkdownV2"
text_entities = [
MessageEntity(type="bold", offset=0, length=3),
MessageEntity(type="italic", offset=4, length=4),
]
class TestInputChecklistTaskWithoutRequest(InputChecklistTaskTestBase):
def test_slot_behaviour(self, input_checklist_task):
for attr in input_checklist_task.__slots__:
assert getattr(input_checklist_task, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(input_checklist_task)) == len(
set(mro_slots(input_checklist_task))
), "duplicate slot"
def test_expected_values(self, input_checklist_task):
assert input_checklist_task.id == self.id
assert input_checklist_task.text == self.text
assert input_checklist_task.parse_mode == self.parse_mode
assert input_checklist_task.text_entities == tuple(self.text_entities)
def test_to_dict(self, input_checklist_task):
iclt_dict = input_checklist_task.to_dict()
assert isinstance(iclt_dict, dict)
assert iclt_dict["id"] == self.id
assert iclt_dict["text"] == self.text
assert iclt_dict["parse_mode"] == self.parse_mode
assert iclt_dict["text_entities"] == [entity.to_dict() for entity in self.text_entities]
# Test that default-value parameter `parse_mode` is handled correctly
input_checklist_task = InputChecklistTask(id=1, text="text")
iclt_dict = input_checklist_task.to_dict()
assert "parse_mode" not in iclt_dict
def test_equality(self, input_checklist_task):
a = input_checklist_task
b = InputChecklistTask(id=self.id, text=f"other {self.text}")
c = InputChecklistTask(id=self.id + 1, text=self.text)
d = Dice(value=1, emoji="🎲")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
@pytest.fixture(scope="module")
def input_checklist():
return InputChecklist(
title=InputChecklistTestBase.title,
tasks=InputChecklistTestBase.tasks,
parse_mode=InputChecklistTestBase.parse_mode,
title_entities=InputChecklistTestBase.title_entities,
others_can_add_tasks=InputChecklistTestBase.others_can_add_tasks,
others_can_mark_tasks_as_done=InputChecklistTestBase.others_can_mark_tasks_as_done,
)
class InputChecklistTestBase:
title = "test list"
tasks = [
InputChecklistTask(id=1, text="eat"),
InputChecklistTask(id=2, text="sleep"),
]
parse_mode = "MarkdownV2"
title_entities = [
MessageEntity(type="bold", offset=0, length=4),
MessageEntity(type="italic", offset=5, length=4),
]
others_can_add_tasks = True
others_can_mark_tasks_as_done = False
class TestInputChecklistWithoutRequest(InputChecklistTestBase):
def test_slot_behaviour(self, input_checklist):
for attr in input_checklist.__slots__:
assert getattr(input_checklist, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(input_checklist)) == len(
set(mro_slots(input_checklist))
), "duplicate slot"
def test_expected_values(self, input_checklist):
assert input_checklist.title == self.title
assert input_checklist.tasks == tuple(self.tasks)
assert input_checklist.parse_mode == self.parse_mode
assert input_checklist.title_entities == tuple(self.title_entities)
assert input_checklist.others_can_add_tasks == self.others_can_add_tasks
assert input_checklist.others_can_mark_tasks_as_done == self.others_can_mark_tasks_as_done
def test_to_dict(self, input_checklist):
icl_dict = input_checklist.to_dict()
assert isinstance(icl_dict, dict)
assert icl_dict["title"] == self.title
assert icl_dict["tasks"] == [task.to_dict() for task in self.tasks]
assert icl_dict["parse_mode"] == self.parse_mode
assert icl_dict["title_entities"] == [entity.to_dict() for entity in self.title_entities]
assert icl_dict["others_can_add_tasks"] == self.others_can_add_tasks
assert icl_dict["others_can_mark_tasks_as_done"] == self.others_can_mark_tasks_as_done
# Test that default-value parameter `parse_mode` is handled correctly
input_checklist = InputChecklist(title=self.title, tasks=self.tasks)
icl_dict = input_checklist.to_dict()
assert "parse_mode" not in icl_dict
def test_equality(self, input_checklist):
a = input_checklist
b = InputChecklist(
title=f"other {self.title}",
tasks=[InputChecklistTask(id=1, text="eat"), InputChecklistTask(id=2, text="sleep")],
)
c = InputChecklist(
title=self.title,
tasks=[InputChecklistTask(id=9, text="Other Task")],
)
d = Dice(value=1, emoji="🎲")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
+101 -9
View File
@@ -31,15 +31,24 @@ from telegram import (
ChatBackground,
ChatBoostAdded,
ChatShared,
Checklist,
ChecklistTask,
ChecklistTasksAdded,
ChecklistTasksDone,
Contact,
Dice,
DirectMessagePriceChanged,
Document,
ExternalReplyInfo,
Game,
Gift,
GiftInfo,
Giveaway,
GiveawayCompleted,
GiveawayCreated,
GiveawayWinners,
InputChecklist,
InputChecklistTask,
InputPaidMediaPhoto,
Invoice,
LinkPreviewOptions,
@@ -63,6 +72,12 @@ from telegram import (
Story,
SuccessfulPayment,
TextQuote,
UniqueGift,
UniqueGiftBackdrop,
UniqueGiftBackdropColors,
UniqueGiftInfo,
UniqueGiftModel,
UniqueGiftSymbol,
Update,
User,
UsersShared,
@@ -76,15 +91,6 @@ from telegram import (
Voice,
WebAppData,
)
from telegram._gifts import Gift, GiftInfo
from telegram._uniquegift import (
UniqueGift,
UniqueGiftBackdrop,
UniqueGiftBackdropColors,
UniqueGiftInfo,
UniqueGiftModel,
UniqueGiftSymbol,
)
from telegram._utils.datetime import UTC
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.types import ODVInput
@@ -331,6 +337,24 @@ def message(bot):
{"refunded_payment": RefundedPayment("EUR", 243, "payload", "charge_id", "provider_id")},
{"paid_star_count": 291},
{"paid_message_price_changed": PaidMessagePriceChanged(291)},
{"direct_message_price_changed": DirectMessagePriceChanged(True, 100)},
{
"checklist": Checklist(
"checklist_id",
tasks=[ChecklistTask(id=42, text="task 1"), ChecklistTask(id=43, text="task 2")],
)
},
{
"checklist_tasks_done": ChecklistTasksDone(
marked_as_done_task_ids=[1, 2, 3],
marked_as_not_done_task_ids=[4, 5],
)
},
{
"checklist_tasks_added": ChecklistTasksAdded(
tasks=[ChecklistTask(id=42, text="task 1"), ChecklistTask(id=43, text="task 2")],
)
},
],
ids=[
"reply",
@@ -408,6 +432,10 @@ def message(bot):
"refunded_payment",
"paid_star_count",
"paid_message_price_changed",
"direct_message_price_changed",
"checklist",
"checklist_tasks_done",
"checklist_tasks_added",
],
)
def message_params(bot, request):
@@ -2196,6 +2224,42 @@ class TestMessageWithoutRequest(MessageTestBase):
message, message.reply_dice, "send_dice", [], monkeypatch
)
async def test_reply_checklist(self, monkeypatch, message):
checklist = InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")])
async def make_assertion(*_, **kwargs):
return (
kwargs["chat_id"] == message.chat_id
and kwargs["business_connection_id"] == message.business_connection_id
and kwargs["checklist"] == checklist
and kwargs["disable_notification"] is True
)
assert check_shortcut_signature(
Message.reply_checklist,
Bot.send_checklist,
["chat_id", "business_connection_id", "reply_to_message_id"],
["do_quote", "reply_to_message_id"],
)
assert await check_shortcut_call(
message.reply_checklist,
message.get_bot(),
"send_checklist",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["chat_id", "business_connection_id"],
)
assert await check_defaults_handling(message.reply_checklist, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_checklist", make_assertion)
assert await message.reply_checklist(checklist, disable_notification=True)
await self.check_quote_parsing(
message,
message.reply_checklist,
"send_checklist",
[checklist, True],
monkeypatch,
)
async def test_reply_action(self, monkeypatch, message: Message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -2531,6 +2595,34 @@ class TestMessageWithoutRequest(MessageTestBase):
monkeypatch.setattr(message.get_bot(), "edit_message_caption", make_assertion)
assert await message.edit_caption(caption="new caption")
async def test_edit_checklist(self, monkeypatch, message):
checklist = InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")])
async def make_assertion(*_, **kwargs):
return (
kwargs["business_connection_id"] == message.business_connection_id
and kwargs["chat_id"] == message.chat_id
and kwargs["message_id"] == message.message_id
and kwargs["checklist"] == checklist
)
assert check_shortcut_signature(
Message.edit_checklist,
Bot.edit_message_checklist,
["chat_id", "message_id", "business_connection_id"],
[],
)
assert await check_shortcut_call(
message.edit_checklist,
message.get_bot(),
"edit_message_checklist",
shortcut_kwargs=["chat_id", "message_id", "business_connection_id"],
)
assert await check_defaults_handling(message.edit_checklist, message.get_bot())
monkeypatch.setattr(message.get_bot(), "edit_message_checklist", make_assertion)
assert await message.edit_checklist(checklist=checklist)
async def test_edit_media(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == message.chat_id
-9
View File
@@ -158,10 +158,6 @@ PTB_EXTRA_PARAMS = {
"InputPaidMedia": {"type", "media"}, # attributes common to all subclasses
"InputStoryContent": {"type"}, # attributes common to all subclasses
"StoryAreaType": {"type"}, # attributes common to all subclasses
# backwards compatibility for api 9.0 changes
# tags: deprecated v22.2, bot api 9.0
"BusinessConnection": {"can_reply"},
"ChatFullInfo": {"can_send_gift"},
"InputProfilePhoto": {"type"}, # attributes common to all subclasses
}
@@ -210,11 +206,6 @@ IGNORED_PARAM_REQUIREMENTS = {
"send_venue": {"latitude", "longitude", "title", "address"},
"send_contact": {"phone_number", "first_name"},
# ---->
# backwards compatibility for api 9.0 changes
# tags: deprecated v22.2, bot api 9.0
"BusinessConnection": {"is_enabled"},
"ChatFullInfo": {"accepted_gift_types"},
"TransactionPartnerUser": {"transaction_type"},
}
+1 -1
View File
@@ -93,7 +93,7 @@ def is_parameter_required_by_tg(field: str) -> bool:
def wrap_with_none(tg_parameter: "TelegramParameter", mapped_type: Any, obj: object) -> type:
"""Adds `None` to type annotation if the parameter isn't required. Respects ignored params."""
# have to import here to avoid circular imports
from tests.test_official.exceptions import ignored_param_requirements
from tests.test_official.exceptions import ignored_param_requirements # noqa: PLC0415
if tg_parameter.param_name in ignored_param_requirements(obj.__name__):
return mapped_type | type(None)
+7 -2
View File
@@ -18,7 +18,6 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
import datetime as dtm
from collections.abc import Sequence
from copy import deepcopy
import pytest
@@ -96,6 +95,7 @@ class OwnedGiftTestBase:
prepaid_upgrade_star_count = 200
can_be_transferred = True
transfer_star_count = 300
next_transfer_date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
class TestOwnedGiftWithoutRequest(OwnedGiftTestBase):
@@ -139,6 +139,7 @@ class TestOwnedGiftWithoutRequest(OwnedGiftTestBase):
"prepaid_upgrade_star_count": self.prepaid_upgrade_star_count,
"can_be_transferred": self.can_be_transferred,
"transfer_star_count": self.transfer_star_count,
"next_transfer_date": to_timestamp(self.next_transfer_date),
}
og = OwnedGift.de_json(json_dict, offline_bot)
@@ -292,6 +293,7 @@ def owned_gift_unique():
is_saved=TestOwnedGiftUniqueWithoutRequest.is_saved,
can_be_transferred=TestOwnedGiftUniqueWithoutRequest.can_be_transferred,
transfer_star_count=TestOwnedGiftUniqueWithoutRequest.transfer_star_count,
next_transfer_date=TestOwnedGiftUniqueWithoutRequest.next_transfer_date,
)
@@ -313,6 +315,7 @@ class TestOwnedGiftUniqueWithoutRequest(OwnedGiftTestBase):
"is_saved": self.is_saved,
"can_be_transferred": self.can_be_transferred,
"transfer_star_count": self.transfer_star_count,
"next_transfer_date": to_timestamp(self.next_transfer_date),
}
ogu = OwnedGiftUnique.de_json(json_dict, offline_bot)
assert ogu.gift == self.unique_gift
@@ -322,6 +325,7 @@ class TestOwnedGiftUniqueWithoutRequest(OwnedGiftTestBase):
assert ogu.is_saved == self.is_saved
assert ogu.can_be_transferred == self.can_be_transferred
assert ogu.transfer_star_count == self.transfer_star_count
assert ogu.next_transfer_date == self.next_transfer_date
assert ogu.api_kwargs == {}
def test_to_dict(self, owned_gift_unique):
@@ -335,6 +339,7 @@ class TestOwnedGiftUniqueWithoutRequest(OwnedGiftTestBase):
assert json_dict["is_saved"] == self.is_saved
assert json_dict["can_be_transferred"] == self.can_be_transferred
assert json_dict["transfer_star_count"] == self.transfer_star_count
assert json_dict["next_transfer_date"] == to_timestamp(self.next_transfer_date)
def test_equality(self, owned_gift_unique):
a = owned_gift_unique
@@ -365,7 +370,7 @@ def owned_gifts(request):
class OwnedGiftsTestBase:
total_count = 2
next_offset = "next_offset_str"
gifts: Sequence[OwnedGifts] = [
gifts: list[OwnedGift] = [
OwnedGiftRegular(
gift=Gift(
id="id1",
+13
View File
@@ -24,6 +24,8 @@ import pytest
from telegram import (
BotCommand,
Chat,
Checklist,
ChecklistTask,
ExternalReplyInfo,
Giveaway,
LinkPreviewOptions,
@@ -47,6 +49,7 @@ def external_reply_info():
link_preview_options=ExternalReplyInfoTestBase.link_preview_options,
giveaway=ExternalReplyInfoTestBase.giveaway,
paid_media=ExternalReplyInfoTestBase.paid_media,
checklist=ExternalReplyInfoTestBase.checklist,
)
@@ -63,6 +66,13 @@ class ExternalReplyInfoTestBase:
1,
)
paid_media = PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)])
checklist = Checklist(
title="Checklist Title",
tasks=[
ChecklistTask(text="Item 1", id=1),
ChecklistTask(text="Item 2", id=2),
],
)
class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase):
@@ -81,6 +91,7 @@ class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase):
"link_preview_options": self.link_preview_options.to_dict(),
"giveaway": self.giveaway.to_dict(),
"paid_media": self.paid_media.to_dict(),
"checklist": self.checklist.to_dict(),
}
external_reply_info = ExternalReplyInfo.de_json(json_dict, offline_bot)
@@ -92,6 +103,7 @@ class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase):
assert external_reply_info.link_preview_options == self.link_preview_options
assert external_reply_info.giveaway == self.giveaway
assert external_reply_info.paid_media == self.paid_media
assert external_reply_info.checklist == self.checklist
def test_to_dict(self, external_reply_info):
ext_reply_info_dict = external_reply_info.to_dict()
@@ -103,6 +115,7 @@ class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase):
assert ext_reply_info_dict["link_preview_options"] == self.link_preview_options.to_dict()
assert ext_reply_info_dict["giveaway"] == self.giveaway.to_dict()
assert ext_reply_info_dict["paid_media"] == self.paid_media.to_dict()
assert ext_reply_info_dict["checklist"] == self.checklist.to_dict()
def test_equality(self, external_reply_info):
a = external_reply_info
+42
View File
@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import datetime as dtm
import pytest
from telegram import (
@@ -29,6 +31,8 @@ from telegram import (
UniqueGiftModel,
UniqueGiftSymbol,
)
from telegram._utils.datetime import UTC, to_timestamp
from telegram.constants import UniqueGiftInfoOrigin
from tests.auxil.slots import mro_slots
@@ -383,6 +387,8 @@ def unique_gift_info():
origin=UniqueGiftInfoTestBase.origin,
owned_gift_id=UniqueGiftInfoTestBase.owned_gift_id,
transfer_star_count=UniqueGiftInfoTestBase.transfer_star_count,
last_resale_star_count=UniqueGiftInfoTestBase.last_resale_star_count,
next_transfer_date=UniqueGiftInfoTestBase.next_transfer_date,
)
@@ -410,6 +416,8 @@ class UniqueGiftInfoTestBase:
origin = UniqueGiftInfo.UPGRADE
owned_gift_id = "some_id"
transfer_star_count = 10
last_resale_star_count = 5
next_transfer_date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
class TestUniqueGiftInfoWithoutRequest(UniqueGiftInfoTestBase):
@@ -426,6 +434,8 @@ class TestUniqueGiftInfoWithoutRequest(UniqueGiftInfoTestBase):
"origin": self.origin,
"owned_gift_id": self.owned_gift_id,
"transfer_star_count": self.transfer_star_count,
"last_resale_star_count": self.last_resale_star_count,
"next_transfer_date": to_timestamp(self.next_transfer_date),
}
unique_gift_info = UniqueGiftInfo.de_json(json_dict, offline_bot)
assert unique_gift_info.api_kwargs == {}
@@ -433,6 +443,32 @@ class TestUniqueGiftInfoWithoutRequest(UniqueGiftInfoTestBase):
assert unique_gift_info.origin == self.origin
assert unique_gift_info.owned_gift_id == self.owned_gift_id
assert unique_gift_info.transfer_star_count == self.transfer_star_count
assert unique_gift_info.last_resale_star_count == self.last_resale_star_count
assert unique_gift_info.next_transfer_date == self.next_transfer_date
def test_de_json_localization(self, tz_bot, offline_bot, raw_bot):
json_dict = {
"gift": self.gift.to_dict(),
"origin": self.origin,
"owned_gift_id": self.owned_gift_id,
"transfer_star_count": self.transfer_star_count,
"last_resale_star_count": self.last_resale_star_count,
"next_transfer_date": to_timestamp(self.next_transfer_date),
}
unique_gift_info_raw = UniqueGiftInfo.de_json(json_dict, raw_bot)
unique_gift_info_offline = UniqueGiftInfo.de_json(json_dict, offline_bot)
unique_gift_info_tz = UniqueGiftInfo.de_json(json_dict, tz_bot)
# comparing utcoffsets because comparing timezones is unpredicatable
unique_gift_info_tz_offset = unique_gift_info_tz.next_transfer_date.utcoffset()
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(
unique_gift_info_tz.next_transfer_date.replace(tzinfo=None)
)
assert unique_gift_info_raw.next_transfer_date.tzinfo == UTC
assert unique_gift_info_offline.next_transfer_date.tzinfo == UTC
assert unique_gift_info_tz_offset == tz_bot_offset
def test_to_dict(self, unique_gift_info):
json_dict = unique_gift_info.to_dict()
@@ -440,6 +476,12 @@ class TestUniqueGiftInfoWithoutRequest(UniqueGiftInfoTestBase):
assert json_dict["origin"] == self.origin
assert json_dict["owned_gift_id"] == self.owned_gift_id
assert json_dict["transfer_star_count"] == self.transfer_star_count
assert json_dict["last_resale_star_count"] == self.last_resale_star_count
assert json_dict["next_transfer_date"] == to_timestamp(self.next_transfer_date)
def test_enum_type_conversion(self, unique_gift_info):
assert type(unique_gift_info.origin) is UniqueGiftInfoOrigin
assert unique_gift_info.origin == UniqueGiftInfoOrigin.UPGRADE
def test_equality(self, unique_gift_info):
a = unique_gift_info
+2 -1
View File
@@ -23,6 +23,7 @@ from copy import deepcopy
import pytest
from telegram import (
BusinessBotRights,
BusinessConnection,
BusinessMessagesDeleted,
CallbackQuery,
@@ -129,7 +130,7 @@ business_connection = BusinessConnection(
1,
from_timestamp(int(time.time())),
True,
True,
rights=BusinessBotRights(can_reply=True),
)
deleted_business_messages = BusinessMessagesDeleted(