Compare commits

..

9 Commits

Author SHA1 Message Date
Hinrich Mahler b1fff6d90a Use Lock instead of semaphore 2025-02-06 12:05:20 +01:00
Hinrich Mahler 31af1a9db8 Add an example on concurrency in FSM 2025-02-06 11:57:18 +01:00
Hinrich Mahler 4441543043 Try setting up infrastructure for optimistic locking. Example will follow 2025-02-05 23:40:15 +01:00
Hinrich Mahler 646ba37391 Move internal state storage to FiniteStateMachine and add state history 2025-02-05 13:13:21 +01:00
Hinrich Mahler 817b71d914 Disable tests harder … 2025-02-05 12:26:02 +01:00
Hinrich Mahler 434cbfade8 Add Some Abstractions for Timeout Jobs 2025-02-05 12:22:07 +01:00
Hinrich Mahler 34832d9db9 Temporarily Disable Tests on this branch 2025-02-05 11:17:47 +01:00
Hinrich Mahler 07225b9a02 Add State.ANY for fallbacks and allow handling multiple states for one update 2025-02-05 10:52:10 +01:00
Hinrich Mahler 0c06ba0a90 Initial FSM PoC 2025-02-04 21:36:40 +01:00
118 changed files with 3482 additions and 4071 deletions
@@ -1,5 +0,0 @@
<b>We've just released {tag}</b>.
Thank you to everyone who contributed to this release.
As usual, upgrade using <code>pip install -U python-telegram-bot</code>.
The release notes can be found <a href="https://docs.python-telegram-bot.org/en/{tag}/changelog.html">here</a>.
-30
View File
@@ -1,30 +0,0 @@
name: Create Chango Fragment
on:
pull_request:
types:
- opened
- reopened
- synchronize
permissions: {}
jobs:
create-chango-fragment:
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
name: create-chango-fragment
runs-on: ubuntu-latest
steps:
# Create the new fragment
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Bibo-Joshi/chango@9d6bd9d7612eca5fab2c5161687011be59baaf19 # v0.4.0
with:
github-token: ${{ secrets.CHANGO_PAT }}
query-issue-types: true
-41
View File
@@ -1,41 +0,0 @@
name: Test Admonitions Generation
on:
pull_request:
paths:
- telegram/**
- docs/**
- .github/workflows/docs-admonitions.yml
push:
branches:
- master
permissions: {}
jobs:
test-admonitions:
name: Test Admonitions Generation
runs-on: ${{matrix.os}}
permissions:
# for uploading artifacts
actions: write
strategy:
matrix:
python-version: ['3.12']
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: '**/requirements*.txt'
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -r requirements-dev-all.txt
- name: Test autogeneration of admonitions
run: pytest -v --tb=short tests/docs/admonition_inserter.py
-41
View File
@@ -1,41 +0,0 @@
name: Check Links in Documentation
on:
schedule:
# First day of month at 05:46 in every 2nd month
- cron: '46 5 1 */2 *'
pull_request:
paths:
- .github/workflows/docs-linkcheck.yml
permissions: {}
jobs:
test-sphinx-build:
name: test-sphinx-linkcheck
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: ['3.12']
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -r requirements-dev-all.txt
- name: Check Links
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto -b linkcheck
- name: Upload linkcheck output
# Run also if the previous steps failed
if: always()
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: linkcheck-output
path: docs/build/html/output.*
+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@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5.3.1
uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
- 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@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
with:
sarif_file: results.sarif
category: zizmor
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
pull-requests: write # for srvaroa/labeler to add labels in PR
runs-on: ubuntu-latest
steps:
- uses: srvaroa/labeler@0a20eccb8c94a1ee0bed5f16859aece1c45c3e55 # v1.13.0
- uses: srvaroa/labeler@fe4b1c73bb8abf2f14a44a6912a8b4fee835d631 # v1.12.0
# Config file at .github/labeler.yml
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+4 -29
View File
@@ -60,7 +60,7 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
compute-signatures:
name: Compute SHA1 Sums and Sign with Sigstore
@@ -119,14 +119,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
TAG: ${{ needs.build.outputs.TAG }}
# Create a tag and a GitHub Release. The description is filled by the static template, we
# just insert the correct tag in the template.
# Create a tag and a GitHub Release. The description can be changed later, as for now
# we don't define it through this workflow.
run: >-
sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html |
gh release create
"$TAG"
--repo '${{ github.repository }}'
--notes-file -
--generate-notes
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -138,27 +137,3 @@ jobs:
gh release upload
"$TAG" dist/**
--repo '${{ github.repository }}'
telegram-channel:
name: Publish to Telegram Channel
needs:
- github-release
runs-on: ubuntu-latest
environment:
name: release_pypi
permissions: {}
steps:
- name: Publish to Telegram Channel
env:
TAG: ${{ needs.build.outputs.TAG }}
# This secret is configured only for the `pypi-release` branch
BOT_TOKEN: ${{ secrets.CHANNEL_BOT_TOKEN }}
run: >-
sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html |
curl
-X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage"
-d "chat_id=@pythontelegrambotchannel"
-d "parse_mode=HTML"
--data-urlencode "text@-"
+4 -5
View File
@@ -60,7 +60,7 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
with:
repository-url: https://test.pypi.org/legacy/
@@ -121,15 +121,14 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
TAG: ${{ needs.build.outputs.TAG }}
# Create a tag and a GitHub Release *draft*. The description is filled by the static
# template, we just insert the correct tag in the template.
# Create a GitHub Release *draft*. The description can be changed later, as for now
# we don't define it through this workflow.
run: >-
sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html |
gh release create
"$TAG"
--repo '${{ github.repository }}'
--generate-notes
--draft
--notes-file -
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
-51
View File
@@ -1,51 +0,0 @@
name: Bot API Tests
on:
pull_request:
paths:
- telegram/**
- tests/**
push:
branches:
- master
schedule:
# Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions
- cron: '7 3 * * 1,5'
permissions: {}
jobs:
check-conformity:
name: check-conformity
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: [3.11]
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install .[all]
python -W ignore -m pip install -r requirements-unit-tests.txt
- name: Compare to official api
run: |
pytest -v tests/test_official/test_official.py --junit-xml=.test_report_official.xml
exit $?
env:
TEST_OFFICIAL: "true"
shell: bash --noprofile --norc {0}
- name: Test Summary
id: test_summary
uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
if: always() # always run, even if tests fail
with:
paths: .test_report_official.xml
-23
View File
@@ -1,23 +0,0 @@
name: Check Type Completeness
on:
pull_request:
paths:
- telegram/**
- pyproject.toml
- .github/workflows/type_completeness.yml
push:
branches:
- master
permissions: {}
jobs:
test-type-completeness:
name: test-type-completeness
runs-on: ubuntu-latest
steps:
- uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1
with:
package-name: telegram
python-version: 3.12
pyright-version: ~=1.1.367
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -18,7 +18,7 @@ python:
install:
- method: pip
path: .
- requirements: requirements-dev-all.txt
- requirements: docs/requirements-docs.txt
build:
os: ubuntu-22.04
+4 -58
View File
@@ -1,62 +1,8 @@
Version 21.11.1
===============
.. _ptb-changelog:
*Released 2025-03-01*
This is the technical changelog for version 21.11. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
Documentation Improvements
--------------------------
- Fix ReadTheDocs Build (:pr:`4695`)
Version 21.11
=============
*Released 2025-03-01*
This is the technical changelog for version 21.11. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
Major Changes and New Features
------------------------------
- Full Support for Bot API 8.3 (:pr:`4676` closes :issue:`4677`, :pr:`4682` by `aelkheir <https://github.com/aelkheir>`_, :pr:`4690` by `aelkheir <https://github.com/aelkheir>`_, :pr:`4691` by `aelkheir <https://github.com/aelkheir>`_)
- Make ``provider_token`` Argument Optional (:pr:`4689`)
- Remove Deprecated ``InlineQueryResultArticle.hide_url`` (:pr:`4640` closes :issue:`4638`)
- Accept ``datetime.timedelta`` Input in ``Bot`` Method Parameters (:pr:`4651`)
- Extend Customization Support for ``Bot.base_(file_)url`` (:pr:`4632` closes :issue:`3355`)
- Support ``allow_paid_broadcast`` in ``AIORateLimiter`` (:pr:`4627` closes :issue:`4578`)
- Add ``BaseUpdateProcessor.current_concurrent_updates`` (:pr:`4626` closes :issue:`3984`)
Minor Changes and Bug Fixes
---------------------------
- Add Bootstrapping Logic to ``Application.run_*`` (:pr:`4673` closes :issue:`4657`)
- Fix a Bug in ``edit_user_star_subscription`` (:pr:`4681` by `vavasik800 <https://github.com/vavasik800>`_)
- Simplify Handling of Empty Data in ``TelegramObject.de_json`` and Friends (:pr:`4617` closes :issue:`4614`)
Documentation Improvements
--------------------------
- Documentation Improvements (:pr:`4641`)
- Overhaul Admonition Insertion in Documentation (:pr:`4462` closes :issue:`4414`)
Internal Changes
----------------
- Stabilize Linkcheck Test (:pr:`4693`)
- Bump ``pre-commit`` Hooks to Latest Versions (:pr:`4643`)
- Refactor Tests for ``TelegramObject`` Classes with Subclasses (:pr:`4654` closes :issue:`4652`)
- Use Fine Grained Permissions for GitHub Actions Workflows (:pr:`4668`)
Dependency Updates
------------------
- Bump ``actions/setup-python`` from 5.3.0 to 5.4.0 (:pr:`4665`)
- Bump ``dependabot/fetch-metadata`` from 2.2.0 to 2.3.0 (:pr:`4666`)
- Bump ``actions/stale`` from 9.0.0 to 9.1.0 (:pr:`4667`)
- Bump ``astral-sh/setup-uv`` from 5.1.0 to 5.2.2 (:pr:`4664`)
- Bump ``codecov/test-results-action`` from 1.0.1 to 1.0.2 (:pr:`4663`)
=========
Changelog
=========
Version 21.10
=============
+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-8.3-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-8.2-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 **8.3** are natively supported by this library.
All types and methods of the Telegram Bot API **8.2** 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
@@ -1,19 +0,0 @@
breaking = """This release removes all functionality that was deprecated in v20.x. This is in line with our :ref:`stability policy <stability-policy>`.
This includes the following changes:
- Removed ``filters.CHAT`` (all messages have an associated chat) and ``filters.StatusUpdate.USER_SHARED`` (use ``filters.StatusUpdate.USERS_SHARED`` instead).
- Removed ``Defaults.disable_web_page_preview`` and ``Defaults.quote``. Use ``Defaults.link_preview_options`` and ``Defaults.do_quote`` instead.
- Removed ``ApplicationBuilder.(get_updates_)proxy_url`` and ``HTTPXRequest.proxy_url``. Use ``ApplicationBuilder.(get_updates_)proxy`` and ``HTTPXRequest.proxy`` instead.
- Removed the ``*_timeout`` arguments of ``Application.run_polling`` and ``Updater.start_webhook``. Instead, specify the values via ``ApplicationBuilder.get_updates_*_timeout``.
- Removed ``constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH``. Use ``constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`` instead.
- Removed the argument ``quote`` of ``Message.reply_*``. Use ``do_quote`` instead.
- Removed the superfluous ``EncryptedPassportElement.credentials`` without replacement.
- Changed attribute value of ``PassportFile.file_date`` from :obj:`int` to :class:`datetime.datetime`. Make sure to adjust your code accordingly.
- Changed the attribute value of ``PassportElementErrors.file_hashes`` from :obj:`list` to :obj:`tuple`. Make sure to adjust your code accordingly.
- Make ``BaseRequest.read_timeout`` an abstract property. If you subclass ``BaseRequest``, you need to implement this property.
- The default value for ``write_timeout`` now defaults to ``DEFAULT_NONE`` also for bot methods that send media. Previously, it was ``20``. If you subclass ``BaseRequest``, make sure to use your desired write timeout if ``RequestData.multipart_data`` is set.
"""
[[pull_requests]]
uid = "4671"
author_uid = "Bibo-Joshi"
closes_threads = ["4659"]
@@ -1,5 +0,0 @@
documentation = "Add `chango <https://chango.readthedocs.io/stable/>`_ As Changelog Management Tool"
[[pull_requests]]
uid = "4672"
author_uid = "Bibo-Joshi"
closes_threads = ["4321"]
@@ -1,5 +0,0 @@
internal = "Bump github/codeql-action from 3.28.8 to 3.28.10"
[[pull_requests]]
uid = "4697"
author_uid = "dependabot[bot]"
closes_threads = []
@@ -1,5 +0,0 @@
internal = "Bump srvaroa/labeler from 1.12.0 to 1.13.0"
[[pull_requests]]
uid = "4698"
author_uid = "dependabot[bot]"
closes_threads = []
@@ -1,5 +0,0 @@
internal = "Bump astral-sh/setup-uv from 5.2.2 to 5.3.1"
[[pull_requests]]
uid = "4699"
author_uid = "dependabot[bot]"
closes_threads = []
@@ -1,5 +0,0 @@
internal = "Bump Bibo-Joshi/chango from 0.3.1 to 0.3.2"
[[pull_requests]]
uid = "4700"
author_uid = "dependabot[bot]"
closes_threads = []
@@ -1,5 +0,0 @@
internal = "Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4"
[[pull_requests]]
uid = "4701"
author_uid = "dependabot[bot]"
closes_threads = []
@@ -1,5 +0,0 @@
internal = "Bump pytest from 8.3.4 to 8.3.5"
[[pull_requests]]
uid = "4709"
author_uid = "dependabot[bot]"
closes_threads = []
@@ -1,5 +0,0 @@
internal = "Bump sphinx from 8.1.3 to 8.2.3"
[[pull_requests]]
uid = "4710"
author_uid = "dependabot[bot]"
closes_threads = []
@@ -1,5 +0,0 @@
internal = "Bump Bibo-Joshi/chango from 0.3.2 to 0.4.0"
[[pull_requests]]
uid = "4712"
author_uid = "Bibo-Joshi"
closes_threads = []
@@ -1,5 +0,0 @@
internal = "Bump Version to v22.0"
[[pull_requests]]
uid = "4719"
author_uid = "Bibo-Joshi"
closes_threads = []
-78
View File
@@ -1,78 +0,0 @@
# noqa: INP001
# pylint: disable=import-error
"""Configuration for the chango changelog tool"""
from collections.abc import Collection
from typing import Optional
from chango.concrete import DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory
from chango.concrete.sections import GitHubSectionChangeNote, Section, SectionVersionNote
version_scanner = DirectoryVersionScanner(base_directory=".", unreleased_directory="unreleased")
class ChangoSectionChangeNote(
GitHubSectionChangeNote.with_sections( # type: ignore[misc]
[
Section(uid="highlights", title="Highlights", sort_order=0),
Section(uid="breaking", title="Breaking Changes", sort_order=1),
Section(uid="security", title="Security Changes", sort_order=2),
Section(uid="deprecations", title="Deprecations", sort_order=3),
Section(uid="features", title="New Features", sort_order=4),
Section(uid="bugfixes", title="Bug Fixes", sort_order=5),
Section(uid="dependencies", title="Dependencies", sort_order=6),
Section(uid="other", title="Other Changes", sort_order=7),
Section(uid="documentation", title="Documentation", sort_order=8),
Section(uid="internal", title="Internal Changes", sort_order=9),
]
)
):
"""Custom change note type for PTB. Mainly overrides get_sections to map labels to sections"""
OWNER = "python-telegram-bot"
REPOSITORY = "python-telegram-bot"
@classmethod
def get_sections(
cls,
labels: Collection[str],
issue_types: Optional[Collection[str]],
) -> set[str]:
"""Override get_sections to have customized auto-detection of relevant sections based on
the pull request and linked issues. Certainly not perfect in all cases, but should be a
good start for most PRs.
"""
combined_labels = set(labels) | (set(issue_types or []))
mapping = {
"🐛 bug": "bugfixes",
"💡 feature": "features",
"🧹 chore": "internal",
"⚙️ bot-api": "features",
"⚙️ documentation": "documentation",
"⚙️ tests": "internal",
"⚙️ ci-cd": "internal",
"⚙️ security": "security",
"⚙️ examples": "documentation",
"⚙️ type-hinting": "other",
"🛠 refactor": "internal",
"🛠 breaking": "breaking",
"⚙️ dependencies": "dependencies",
"🔗 github-actions": "internal",
"🛠 code-quality": "internal",
}
# we want to return *all* from the mapping that are in the combined_labels
# removing superfluous sections from the fragment is a tad easier than adding them
found = {section for label, section in mapping.items() if label in combined_labels}
# if we have not found any sections, we default to "other"
return found or {"other"}
chango_instance = DirectoryChanGo(
change_note_type=ChangoSectionChangeNote,
version_note_type=SectionVersionNote,
version_history_type=HeaderVersionHistory,
scanner=version_scanner,
)
View File
+9 -9
View File
@@ -47,29 +47,29 @@ keyword_args = [
"",
]
media_write_timeout_change_methods = [
"add_sticker_to_set",
"create_new_sticker_set",
"send_animation",
media_write_timeout_deprecation_methods = [
"send_photo",
"send_audio",
"send_document",
"send_media_group",
"send_photo",
"send_sticker",
"send_video",
"send_video_note",
"send_animation",
"send_voice",
"send_media_group",
"set_chat_photo",
"upload_sticker_file",
"add_sticker_to_set",
"create_new_sticker_set",
]
media_write_timeout_change = [
media_write_timeout_deprecation = [
" write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to "
" :paramref:`telegram.request.BaseRequest.post.write_timeout`. By default, ``20`` "
" seconds are used as write timeout."
"",
"",
" .. versionchanged:: 22.0",
" The default value changed to "
" .. deprecated:: 20.7",
" In future versions, the default value will be changed to "
" :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
"",
"",
+4 -4
View File
@@ -32,8 +32,8 @@ from docs.auxil.kwargs_insertion import (
find_insert_pos_for_kwargs,
get_updates_read_timeout_addition,
keyword_args,
media_write_timeout_change,
media_write_timeout_change_methods,
media_write_timeout_deprecation,
media_write_timeout_deprecation_methods,
)
from docs.auxil.link_code import LINE_NUMBERS
@@ -116,9 +116,9 @@ def autodoc_process_docstring(
if (
"post.write_timeout`. Defaults to" in to_insert
and method_name in media_write_timeout_change_methods
and method_name in media_write_timeout_deprecation_methods
):
effective_insert: list[str] = media_write_timeout_change
effective_insert: list[str] = media_write_timeout_deprecation
elif get_updates and to_insert.lstrip().startswith("read_timeout"):
effective_insert = [to_insert, *get_updates_read_timeout_addition]
else:
+1 -2
View File
@@ -1,5 +1,4 @@
chango~=0.4.0
sphinx==8.2.3
sphinx==8.1.3
furo==2024.8.6
furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1
sphinx-paramlinks==0.6.0
+1 -9
View File
@@ -1,9 +1 @@
.. _ptb-changelog:
=========
Changelog
=========
.. chango::
.. include:: ../../changes/LEGACY.rst
.. include:: ../../CHANGES.rst
-14
View File
@@ -8,11 +8,6 @@ from pathlib import Path
# documentation root, use os.path.abspath to make it absolute, like shown here.
from sphinx.application import Sphinx
if sys.version_info < (3, 12):
# Due to dependency on chango
raise RuntimeError("This documentation needs at least Python 3.12")
sys.path.insert(0, str(Path("../..").resolve().absolute()))
# -- General configuration ------------------------------------------------
@@ -41,7 +36,6 @@ needs_sphinx = "8.1.3"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"chango.sphinx_ext",
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.intersphinx",
@@ -58,9 +52,6 @@ extensions = [
if os.environ.get("READTHEDOCS", "") == "True":
extensions.append("sphinx_build_compatibility.extension")
# Configuration for the chango sphinx directive
chango_pyproject_toml_path = Path(__file__).parent.parent.parent
# For shorter links to Wiki in docstrings
extlinks = {
"wiki": ("https://github.com/python-telegram-bot/python-telegram-bot/wiki/%s", "%s"),
@@ -120,11 +111,6 @@ linkcheck_ignore = [
# Anchors are apparently inserted by GitHub dynamically, so let's skip checking them
"https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples#",
r"https://github\.com/python-telegram-bot/python-telegram-bot/wiki/[\w\-_,]+\#",
# The LGPL license link regularly causes network errors for some reason
re.escape("https://www.gnu.org/licenses/lgpl-3.0.html"),
# The doc-fixes branch may not always exist - doesn't matter, we only link to it from the
# contributing guide
re.escape("https://docs.python-telegram-bot.org/en/doc-fixes"),
]
linkcheck_allowed_redirects = {
# Redirects to the default version are okay
-2
View File
@@ -1,5 +1,3 @@
.. _stability-policy:
Stability Policy
================
-1
View File
@@ -27,7 +27,6 @@ Your bot can accept payments from Telegram users. Please see the `introduction t
telegram.successfulpayment
telegram.transactionpartner
telegram.transactionpartneraffiliateprogram
telegram.transactionpartnerchat
telegram.transactionpartnerfragment
telegram.transactionpartnerother
telegram.transactionpartnertelegramads
@@ -1,7 +0,0 @@
TransactionPartnerChat
======================
.. autoclass:: telegram.TransactionPartnerChat
:members:
:show-inheritance:
:inherited-members: TransactionPartner
+1 -3
View File
@@ -98,6 +98,4 @@
.. |tz-naive-dtms| replace:: For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
.. |org-verify| replace:: `on behalf of the organization <https://telegram.org/verify#third-party-verification>`__
.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env python
# pylint: disable=unused-argument
# This program is dedicated to the public domain under the CC0 license.
"""Simple state machine to handle user support.
One admin is supported. The admin can have one active conversation at a time. Other users
are put on hold until the admin finishes the current conversation.
In each conversation, the admin and the user take turns to send messages.
"""
import logging
from typing import Optional
from telegram import Update
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
FiniteStateMachine,
MessageHandler,
State,
StateInfo,
filters,
)
# Enable logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("telegram").setLevel(logging.WARNING)
logging.getLogger("telegram.ext.Application").setLevel(logging.DEBUG)
logger = logging.getLogger(__name__)
class UserSupportMachine(FiniteStateMachine[Optional[int]]):
HOLD = State("HOLD")
WELCOMING = State("WELCOMING")
WAITING_FOR_REPLY = State("WAITING_FOR_REPLY")
WRITING = State("WRITING")
def __init__(self, admin_id: int):
self.admin_id = admin_id
super().__init__()
def _get_admin_state(self) -> tuple[State, int]:
return self._states[self.admin_id]
def get_state_info(self, update: object) -> StateInfo[Optional[int]]:
if not isinstance(update, Update) or not (user := update.effective_user):
key = None
state, version = self.states[key]
return StateInfo(key=key, state=state, version=version)
# Admin is easy - just return the state
admin_state, admin_version = self._get_admin_state()
if user.id == self.admin_id:
logging.debug("Returning admin state: %s", admin_state)
return StateInfo(self.admin_id, admin_state, admin_version)
# If the user state is active in the conversation, we can just return that state
user_state, user_version = self._states[user.id]
if user_state.matches(self.WELCOMING | self.WRITING | self.WAITING_FOR_REPLY):
logging.debug("Returning user state: %s", user_state)
return StateInfo(user.id, user_state, user_version)
# On first interaction, we need to determine what to do with the user
# if the admin is not idle, we put the user on hold. Otherwise, they may send the first
# message, and we put the admin in waiting for reply to avoid another user occupying the
# admin first
effective_user_state = self.HOLD if admin_state != State.IDLE else self.WELCOMING
self._do_set_state(user.id, effective_user_state, user_version)
if effective_user_state == self.WELCOMING:
self._do_set_state(self.admin_id, self.WAITING_FOR_REPLY)
logging.debug("Returning user state: %s", effective_user_state)
return StateInfo(user.id, effective_user_state, user_version)
async def welcome_user(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_message.forward(context.bot_data["admin_id"])
suffix = ""
if UserSupportMachine.HOLD in context.fsm.get_state_history(context.fsm_state_info.key)[:-1]:
suffix = " Thank you for patiently waiting. We hope you enjoyed the music."
await update.effective_message.reply_text(
"Welcome! Your message has been forwarded to the admin. "
f"They will get back to you soon.{suffix}"
)
await context.set_state(UserSupportMachine.WAITING_FOR_REPLY)
await context.fsm.set_state(context.bot_data["admin_id"], UserSupportMachine.WRITING)
context.bot_data["active_user"] = update.effective_user.id
async def conversation_timeout(context: ContextTypes.DEFAULT_TYPE) -> None:
active_user = context.bot_data.get("active_user")
admin_id = context.bot_data["admin_id"]
async def handle(user_id: int) -> None:
await context.bot.send_message(
user_id, "The conversation has been stopped due to inactivity."
)
await context.fsm.set_state(user_id, State.IDLE)
if active_user:
await handle(active_user)
await handle(admin_id)
async def handle_reply(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if not (active_user := context.bot_data.get("active_user")):
logger.warning("No active user found, ignoring message")
target = (
active_user
if update.effective_user.id == (admin_id := context.bot_data["admin_id"])
else admin_id
)
await context.bot.send_message(target, update.effective_message.text)
logging.debug("Forwarded message to %s", target)
await context.set_state(UserSupportMachine.WAITING_FOR_REPLY)
logging.debug("Done setting state to WAITING_FOR_REPLY for %s", target)
await context.fsm.set_state(target, UserSupportMachine.WRITING)
logging.debug("Done setting state to WRITING for %s, context.fsm_key")
context.fsm.schedule_timeout(
when=30,
callback=conversation_timeout,
cancel_keys=[active_user, admin_id],
)
async def stop_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
text = "The conversation has been stopped."
admin_id = context.bot_data["admin_id"]
active_user = context.bot_data.get("active_user")
await context.bot.send_message(admin_id, text)
await context.fsm.set_state(admin_id, State.IDLE)
if active_user:
await context.bot.send_message(active_user, text)
await context.fsm.set_state(active_user, State.IDLE)
async def hold_melody(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_message.reply_text(
"You have been put on hold. The admin will get back to you soon. Please hear some music "
"while you wait: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
)
async def not_your_turn(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_message.reply_text(
"It's not your turn yet. Please wait for the other party to reply to your message."
)
async def unsupported_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_message.reply_text("This message is not supported.")
def main() -> None:
application = Application.builder().token("TOKEN").build()
application.fsm = UserSupportMachine(admin_id=123456)
application.fsm.set_job_queue(application.job_queue)
application.bot_data["admin_id"] = application.fsm.admin_id
# Users are welcomed only if they are in the corresponding state
application.add_handler(
MessageHandler(~filters.User(application.fsm.admin_id) & filters.TEXT, welcome_user),
state=UserSupportMachine.WELCOMING,
)
# Conversation logic:
# * forward messages between user and admin
# * stop the conversation at any time (admin or user)
# * point out that the other party is currently writing
# Important: Order matters!
application.add_handler(
CommandHandler("stop", stop_conversation),
state=UserSupportMachine.WAITING_FOR_REPLY | UserSupportMachine.WRITING,
)
application.add_handler(
MessageHandler(filters.TEXT, handle_reply), state=UserSupportMachine.WRITING
)
application.add_handler(
MessageHandler(filters.TEXT, not_your_turn), state=UserSupportMachine.WAITING_FOR_REPLY
)
# If the admin is busy, put the user on hold
application.add_handler(
MessageHandler(filters.TEXT, hold_melody), state=UserSupportMachine.HOLD
)
# Fallback
application.add_handler(MessageHandler(filters.ALL, unsupported_message), state=State.ANY)
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python
# pylint: disable=unused-argument
# This program is dedicated to the public domain under the CC0 license.
"""State machine bot showcasing how concurrency can be handled with FSM.
How to use:
* Use Case 1: Concurrent balance updates
- /unsafe_update <balance_update>: Unsafe update of the wallet balance. Send the command
multiple times in quick succession (less than 1 second) to see the effect
- /safe_update <balance_update>: Safe update of the wallet balance. Send the command
multiple times in quick succession (less than 1 second) to see the effect
* Use Case 2: Declare a winner - who is the fastest?
- /unsafe_declare_winner: Unsafe declaration of the user as winner. Send the command
multiple times in quick succession (less than 1 second) to see the effect. Needs restart
after the winner is declared.
- /safe_declare_winner: Safe declaration of the user as winner. Send the command
multiple times in quick succession (less than 1 second) to see the effect. Needs restart
after the winner is declared.
"""
import asyncio
import logging
from telegram import Update
from telegram.constants import ChatAction
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
FiniteStateMachine,
MessageHandler,
State,
StateInfo,
filters,
)
# Enable logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("telegram").setLevel(logging.WARNING)
logging.getLogger("telegram.ext.Application").setLevel(logging.DEBUG)
logger = logging.getLogger(__name__)
class ConcurrentMachine(FiniteStateMachine[None]):
"""This FSM only knows a global state for the whole bot"""
UPDATING_BALANCE = State("UPDATING_BALANCE")
WINNER_DECLARED = State("WINNER_DECLARED")
def get_state_info(self, update: object) -> StateInfo[None]:
state, version = self.states[None]
return StateInfo(key=None, state=state, version=version)
########################################
# Use case 1: Concurrent balance updates
########################################
async def update_balance(context: ContextTypes.DEFAULT_TYPE, update: Update) -> None:
initial_balance = context.bot_data.get("balance", 0)
balance_update = int(context.args[0])
# Simulate heavy computation
await update.effective_message.reply_text(
f"Initiating balance update: {initial_balance}. Updating ..."
)
await update.effective_chat.send_action(ChatAction.TYPING)
await asyncio.sleep(4.5)
new_balance = context.bot_data["balance"] = initial_balance + balance_update
await update.effective_message.reply_text(f"Balance updated. New balance: {new_balance}")
async def unsafe_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Unsafe update of the wallet balance"""
# Simulate heavy computation *before* the update is processed
await asyncio.sleep(1)
await context.fsm.set_state(context.fsm_state_info.key, ConcurrentMachine.UPDATING_BALANCE)
# At this point, the lock is released such that multiple updates can update
# the balance concurrently. This can lead to race conditions.
await update_balance(context, update)
await context.fsm.set_state(context.fsm_state_info.key, State.IDLE)
async def safe_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Safe update of the wallet balance"""
# Simulate heavy computation *before* the update is processed
await asyncio.sleep(1)
async with context.as_fsm_state(ConcurrentMachine.UPDATING_BALANCE):
# At this point, the lock is acquired such that only one update can update
# the balance at a time. This prevents race conditions.
await update_balance(context, update)
async def busy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Busy state"""
await update.effective_message.reply_text("I'm busy, try again later.")
####################################################
# Use case 2: Declare a winner - who is the fastest?
####################################################
async def declare_winner_unsafe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Declare the user as winner"""
# Simulate heavy computation *before* the update is processed
await asyncio.sleep(1)
# Unsafe state update: No version check, so the state might have already changed
await context.fsm.set_state(context.fsm_state_info.key, ConcurrentMachine.WINNER_DECLARED)
await update.effective_message.reply_text("You are the winner!")
async def declare_winner_safe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Declare the user as winner"""
# Simulate heavy computation *before* the update is processed
await asyncio.sleep(1)
try:
await context.set_state(ConcurrentMachine.WINNER_DECLARED)
await update.effective_message.reply_text("You are the winner!")
except ValueError:
await update.effective_message.reply_text(
"Sorry, you are too late. Someone else was faster."
)
def main() -> None:
application = Application.builder().token("TOKEN").concurrent_updates(True).build()
application.fsm = ConcurrentMachine()
# Note: OR-combination of states is used here to allow both use cases to be handled
# in parallel. Not really necessary for the showcasing, just a nice touch :)
# Use case 2: Declare a winner - who is the fastest?
application.add_handler(
CommandHandler("unsafe_declare_winner", declare_winner_unsafe),
state=State.IDLE | ConcurrentMachine.UPDATING_BALANCE,
)
application.add_handler(
CommandHandler("safe_declare_winner", declare_winner_safe),
state=State.IDLE | ConcurrentMachine.UPDATING_BALANCE,
)
# Use case 1: Concurrent balance updates
application.add_handler(
CommandHandler("unsafe_update", unsafe_update, has_args=1),
state=State.IDLE | ConcurrentMachine.WINNER_DECLARED,
)
application.add_handler(
CommandHandler("safe_update", safe_update, has_args=1),
state=State.IDLE | ConcurrentMachine.WINNER_DECLARED,
)
# Order matters, so this needs to be added last
application.add_handler(
MessageHandler(filters.ALL, busy), state=ConcurrentMachine.UPDATING_BALANCE
)
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
+2 -8
View File
@@ -61,9 +61,9 @@ async def start_with_shipping_callback(update: Update, context: ContextTypes.DEF
title,
description,
payload,
PAYMENT_PROVIDER_TOKEN,
currency,
prices,
provider_token=PAYMENT_PROVIDER_TOKEN,
need_name=True,
need_phone_number=True,
need_email=True,
@@ -90,13 +90,7 @@ async def start_without_shipping_callback(
# optionally pass need_name=True, need_phone_number=True,
# need_email=True, need_shipping_address=True, is_flexible=True
await context.bot.send_invoice(
chat_id,
title,
description,
payload,
currency,
prices,
provider_token=PAYMENT_PROVIDER_TOKEN,
chat_id, title, description, payload, PAYMENT_PROVIDER_TOKEN, currency, prices
)
-5
View File
@@ -105,11 +105,6 @@ search-paths = ["telegram"]
[tool.hatch.build]
packages = ["telegram"]
# CHANGO
[tool.chango]
sys_path = "changes"
chango_instance = { name= "chango_instance", module = "config" }
# BLACK:
[tool.black]
line-length = 99
+1 -1
View File
@@ -4,7 +4,7 @@
build
# For the test suite
pytest==8.3.5
pytest==8.3.4
# needed because pytest doesn't come with native support for coroutines as tests
pytest-asyncio==0.21.2
-2
View File
@@ -238,7 +238,6 @@ __all__ = (
"TextQuote",
"TransactionPartner",
"TransactionPartnerAffiliateProgram",
"TransactionPartnerChat",
"TransactionPartnerFragment",
"TransactionPartnerOther",
"TransactionPartnerTelegramAds",
@@ -276,7 +275,6 @@ from telegram._payment.stars.startransactions import StarTransaction, StarTransa
from telegram._payment.stars.transactionpartner import (
TransactionPartner,
TransactionPartnerAffiliateProgram,
TransactionPartnerChat,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
+55 -85
View File
@@ -112,7 +112,7 @@ from telegram.error import EndPointNotFound, InvalidToken
from telegram.request import BaseRequest, RequestData
from telegram.request._httpxrequest import HTTPXRequest
from telegram.request._requestparameter import RequestParameter
from telegram.warnings import PTBUserWarning
from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
if TYPE_CHECKING:
from telegram import (
@@ -245,7 +245,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
Example:
``"https://api.telegram.org/bot{token}/test"``
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
Supports callable input and string formatting.
base_file_url (:obj:`str`, optional): Telegram Bot API file URL.
If the string contains ``{token}``, it will be replaced with the bot's
@@ -262,7 +262,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
Example:
``"https://api.telegram.org/file/bot{token}/test"``
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
Supports callable input and string formatting.
request (:class:`telegram.request.BaseRequest`, optional): Pre initialized
:class:`telegram.request.BaseRequest` instances. Will be used for all bot methods
@@ -1218,7 +1218,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1243,10 +1242,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
original message was sent (or channel username in the format ``@channelusername``).
message_id (:obj:`int`): Message identifier in the chat specified in
:paramref:`from_chat_id`.
video_start_timestamp (:obj:`int`, optional): New start timestamp for the
forwarded video in the message
.. versionadded:: 21.11
disable_notification (:obj:`bool`, optional): |disable_notification|
protect_content (:obj:`bool`, optional): |protect_content|
@@ -1265,7 +1260,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
"chat_id": chat_id,
"from_chat_id": from_chat_id,
"message_id": message_id,
"video_start_timestamp": video_start_timestamp,
}
return await self._send_message(
@@ -1564,7 +1558,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent audio
in seconds.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
performer (:obj:`str`, optional): Performer.
title (:obj:`str`, optional): Track name.
@@ -1961,8 +1955,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -2006,17 +1998,10 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
in seconds.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): Cover for the video in the message. |fileinputnopath|
.. versionadded:: 21.11
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message.
.. versionadded:: 21.11
caption (:obj:`str`, optional): Video caption (may also be used when resending videos
by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH`
characters after entities parsing.
@@ -2103,8 +2088,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
"width": width,
"height": height,
"supports_streaming": supports_streaming,
"cover": self._parse_file_input(cover, attach=True) if cover else None,
"start_timestamp": start_timestamp,
"thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None,
"has_spoiler": has_spoiler,
"show_caption_above_media": show_caption_above_media,
@@ -2192,7 +2175,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
in seconds.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
length (:obj:`int`, optional): Video width and height, i.e. diameter of the video
message.
@@ -2344,7 +2327,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent
animation in seconds.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
width (:obj:`int`, optional): Animation width.
height (:obj:`int`, optional): Animation height.
@@ -2528,7 +2511,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the voice
message in seconds.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
disable_notification (:obj:`bool`, optional): |disable_notification|
protect_content (:obj:`bool`, optional): |protect_content|
@@ -2842,7 +2825,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
locations that can be edited indefinitely.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
heading (:obj:`int`, optional): For live locations, a direction in which the user is
moving, in degrees. Must be between
@@ -3012,7 +2995,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
.. versionadded:: 21.2.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
business_connection_id (:obj:`str`, optional): |business_id_str_edit|
@@ -3709,7 +3692,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
time in seconds that the
result of the inline query may be cached on the server. Defaults to ``300``.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
is_personal (:obj:`bool`, optional): Pass :obj:`True`, if results may be cached on
the server side only for the user that sent the query. By default,
@@ -4161,7 +4144,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
time in seconds that the
result of the callback query may be cached client-side. Defaults to 0.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
Returns:
@@ -4581,7 +4564,19 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
if not isinstance(read_timeout, DefaultValue):
arg_read_timeout: float = read_timeout or 0
else:
arg_read_timeout = self._request[0].read_timeout or 0
try:
arg_read_timeout = self._request[0].read_timeout or 0
except NotImplementedError:
arg_read_timeout = 2
self._warn(
PTBDeprecationWarning(
"20.7",
f"The class {self._request[0].__class__.__name__} does not override "
"the property `read_timeout`. Overriding this property will be mandatory "
"in future versions. Using 2 seconds as fallback.",
),
stacklevel=2,
)
# Ideally we'd use an aggressive read timeout for the polling. However,
# * Short polling should return within 2 seconds.
@@ -4636,11 +4631,8 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
"""
Use this method to specify a url and receive incoming updates via an outgoing webhook.
Whenever there is an update for the bot, Telegram will send an HTTPS POST request to the
specified url, containing An Update. In case of an unsuccessful request
(a request with response
`HTTP status code <https://en.wikipedia.org/wiki/List_of_HTTP_status_codes>`_different
from ``2XY``),
Telegram will repeat the request and give up after a reasonable amount of attempts.
specified url, containing An Update. In case of an unsuccessful request,
Telegram will give up after a reasonable amount of attempts.
If you'd like to make sure that the Webhook was set by you, you can specify secret data in
the parameter :paramref:`secret_token`. If specified, the request will contain a header
@@ -5185,9 +5177,9 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
title: str,
description: str,
payload: str,
provider_token: Optional[str], # This arg is now optional as of Bot API 7.4
currency: str,
prices: Sequence["LabeledPrice"],
provider_token: Optional[str] = None,
start_parameter: Optional[str] = None,
photo_url: Optional[str] = None,
photo_size: Optional[int] = None,
@@ -5240,13 +5232,13 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
displayed to the user, use it for your internal processes.
provider_token (:obj:`str`, optional): Payments provider token, obtained via
provider_token (:obj:`str`): Payments provider token, obtained via
`@BotFather <https://t.me/BotFather>`_. Pass an empty string for payments in
|tg_stars|.
.. versionchanged:: 21.11
Bot API 7.4 made this parameter is optional and this is now reflected in the
function signature.
.. deprecated:: 21.3
As of Bot API 7.4, this parameter is now optional and future versions of the
library will make it optional as well.
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
<https://core.telegram.org/bots/payments#supported-currencies>`_. Pass ``XTR`` for
@@ -6972,7 +6964,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
``.WEBM`` video.
WEBM video.
.. versionadded:: 21.1
@@ -6986,7 +6978,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_THUMBNAIL_SIZE`
kilobytes in size; see
`the docs <https://core.telegram.org/stickers#animation-requirements>`_ for
animated sticker technical requirements, or a ``.WEBM`` video with the thumbnail up
animated sticker technical requirements, or a **.WEBM** video with the thumbnail up
to :tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_THUMBNAIL_SIZE`
kilobytes in size; see
`this <https://core.telegram.org/stickers#video-requirements>`_ for video sticker
@@ -7375,7 +7367,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:tg-const:`telegram.Poll.MAX_OPEN_PERIOD`. Can't be used together with
:paramref:`close_date`.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
close_date (:obj:`int` | :obj:`datetime.datetime`, optional): Point in time (Unix
timestamp) when the poll will be automatically closed. Must be at least
@@ -7982,7 +7974,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -8002,10 +7993,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the
original message was sent (or channel username in the format ``@channelusername``).
message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id.
video_start_timestamp (:obj:`int`, optional): New start timestamp for the
copied video in the message
.. versionadded:: 21.11
caption (:obj:`str`, optional): New caption for media,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing. If not specified, the original caption is kept.
@@ -8096,7 +8083,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"reply_parameters": reply_parameters,
"show_caption_above_media": show_caption_above_media,
"allow_paid_broadcast": allow_paid_broadcast,
"video_start_timestamp": video_start_timestamp,
}
result = await self._post(
@@ -8266,9 +8252,9 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
title: str,
description: str,
payload: str,
provider_token: Optional[str], # This arg is now optional as of Bot API 7.4
currency: str,
prices: Sequence["LabeledPrice"],
provider_token: Optional[str] = None,
max_tip_amount: Optional[int] = None,
suggested_tip_amounts: Optional[Sequence[int]] = None,
provider_data: Optional[Union[str, object]] = None,
@@ -8310,13 +8296,13 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
displayed to the user, use it for your internal processes.
provider_token (:obj:`str`, optional): Payments provider token, obtained via
provider_token (:obj:`str`): Payments provider token, obtained via
`@BotFather <https://t.me/BotFather>`_. Pass an empty string for payments in
|tg_stars|.
.. versionchanged:: 21.11
Bot API 7.4 made this parameter is optional and this is now reflected in the
function signature.
.. deprecated:: 21.3
As of Bot API 7.4, this parameter is now optional and future versions of the
library will make it optional as well.
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
<https://core.telegram.org/bots/payments#supported-currencies>`_. Pass ``XTR`` for
@@ -9288,8 +9274,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""
Use this method to change the chosen reactions on a message. Service messages of some types
can't be
Use this method to change the chosen reactions on a message. Service messages can't be
reacted to. Automatically forwarded messages from a channel to its discussion group have
the same available reactions as messages in the channel. Bots can't use paid reactions.
@@ -9576,7 +9561,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"is_canceled": is_canceled,
}
return await self._post(
"editUserStarSubscription",
"editUserStartSubscription",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -9722,7 +9707,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
active for before the next payment. Currently, it must always be
:tg-const:`telegram.constants.ChatSubscriptionLimit.SUBSCRIPTION_PERIOD` (30 days).
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
|time-period-input|
subscription_price (:obj:`int`): The number of Telegram Stars a user must pay initially
and after each subsequent subscription period to be a member of the chat;
@@ -9819,7 +9804,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Gifts:
"""Returns the list of gifts that can be sent by the bot to users and channel chats.
"""Returns the list of gifts that can be sent by the bot to users.
Requires no parameters.
.. versionadded:: 21.8
@@ -9843,13 +9828,12 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def send_gift(
self,
user_id: Optional[int] = None,
gift_id: Union[str, Gift] = None, # type: ignore
user_id: int,
gift_id: Union[str, Gift],
text: Optional[str] = None,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence["MessageEntity"]] = None,
pay_for_upgrade: Optional[bool] = None,
chat_id: Optional[Union[str, int]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -9857,23 +9841,15 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Sends a gift to the given user or channel chat.
The gift can't be converted to Telegram Stars by the receiver.
"""Sends a gift to the given user.
The gift can't be converted to Telegram Stars by the user
.. versionadded:: 21.8
Args:
user_id (:obj:`int`, optional): Required if :paramref:`chat_id` is not specified.
Unique identifier of the target user that will receive the gift.
.. versionchanged:: 21.11
Now optional.
user_id (:obj:`int`): Unique identifier of the target user that will receive the gift
gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a
:class:`~telegram.Gift` object
chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`user_id`
is not specified. |chat_id_channel| It will receive the gift.
.. versionadded:: 21.11
text (:obj:`str`, optional): Text that will be shown along with the gift;
0- :tg-const:`telegram.constants.GiftLimit.MAX_TEXT_LENGTH` characters
text_parse_mode (:obj:`str`, optional): Mode for parsing entities.
@@ -9900,11 +9876,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Raises:
:class:`telegram.error.TelegramError`
"""
# TODO: Remove when stability policy allows, tags: deprecated 21.11
# also we should raise a deprecation warnung if anything is passed by
# position since it will be moved, not sure how
if gift_id is None:
raise TypeError("Missing required argument `gift_id`.")
data: JSONDict = {
"user_id": user_id,
"gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id,
@@ -9912,7 +9883,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"text_parse_mode": text_parse_mode,
"text_entities": text_entities,
"pay_for_upgrade": pay_for_upgrade,
"chat_id": chat_id,
}
return await self._post(
"sendGift",
@@ -9935,7 +9905,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Verifies a chat |org-verify| which is represented by the bot.
"""Verifies a chat on behalf of the organization which is represented by the bot.
.. versionadded:: 21.10
@@ -9977,7 +9947,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Verifies a user |org-verify| which is represented by the bot.
"""Verifies a user on behalf of the organization which is represented by the bot.
.. versionadded:: 21.10
@@ -10018,8 +9988,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Removes verification from a chat that is currently verified |org-verify|
represented by the bot.
"""Removes verification from a chat that is currently verified on behalf of the
organization represented by the bot.
@@ -10057,8 +10027,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Removes verification from a user who is currently verified |org-verify|
represented by the bot.
"""Removes verification from a user who is currently verified on behalf of the
organization represented by the bot.
-2
View File
@@ -831,7 +831,6 @@ class CallbackQuery(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -865,7 +864,6 @@ class CallbackQuery(TelegramObject):
chat_id=chat_id,
caption=caption,
parse_mode=parse_mode,
video_start_timestamp=video_start_timestamp,
caption_entities=caption_entities,
disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id,
+3 -23
View File
@@ -1576,9 +1576,9 @@ class _ChatBase(TelegramObject):
title: str,
description: str,
payload: str,
provider_token: Optional[str],
currency: str,
prices: Sequence["LabeledPrice"],
provider_token: Optional[str] = None,
start_parameter: Optional[str] = None,
photo_url: Optional[str] = None,
photo_size: Optional[int] = None,
@@ -1940,8 +1940,6 @@ class _ChatBase(TelegramObject):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1980,8 +1978,6 @@ class _ChatBase(TelegramObject):
parse_mode=parse_mode,
supports_streaming=supports_streaming,
thumbnail=thumbnail,
cover=cover,
start_timestamp=start_timestamp,
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
@@ -2203,7 +2199,6 @@ class _ChatBase(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2230,7 +2225,6 @@ class _ChatBase(TelegramObject):
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -2263,7 +2257,6 @@ class _ChatBase(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2290,7 +2283,6 @@ class _ChatBase(TelegramObject):
chat_id=chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -2406,7 +2398,6 @@ class _ChatBase(TelegramObject):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2432,7 +2423,6 @@ class _ChatBase(TelegramObject):
chat_id=self.id,
from_chat_id=from_chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2450,7 +2440,6 @@ class _ChatBase(TelegramObject):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2477,7 +2466,6 @@ class _ChatBase(TelegramObject):
from_chat_id=self.id,
chat_id=chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -3474,25 +3462,18 @@ class _ChatBase(TelegramObject):
await bot.send_gift(user_id=update.effective_chat.id, *args, **kwargs )
or::
await bot.send_gift(chat_id=update.effective_chat.id, *args, **kwargs )
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
Caution:
Will only work if the chat is a private or channel chat, see :attr:`type`.
Can only work, if the chat is a private chat, see :attr:`type`.
.. versionadded:: 21.8
.. versionchanged:: 21.11
Added support for channel chats.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().send_gift(
user_id=self.id,
gift_id=gift_id,
text=text,
text_parse_mode=text_parse_mode,
@@ -3503,7 +3484,6 @@ class _ChatBase(TelegramObject):
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
**{"chat_id" if self.type == Chat.CHANNEL else "user_id": self.id},
)
async def verify(
+2 -2
View File
@@ -388,8 +388,8 @@ class BackgroundTypeWallpaper(BackgroundType):
class BackgroundTypePattern(BackgroundType):
"""
The background is a ``.PNG`` or ``.TGV`` (gzipped subset of ``SVG`` with ``MIME`` type
``"application/x-tgwallpattern"``) pattern to be combined with the background fill
The background is a `PNG` or `TGV` (gzipped subset of `SVG` with `MIME` type
`"application/x-tgwallpattern"`) pattern to be combined with the background fill
chosen by the user.
Objects of this class are comparable in terms of equality. Two objects of this class are
-9
View File
@@ -200,9 +200,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
Attributes:
id (:obj:`int`): Unique identifier for this chat.
@@ -357,9 +354,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
.. _accent colors: https://core.telegram.org/bots/api#accent-colors
.. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups
@@ -375,7 +369,6 @@ class ChatFullInfo(_ChatBase):
"business_intro",
"business_location",
"business_opening_hours",
"can_send_gift",
"can_send_paid_media",
"can_set_sticker_set",
"custom_emoji_sticker_set_name",
@@ -452,7 +445,6 @@ class ChatFullInfo(_ChatBase):
linked_chat_id: Optional[int] = None,
location: Optional[ChatLocation] = None,
can_send_paid_media: Optional[bool] = None,
can_send_gift: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -518,7 +510,6 @@ 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
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
+1 -2
View File
@@ -24,7 +24,6 @@ from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._telegramobject import TelegramObject
from telegram._user import User
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
@@ -100,7 +99,7 @@ class ChatMember(TelegramObject):
super().__init__(api_kwargs=api_kwargs)
# Required by all subclasses
self.user: User = user
self.status: str = enum.get_member(constants.ChatMemberStatus, status, status)
self.status: str = status
self._id_attrs = (self.user, self.status)
+1 -51
View File
@@ -214,13 +214,6 @@ class InputPaidMediaVideo(InputPaidMedia):
Lastly you can pass an existing :class:`telegram.Video` object to send.
thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): |thumbdocstringnopath|
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): Cover for the video in the message. |fileinputnopath|
.. versionchanged:: 21.11
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message
.. versionchanged:: 21.11
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
duration (:obj:`int`, optional): Video duration in seconds.
@@ -232,13 +225,6 @@ class InputPaidMediaVideo(InputPaidMedia):
:tg-const:`telegram.constants.InputPaidMediaType.VIDEO`.
media (:obj:`str` | :class:`telegram.InputFile`): Video to send.
thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase|
cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message.
|fileinputnopath|
.. versionchanged:: 21.11
start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message
.. versionchanged:: 21.11
width (:obj:`int`): Optional. Video width.
height (:obj:`int`): Optional. Video height.
duration (:obj:`int`): Optional. Video duration in seconds.
@@ -246,15 +232,7 @@ class InputPaidMediaVideo(InputPaidMedia):
suitable for streaming.
"""
__slots__ = (
"cover",
"duration",
"height",
"start_timestamp",
"supports_streaming",
"thumbnail",
"width",
)
__slots__ = ("duration", "height", "supports_streaming", "thumbnail", "width")
def __init__(
self,
@@ -264,8 +242,6 @@ class InputPaidMediaVideo(InputPaidMedia):
height: Optional[int] = None,
duration: Optional[int] = None,
supports_streaming: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -288,10 +264,6 @@ class InputPaidMediaVideo(InputPaidMedia):
self.height: Optional[int] = height
self.duration: Optional[int] = duration
self.supports_streaming: Optional[bool] = supports_streaming
self.cover: Optional[Union[InputFile, str]] = (
parse_file_input(cover, attach=True, local_mode=True) if cover else None
)
self.start_timestamp: Optional[int] = start_timestamp
class InputMediaAnimation(InputMedia):
@@ -564,13 +536,6 @@ class InputMediaVideo(InputMedia):
optional): |thumbdocstringnopath|
.. versionadded:: 20.2
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): Cover for the video in the message. |fileinputnopath|
.. versionchanged:: 21.11
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message
.. versionchanged:: 21.11
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
.. versionadded:: 21.3
@@ -603,22 +568,13 @@ class InputMediaVideo(InputMedia):
show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med|
.. versionadded:: 21.3
cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message.
|fileinputnopath|
.. versionchanged:: 21.11
start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message
.. versionchanged:: 21.11
"""
__slots__ = (
"cover",
"duration",
"has_spoiler",
"height",
"show_caption_above_media",
"start_timestamp",
"supports_streaming",
"thumbnail",
"width",
@@ -638,8 +594,6 @@ class InputMediaVideo(InputMedia):
has_spoiler: Optional[bool] = None,
thumbnail: Optional[FileInput] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -671,10 +625,6 @@ class InputMediaVideo(InputMedia):
self.supports_streaming: Optional[bool] = supports_streaming
self.has_spoiler: Optional[bool] = has_spoiler
self.show_caption_above_media: Optional[bool] = show_caption_above_media
self.cover: Optional[Union[InputFile, str]] = (
parse_file_input(cover, attach=True, local_mode=True) if cover else None
)
self.start_timestamp: Optional[int] = start_timestamp
class InputMediaAudio(InputMedia):
+4 -4
View File
@@ -61,8 +61,8 @@ class InputSticker(TelegramObject):
format (:obj:`str`): Format of the added sticker, must be one of
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
``.WEBM`` video.
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM
video.
.. versionadded:: 21.1
@@ -84,8 +84,8 @@ class InputSticker(TelegramObject):
format (:obj:`str`): Format of the added sticker, must be one of
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
``.WEBM`` video.
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM
video.
.. versionadded:: 21.1
"""
+2 -42
View File
@@ -17,17 +17,12 @@
# 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 Telegram Video."""
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional
from typing import Optional
from telegram._files._basethumbedmedium import _BaseThumbedMedium
from telegram._files.photosize import PhotoSize
from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class Video(_BaseThumbedMedium):
"""This object represents a video file.
@@ -53,13 +48,6 @@ class Video(_BaseThumbedMedium):
thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail.
.. versionadded:: 20.2
cover (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the cover of
the video in the message.
.. versionadded:: 21.11
start_timestamp (:obj:`int`, optional): Timestamp in seconds from which the video
will play in the message
.. versionadded:: 21.11
Attributes:
file_id (:obj:`str`): Identifier for this file, which can be used to download
@@ -76,24 +64,9 @@ class Video(_BaseThumbedMedium):
thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
.. versionadded:: 20.2
cover (tuple[:class:`telegram.PhotoSize`]): Optional, Available sizes of the cover of
the video in the message.
.. versionadded:: 21.11
start_timestamp (:obj:`int`): Optional, Timestamp in seconds from which the video
will play in the message
.. versionadded:: 21.11
"""
__slots__ = (
"cover",
"duration",
"file_name",
"height",
"mime_type",
"start_timestamp",
"width",
)
__slots__ = ("duration", "file_name", "height", "mime_type", "width")
def __init__(
self,
@@ -106,8 +79,6 @@ class Video(_BaseThumbedMedium):
file_size: Optional[int] = None,
file_name: Optional[str] = None,
thumbnail: Optional[PhotoSize] = None,
cover: Optional[Sequence[PhotoSize]] = None,
start_timestamp: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -126,14 +97,3 @@ class Video(_BaseThumbedMedium):
# Optional
self.mime_type: Optional[str] = mime_type
self.file_name: Optional[str] = file_name
self.cover: Optional[Sequence[PhotoSize]] = parse_sequence_arg(cover)
self.start_timestamp: Optional[int] = start_timestamp
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Video":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
data["cover"] = de_list_optional(data.get("cover"), PhotoSize, bot)
return super().de_json(data=data, bot=bot)
+10 -6
View File
@@ -62,12 +62,6 @@ class InlineQuery(TelegramObject):
``auto_pagination``. Use a named argument for those,
and notice that some positional arguments changed position as a result.
.. versionchanged:: 22.0
Removed constants ``MIN_START_PARAMETER_LENGTH`` and ``MAX_START_PARAMETER_LENGTH``.
Use :attr:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` and
:attr:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`
instead.
Args:
id (:obj:`str`): Unique identifier for this query.
from_user (:class:`telegram.User`): Sender.
@@ -208,6 +202,16 @@ class InlineQuery(TelegramObject):
.. versionadded:: 13.2
"""
MIN_SWITCH_PM_TEXT_LENGTH: Final[int] = constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH
""":const:`telegram.constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH`
.. versionadded:: 20.0
"""
MAX_SWITCH_PM_TEXT_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH
""":const:`telegram.constants.InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH`
.. versionadded:: 20.0
"""
MAX_OFFSET_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_OFFSET_LENGTH
""":const:`telegram.constants.InlineQueryLimit.MAX_OFFSET_LENGTH`
+25 -5
View File
@@ -23,7 +23,9 @@ from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
from telegram._utils.types import JSONDict
from telegram._utils.warnings import warn
from telegram.constants import InlineQueryResultType
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import InputMessageContent
@@ -38,9 +40,6 @@ class InlineQueryResultArticle(InlineQueryResult):
.. versionchanged:: 20.5
Removed the deprecated arguments and attributes ``thumb_*``.
.. versionchanged:: 21.11
Removed the deprecated argument and attribute ``hide_url``.
Args:
id (:obj:`str`): Unique identifier for this result,
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
@@ -51,9 +50,12 @@ class InlineQueryResultArticle(InlineQueryResult):
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
url (:obj:`str`, optional): URL of the result.
hide_url (:obj:`bool`, optional): Pass :obj:`True`, if you don't want the URL to be shown
in the message.
Tip:
Pass an empty string as URL if you don't want the URL to be shown in the message.
.. deprecated:: 21.10
This attribute will be removed in future PTB versions. Pass an empty string as URL
instead.
description (:obj:`str`, optional): Short description of the result.
thumbnail_url (:obj:`str`, optional): Url of the thumbnail for the result.
@@ -76,6 +78,12 @@ class InlineQueryResultArticle(InlineQueryResult):
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
url (:obj:`str`): Optional. URL of the result.
hide_url (:obj:`bool`): Optional. Pass :obj:`True`, if you don't want the URL to be shown
in the message.
.. deprecated:: 21.10
This attribute will be removed in future PTB versions. Pass an empty string as URL
instead.
description (:obj:`str`): Optional. Short description of the result.
thumbnail_url (:obj:`str`): Optional. Url of the thumbnail for the result.
@@ -91,6 +99,7 @@ class InlineQueryResultArticle(InlineQueryResult):
__slots__ = (
"description",
"hide_url",
"input_message_content",
"reply_markup",
"thumbnail_height",
@@ -107,6 +116,7 @@ class InlineQueryResultArticle(InlineQueryResult):
input_message_content: "InputMessageContent",
reply_markup: Optional[InlineKeyboardMarkup] = None,
url: Optional[str] = None,
hide_url: Optional[bool] = None,
description: Optional[str] = None,
thumbnail_url: Optional[str] = None,
thumbnail_width: Optional[int] = None,
@@ -123,6 +133,16 @@ class InlineQueryResultArticle(InlineQueryResult):
# Optional
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.url: Optional[str] = url
if hide_url is not None:
warn(
PTBDeprecationWarning(
"21.10",
"The argument `hide_url` will be removed in future PTB"
"versions. Pass an empty string as URL instead.",
),
stacklevel=2,
)
self.hide_url: Optional[bool] = hide_url
self.description: Optional[str] = description
self.thumbnail_url: Optional[str] = thumbnail_url
self.thumbnail_width: Optional[int] = thumbnail_width
+2 -2
View File
@@ -47,7 +47,7 @@ class InlineQueryResultGif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result,
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
gif_url (:obj:`str`): A valid URL for the GIF file.
gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB.
gif_width (:obj:`int`, optional): Width of the GIF.
gif_height (:obj:`int`, optional): Height of the GIF.
gif_duration (:obj:`int`, optional): Duration of the GIF in seconds.
@@ -86,7 +86,7 @@ class InlineQueryResultGif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result,
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
gif_url (:obj:`str`): A valid URL for the GIF file.
gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB.
gif_width (:obj:`int`): Optional. Width of the GIF.
gif_height (:obj:`int`): Optional. Height of the GIF.
gif_duration (:obj:`int`): Optional. Duration of the GIF in seconds.
@@ -48,7 +48,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result,
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
mpeg4_width (:obj:`int`, optional): Video width.
mpeg4_height (:obj:`int`, optional): Video height.
mpeg4_duration (:obj:`int`, optional): Video duration in seconds.
@@ -88,7 +88,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result,
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
mpeg4_width (:obj:`int`): Optional. Video width.
mpeg4_height (:obj:`int`): Optional. Video height.
mpeg4_duration (:obj:`int`): Optional. Video duration in seconds.
+7 -8
View File
@@ -47,10 +47,9 @@ class InlineQueryResultsButton(TelegramObject):
inside the Web App.
start_parameter (:obj:`str`, optional): Deep-linking parameter for the
:guilabel:`/start` message sent to the bot when user presses the switch button.
:tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`
-
:tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`
characters, only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
:tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`-
:tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters,
only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
Example:
An inline bot that sends YouTube videos can ask the user to connect the bot to
@@ -68,10 +67,10 @@ class InlineQueryResultsButton(TelegramObject):
user presses the button. The Web App will be able to switch back to the inline mode
using the method ``web_app_switch_inline_query`` inside the Web App.
start_parameter (:obj:`str`): Optional. Deep-linking parameter for the
:tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`
-
:tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`
characters, only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
:guilabel:`/start` message sent to the bot when user presses the switch button.
:tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`-
:tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters,
only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
"""
@@ -35,11 +35,9 @@ class InputInvoiceMessageContent(InputMessageContent):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`title`, :attr:`description`, :attr:`payload`,
:attr:`currency` and :attr:`prices` are equal.
:attr:`provider_token`, :attr:`currency` and :attr:`prices` are equal.
.. versionadded:: 13.5
.. versionchanged:: 21.11
:attr:`provider_token` is no longer considered for equality comparison.
Args:
title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
@@ -51,13 +49,13 @@ class InputInvoiceMessageContent(InputMessageContent):
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed
to the user, use it for your internal processes.
provider_token (:obj:`str`, optional): Payment provider token, obtained via
provider_token (:obj:`str`): Payment provider token, obtained via
`@Botfather <https://t.me/Botfather>`_. Pass an empty string for payments in
|tg_stars|.
.. versionchanged:: 21.11
Bot API 7.4 made this parameter is optional and this is now reflected in the
class signature.
.. deprecated:: 21.3
As of Bot API 7.4, this parameter is now optional and future versions of the
library will make it optional as well.
currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on
`currencies <https://core.telegram.org/bots/payments#supported-currencies>`_.
Pass ``XTR`` for payments in |tg_stars|.
@@ -201,9 +199,9 @@ class InputInvoiceMessageContent(InputMessageContent):
title: str,
description: str,
payload: str,
provider_token: Optional[str], # This arg is now optional since Bot API 7.4
currency: str,
prices: Sequence[LabeledPrice],
provider_token: Optional[str] = None,
max_tip_amount: Optional[int] = None,
suggested_tip_amounts: Optional[Sequence[int]] = None,
provider_data: Optional[str] = None,
@@ -227,10 +225,10 @@ class InputInvoiceMessageContent(InputMessageContent):
self.title: str = title
self.description: str = description
self.payload: str = payload
self.provider_token: Optional[str] = provider_token
self.currency: str = currency
self.prices: tuple[LabeledPrice, ...] = parse_sequence_arg(prices)
# Optionals
self.provider_token: Optional[str] = provider_token
self.max_tip_amount: Optional[int] = max_tip_amount
self.suggested_tip_amounts: tuple[int, ...] = parse_sequence_arg(suggested_tip_amounts)
self.provider_data: Optional[str] = provider_data
@@ -250,6 +248,7 @@ class InputInvoiceMessageContent(InputMessageContent):
self.title,
self.description,
self.payload,
self.provider_token,
self.currency,
self.prices,
)
+157 -107
View File
@@ -930,9 +930,6 @@ class Message(MaybeInaccessibleMessage):
.. |reply_same_thread| replace:: If :paramref:`message_thread_id` is not provided,
this will reply to the same thread (topic) of the original message.
.. |quote_removed| replace:: Removed deprecated parameter ``quote``. Use :paramref:`do_quote`
instead.
"""
# fmt: on
@@ -1487,17 +1484,22 @@ class Message(MaybeInaccessibleMessage):
return self._effective_attachment # type: ignore[return-value]
def _do_quote(self, do_quote: Optional[bool]) -> Optional[ReplyParameters]:
def _quote(
self, quote: Optional[bool], reply_to_message_id: Optional[int] = None
) -> Optional[ReplyParameters]:
"""Modify kwargs for replying with or without quoting."""
if do_quote is not None:
if do_quote:
if reply_to_message_id is not None:
return ReplyParameters(reply_to_message_id)
if quote is not None:
if quote:
return ReplyParameters(self.message_id)
else:
# Unfortunately we need some ExtBot logic here because it's hard to move shortcut
# logic into ExtBot
if hasattr(self.get_bot(), "defaults") and self.get_bot().defaults: # type: ignore
default_quote = self.get_bot().defaults.do_quote # type: ignore[attr-defined]
default_quote = self.get_bot().defaults.quote # type: ignore[attr-defined]
else:
default_quote = None
if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote:
@@ -1673,14 +1675,29 @@ class Message(MaybeInaccessibleMessage):
async def _parse_quote_arguments(
self,
do_quote: Optional[Union[bool, _ReplyKwargs]],
quote: Optional[bool],
reply_to_message_id: Optional[int],
reply_parameters: Optional["ReplyParameters"],
) -> tuple[Union[str, int], ReplyParameters]:
if quote and do_quote:
raise ValueError("The arguments `quote` and `do_quote` are mutually exclusive")
if reply_to_message_id is not None and reply_parameters is not None:
raise ValueError(
"`reply_to_message_id` and `reply_parameters` are mutually exclusive."
)
if quote is not None:
warn(
PTBDeprecationWarning(
"20.8",
"The `quote` parameter is deprecated in favor of the `do_quote` parameter. "
"Please update your code to use `do_quote` instead.",
),
stacklevel=2,
)
effective_do_quote = quote or do_quote
chat_id: Union[str, int] = self.chat_id
# reply_parameters and reply_to_message_id overrule the do_quote parameter
@@ -1688,11 +1705,11 @@ class Message(MaybeInaccessibleMessage):
effective_reply_parameters = reply_parameters
elif reply_to_message_id is not None:
effective_reply_parameters = ReplyParameters(message_id=reply_to_message_id)
elif isinstance(do_quote, dict):
effective_reply_parameters = do_quote["reply_parameters"]
chat_id = do_quote["chat_id"]
elif isinstance(effective_do_quote, dict):
effective_reply_parameters = effective_do_quote["reply_parameters"]
chat_id = effective_do_quote["chat_id"]
else:
effective_reply_parameters = self._do_quote(do_quote)
effective_reply_parameters = self._quote(effective_do_quote)
return chat_id, effective_reply_parameters
@@ -1733,6 +1750,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
disable_web_page_preview: Optional[bool] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1755,10 +1773,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -1769,7 +1788,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_message(
@@ -1811,6 +1830,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
disable_web_page_preview: Optional[bool] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1836,14 +1856,15 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Note:
:tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by
Telegram for backward compatibility. You should use :meth:`reply_markdown_v2` instead.
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -1853,7 +1874,7 @@ class Message(MaybeInaccessibleMessage):
: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
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_message(
@@ -1895,6 +1916,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
disable_web_page_preview: Optional[bool] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1920,10 +1942,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -1933,7 +1956,7 @@ class Message(MaybeInaccessibleMessage):
: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
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_message(
@@ -1975,6 +1998,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
disable_web_page_preview: Optional[bool] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2000,10 +2024,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2013,7 +2038,7 @@ class Message(MaybeInaccessibleMessage):
: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
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_message(
@@ -2053,6 +2078,7 @@ class Message(MaybeInaccessibleMessage):
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2078,10 +2104,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2094,7 +2121,7 @@ class Message(MaybeInaccessibleMessage):
:class:`telegram.error.TelegramError`
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_media_group(
@@ -2137,6 +2164,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
filename: Optional[str] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2159,10 +2187,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2173,7 +2202,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_photo(
@@ -2222,6 +2251,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
filename: Optional[str] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2244,10 +2274,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2258,7 +2289,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_audio(
@@ -2307,6 +2338,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
filename: Optional[str] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2329,10 +2361,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2343,7 +2376,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_document(
@@ -2394,6 +2427,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
filename: Optional[str] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2416,10 +2450,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2430,7 +2465,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_animation(
@@ -2476,6 +2511,7 @@ class Message(MaybeInaccessibleMessage):
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2498,10 +2534,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2512,7 +2549,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_sticker(
@@ -2555,12 +2592,11 @@ class Message(MaybeInaccessibleMessage):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
filename: Optional[str] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2583,10 +2619,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2597,7 +2634,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_video(
@@ -2624,8 +2661,6 @@ class Message(MaybeInaccessibleMessage):
message_thread_id=message_thread_id,
has_spoiler=has_spoiler,
thumbnail=thumbnail,
cover=cover,
start_timestamp=start_timestamp,
business_connection_id=self.business_connection_id,
message_effect_id=message_effect_id,
allow_paid_broadcast=allow_paid_broadcast,
@@ -2649,6 +2684,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
filename: Optional[str] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2671,10 +2707,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2685,7 +2722,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_video_note(
@@ -2729,6 +2766,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
filename: Optional[str] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2751,10 +2789,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2765,7 +2804,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_voice(
@@ -2811,6 +2850,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
location: Optional[Location] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2833,10 +2873,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2847,7 +2888,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_location(
@@ -2896,6 +2937,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
venue: Optional[Venue] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2918,10 +2960,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -2932,7 +2975,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_venue(
@@ -2979,6 +3022,7 @@ class Message(MaybeInaccessibleMessage):
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
contact: Optional[Contact] = None,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3001,10 +3045,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -3015,7 +3060,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_contact(
@@ -3067,6 +3112,7 @@ class Message(MaybeInaccessibleMessage):
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3089,10 +3135,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -3103,7 +3150,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_poll(
@@ -3151,6 +3198,7 @@ class Message(MaybeInaccessibleMessage):
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3173,10 +3221,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -3187,7 +3236,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_dice(
@@ -3266,6 +3315,7 @@ class Message(MaybeInaccessibleMessage):
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3288,10 +3338,11 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -3304,7 +3355,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_game(
@@ -3331,9 +3382,9 @@ class Message(MaybeInaccessibleMessage):
title: str,
description: str,
payload: str,
provider_token: Optional[str],
currency: str,
prices: Sequence["LabeledPrice"],
provider_token: Optional[str] = None,
start_parameter: Optional[str] = None,
photo_url: Optional[str] = None,
photo_size: Optional[int] = None,
@@ -3359,6 +3410,7 @@ class Message(MaybeInaccessibleMessage):
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3380,9 +3432,6 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Warning:
As of API 5.2 :paramref:`start_parameter <telegram.Bot.send_invoice.start_parameter>`
is an optional argument and therefore the
@@ -3396,6 +3445,10 @@ class Message(MaybeInaccessibleMessage):
:paramref:`start_parameter <telegram.Bot.send_invoice.start_parameter>` is optional.
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -3406,7 +3459,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_invoice(
@@ -3453,7 +3506,6 @@ class Message(MaybeInaccessibleMessage):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3488,7 +3540,6 @@ class Message(MaybeInaccessibleMessage):
chat_id=chat_id,
from_chat_id=self.chat_id,
message_id=self.message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
protect_content=protect_content,
message_thread_id=message_thread_id,
@@ -3512,7 +3563,6 @@ class Message(MaybeInaccessibleMessage):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3543,7 +3593,6 @@ class Message(MaybeInaccessibleMessage):
from_chat_id=self.chat_id,
message_id=self.message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -3576,10 +3625,10 @@ class Message(MaybeInaccessibleMessage):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
quote: Optional[bool] = None,
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3602,10 +3651,12 @@ class Message(MaybeInaccessibleMessage):
.. versionchanged:: 21.1
|reply_same_thread|
.. versionchanged:: 22.0
|quote_removed|
Keyword Args:
quote (:obj:`bool`, optional): |reply_quote|
.. versionadded:: 13.1
.. deprecated:: 20.8
This argument is deprecated in favor of :paramref:`do_quote`
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Mutually exclusive with :paramref:`quote`.
@@ -3616,7 +3667,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, quote, reply_to_message_id, reply_parameters
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().copy_message(
@@ -3624,7 +3675,6 @@ class Message(MaybeInaccessibleMessage):
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -3688,7 +3738,7 @@ class Message(MaybeInaccessibleMessage):
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters
do_quote, None, reply_to_message_id, reply_parameters
)
return await self.get_bot().send_paid_media(
chat_id=chat_id,
@@ -164,6 +164,10 @@ class EncryptedPassportElement(TelegramObject):
reverse_side: Optional[PassportFile] = None,
selfie: Optional[PassportFile] = None,
translation: Optional[Sequence[PassportFile]] = None,
# TODO: Remove the credentials argument in 22.0 or later
credentials: Optional[ # pylint: disable=unused-argument # noqa: ARG002
"Credentials"
] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
+59 -26
View File
@@ -19,12 +19,12 @@
# pylint: disable=redefined-builtin
"""This module contains the classes that represent Telegram PassportElementError."""
from collections.abc import Sequence
from typing import Optional
from telegram._telegramobject import TelegramObject
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.types import JSONDict
from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning
class PassportElementError(TelegramObject):
@@ -168,30 +168,23 @@ class PassportElementErrorFiles(PassportElementError):
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``,
``"passport_registration"``, ``"temporary_registration"``.
file_hashes (Sequence[:obj:`str`]): List of base64-encoded file hashes.
.. versionchanged:: 22.0
|sequenceargs|
file_hashes (list[:obj:`str`]): List of base64-encoded file hashes.
message (:obj:`str`): Error message.
Attributes:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``,
``"passport_registration"``, ``"temporary_registration"``.
file_hashes (tuple[:obj:`str`]): List of base64-encoded file hashes.
.. versionchanged:: 22.0
|tupleclassattrs|
message (:obj:`str`): Error message.
"""
__slots__ = ("file_hashes",)
__slots__ = ("_file_hashes",)
def __init__(
self,
type: str,
file_hashes: Sequence[str],
file_hashes: list[str],
message: str,
*,
api_kwargs: Optional[JSONDict] = None,
@@ -199,9 +192,32 @@ class PassportElementErrorFiles(PassportElementError):
# Required
super().__init__("files", type, message, api_kwargs=api_kwargs)
with self._unfrozen():
self.file_hashes: tuple[str, ...] = parse_sequence_arg(file_hashes)
self._file_hashes: list[str] = file_hashes
self._id_attrs = (self.source, self.type, self.message, self.file_hashes)
self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes))
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict` for details."""
data = super().to_dict(recursive)
data["file_hashes"] = self._file_hashes
return data
@property
def file_hashes(self) -> list[str]:
"""List of base64-encoded file hashes.
.. deprecated:: 20.6
This attribute will return a tuple instead of a list in future major versions.
"""
warn(
PTBDeprecationWarning(
"20.6",
"The attribute `file_hashes` will return a tuple instead of a list in future major"
" versions.",
),
stacklevel=2,
)
return self._file_hashes
class PassportElementErrorFrontSide(PassportElementError):
@@ -370,10 +386,7 @@ class PassportElementErrorTranslationFiles(PassportElementError):
one of ``"passport"``, ``"driver_license"``, ``"identity_card"``,
``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``,
``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``.
file_hashes (Sequence[:obj:`str`]): List of base64-encoded file hashes.
.. versionchanged:: 22.0
|sequenceargs|
file_hashes (list[:obj:`str`]): List of base64-encoded file hashes.
message (:obj:`str`): Error message.
Attributes:
@@ -381,20 +394,16 @@ class PassportElementErrorTranslationFiles(PassportElementError):
one of ``"passport"``, ``"driver_license"``, ``"identity_card"``,
``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``,
``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``.
file_hashes (tuple[:obj:`str`]): List of base64-encoded file hashes.
.. versionchanged:: 22.0
|tupleclassattrs|
message (:obj:`str`): Error message.
"""
__slots__ = ("file_hashes",)
__slots__ = ("_file_hashes",)
def __init__(
self,
type: str,
file_hashes: Sequence[str],
file_hashes: list[str],
message: str,
*,
api_kwargs: Optional[JSONDict] = None,
@@ -402,9 +411,33 @@ class PassportElementErrorTranslationFiles(PassportElementError):
# Required
super().__init__("translation_files", type, message, api_kwargs=api_kwargs)
with self._unfrozen():
self.file_hashes: tuple[str, ...] = parse_sequence_arg(file_hashes)
self._file_hashes: list[str] = file_hashes
self._id_attrs = (self.source, self.type, self.message, self.file_hashes)
self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes))
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict` for details."""
data = super().to_dict(recursive)
data["file_hashes"] = self._file_hashes
return data
@property
def file_hashes(self) -> list[str]:
"""List of base64-encoded file hashes.
.. deprecated:: 20.6
This attribute will return a tuple instead of a list in future major versions.
"""
warn(
PTBDeprecationWarning(
"20.6",
"The attribute `file_hashes` will return a tuple instead of a list in future major"
" versions. See the stability policy:"
" https://docs.python-telegram-bot.org/en/stable/stability_policy.html",
),
stacklevel=2,
)
return self._file_hashes
class PassportElementErrorUnspecified(PassportElementError):
+29 -22
View File
@@ -18,13 +18,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Encrypted PassportFile."""
import datetime as dtm
from typing import TYPE_CHECKING, Optional
from telegram._telegramobject import TelegramObject
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.types import JSONDict, ODVInput
from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import Bot, File, FileCredentials
@@ -45,11 +45,11 @@ class PassportFile(TelegramObject):
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
file_size (:obj:`int`): File size in bytes.
file_date (:class:`datetime.datetime`): Time when the file was uploaded.
file_date (:obj:`int`): Unix time when the file was uploaded.
.. versionchanged:: 22.0
Accepts only :class:`datetime.datetime` instead of :obj:`int`.
|datetime_localization|
.. deprecated:: 20.6
This argument will only accept a datetime instead of an integer in future
major versions.
Attributes:
file_id (:obj:`str`): Identifier for this file, which can be used to download
@@ -58,16 +58,11 @@ class PassportFile(TelegramObject):
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
file_size (:obj:`int`): File size in bytes.
file_date (:class:`datetime.datetime`): Time when the file was uploaded.
.. versionchanged:: 22.0
Returns :class:`datetime.datetime` instead of :obj:`int`.
|datetime_localization|
"""
__slots__ = (
"_credentials",
"file_date",
"_file_date",
"file_id",
"file_size",
"file_unique_id",
@@ -77,7 +72,7 @@ class PassportFile(TelegramObject):
self,
file_id: str,
file_unique_id: str,
file_date: dtm.datetime,
file_date: int,
file_size: int,
credentials: Optional["FileCredentials"] = None,
*,
@@ -89,7 +84,7 @@ class PassportFile(TelegramObject):
self.file_id: str = file_id
self.file_unique_id: str = file_unique_id
self.file_size: int = file_size
self.file_date: dtm.datetime = file_date
self._file_date: int = file_date
# Optionals
self._credentials: Optional[FileCredentials] = credentials
@@ -98,16 +93,28 @@ class PassportFile(TelegramObject):
self._freeze()
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PassportFile":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict` for details."""
data = super().to_dict(recursive)
data["file_date"] = self._file_date
return data
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
data["file_date"] = from_timestamp(data.get("file_date"), tzinfo=loc_tzinfo)
@property
def file_date(self) -> int:
""":obj:`int`: Unix time when the file was uploaded.
return super().de_json(data=data, bot=bot)
.. deprecated:: 20.6
This attribute will return a datetime instead of a integer in future major versions.
"""
warn(
PTBDeprecationWarning(
"20.6",
"The attribute `file_date` will return a datetime instead of an integer in future"
" major versions.",
),
stacklevel=2,
)
return self._file_date
@classmethod
def de_json_decrypted(
@@ -36,9 +36,6 @@ if TYPE_CHECKING:
class StarTransaction(TelegramObject):
"""Describes a Telegram Star transaction.
Note that if the buyer initiates a chargeback with the payment provider from whom they
acquired Stars (e.g., Apple, Google) following this transaction, the refunded Stars will be
deducted from the bot's balance. This is outside of Telegram's control.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id`, :attr:`source`, and :attr:`receiver` are equal.
@@ -23,7 +23,6 @@ from collections.abc import Sequence
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._chat import Chat
from telegram._gifts import Gift
from telegram._paidmedia import PaidMedia
from telegram._telegramobject import TelegramObject
@@ -44,7 +43,6 @@ class TransactionPartner(TelegramObject):
transactions. Currently, it can be one of:
* :class:`TransactionPartnerUser`
* :class:`TransactionPartnerChat`
* :class:`TransactionPartnerAffiliateProgram`
* :class:`TransactionPartnerFragment`
* :class:`TransactionPartnerTelegramAds`
@@ -56,9 +54,6 @@ class TransactionPartner(TelegramObject):
.. versionadded:: 21.4
..versionchanged:: 21.11
Added :class:`TransactionPartnerChat`
Args:
type (:obj:`str`): The type of the transaction partner.
@@ -73,11 +68,6 @@ class TransactionPartner(TelegramObject):
.. versionadded:: 21.9
"""
CHAT: Final[str] = constants.TransactionPartnerType.CHAT
""":const:`telegram.constants.TransactionPartnerType.CHAT`
.. versionadded:: 21.11
"""
FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT
""":const:`telegram.constants.TransactionPartnerType.FRAGMENT`"""
OTHER: Final[str] = constants.TransactionPartnerType.OTHER
@@ -113,7 +103,6 @@ class TransactionPartner(TelegramObject):
_class_mapping: dict[str, type[TransactionPartner]] = {
cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram,
cls.CHAT: TransactionPartnerChat,
cls.FRAGMENT: TransactionPartnerFragment,
cls.USER: TransactionPartnerUser,
cls.TELEGRAM_ADS: TransactionPartnerTelegramAds,
@@ -182,60 +171,6 @@ class TransactionPartnerAffiliateProgram(TransactionPartner):
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerChat(TransactionPartner):
"""Describes a transaction with a chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`chat` are equal.
.. versionadded:: 21.11
Args:
chat (:class:`telegram.Chat`): Information about the chat.
gift (:class:`telegram.Gift`, optional): The gift sent to the chat by the bot.
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.CHAT`.
chat (:class:`telegram.Chat`): Information about the chat.
gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot.
"""
__slots__ = (
"chat",
"gift",
)
def __init__(
self,
chat: Chat,
gift: Optional[Gift] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.CHAT, api_kwargs=api_kwargs)
with self._unfrozen():
self.chat: Chat = chat
self.gift: Optional[Gift] = gift
self._id_attrs = (
self.type,
self.chat,
)
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPartnerChat":
"""See :meth:`telegram.TransactionPartner.de_json`."""
data = cls._parse_data(data)
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
data["gift"] = de_json_optional(data.get("gift"), Gift, bot)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerFragment(TransactionPartner):
"""Describes a withdrawal transaction with Fragment.
-3
View File
@@ -33,9 +33,6 @@ if TYPE_CHECKING:
class SuccessfulPayment(TelegramObject):
"""This object contains basic information about a successful payment.
Note that if the buyer initiates a chargeback with the relevant payment provider following
this transaction, the funds may be debited from your balance. This is outside of
Telegram's control.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`telegram_payment_charge_id` and
+2 -15
View File
@@ -1018,9 +1018,9 @@ class User(TelegramObject):
title: str,
description: str,
payload: str,
provider_token: Optional[str],
currency: str,
prices: Sequence["LabeledPrice"],
provider_token: Optional[str] = None,
start_parameter: Optional[str] = None,
photo_url: Optional[str] = None,
photo_size: Optional[int] = None,
@@ -1328,8 +1328,6 @@ class User(TelegramObject):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1371,8 +1369,6 @@ class User(TelegramObject):
parse_mode=parse_mode,
supports_streaming=supports_streaming,
thumbnail=thumbnail,
cover=cover,
start_timestamp=start_timestamp,
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
@@ -1674,7 +1670,7 @@ class User(TelegramObject):
) -> bool:
"""Shortcut for::
await bot.send_gift(user_id=update.effective_user.id, *args, **kwargs )
await bot.send_gift( user_id=update.effective_user.id, *args, **kwargs )
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
@@ -1684,7 +1680,6 @@ class User(TelegramObject):
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().send_gift(
chat_id=None,
user_id=self.id,
gift_id=gift_id,
text=text,
@@ -1712,7 +1707,6 @@ class User(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1740,7 +1734,6 @@ class User(TelegramObject):
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -1773,7 +1766,6 @@ class User(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1801,7 +1793,6 @@ class User(TelegramObject):
chat_id=chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -1917,7 +1908,6 @@ class User(TelegramObject):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1943,7 +1933,6 @@ class User(TelegramObject):
chat_id=self.id,
from_chat_id=from_chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -1961,7 +1950,6 @@ class User(TelegramObject):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1988,7 +1976,6 @@ class User(TelegramObject):
from_chat_id=self.id,
chat_id=chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
+1 -1
View File
@@ -51,6 +51,6 @@ class Version(NamedTuple):
__version_info__: Final[Version] = Version(
major=22, minor=0, micro=0, releaselevel="final", serial=0
major=21, minor=10, micro=0, releaselevel="final", serial=0
)
__version__: Final[str] = str(__version_info__)
+23 -21
View File
@@ -155,7 +155,7 @@ class _AccentColor(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=3)
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=2)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@@ -1236,12 +1236,9 @@ class GiftLimit(IntEnum):
__slots__ = ()
MAX_TEXT_LENGTH = 128
MAX_TEXT_LENGTH = 255
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
:paramref:`~telegram.Bot.send_gift.text` parameter of :meth:`~telegram.Bot.send_gift`.
.. versionchanged:: 21.11
Updated Value to 128 based on Bot API 8.3
"""
@@ -1375,12 +1372,6 @@ class InlineQueryLimit(IntEnum):
of :class:`int` and can be treated as such.
.. versionadded:: 20.0
.. versionchanged:: 22.0
Removed deprecated attributes ``InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH`` and
``InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH``. Please instead use
:attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` and
:attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`.
"""
__slots__ = ()
@@ -1395,6 +1386,22 @@ class InlineQueryLimit(IntEnum):
MAX_QUERY_LENGTH = 256
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
:paramref:`~telegram.InlineQuery.query` parameter of :class:`telegram.InlineQuery`."""
MIN_SWITCH_PM_TEXT_LENGTH = 1
""":obj:`int`: Minimum number of characters in a :obj:`str` passed as the
:paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of
:meth:`telegram.Bot.answer_inline_query`.
.. deprecated:: 20.3
Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`.
"""
MAX_SWITCH_PM_TEXT_LENGTH = 64
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
:paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of
:meth:`telegram.Bot.answer_inline_query`.
.. deprecated:: 20.3
Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`.
"""
class InlineQueryResultLimit(IntEnum):
@@ -1427,12 +1434,12 @@ class InlineQueryResultsButtonLimit(IntEnum):
__slots__ = ()
MIN_START_PARAMETER_LENGTH = 1
MIN_START_PARAMETER_LENGTH = InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH
""":obj:`int`: Minimum number of characters in a :obj:`str` passed as the
:paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of
:meth:`telegram.InlineQueryResultsButton`."""
MAX_START_PARAMETER_LENGTH = 64
MAX_START_PARAMETER_LENGTH = InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
:paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of
:meth:`telegram.InlineQueryResultsButton`."""
@@ -2611,13 +2618,13 @@ class StickerSetLimit(IntEnum):
:meth:`telegram.Bot.add_sticker_to_set`.
"""
MAX_STATIC_THUMBNAIL_SIZE = 128
""":obj:`int`: Maximum size of the thumbnail if it is a ``.WEBP`` or ``.PNG`` in kilobytes,
""":obj:`int`: Maximum size of the thumbnail if it is a **.WEBP** or **.PNG** in kilobytes,
as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
MAX_ANIMATED_THUMBNAIL_SIZE = 32
""":obj:`int`: Maximum size of the thumbnail if it is a ``.TGS`` or ``.WEBM`` in kilobytes,
""":obj:`int`: Maximum size of the thumbnail if it is a **.TGS** or **.WEBM** in kilobytes,
as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
STATIC_THUMB_DIMENSIONS = 100
""":obj:`int`: Exact height and width of the thumbnail if it is a ``.WEBP`` or ``.PNG`` in
""":obj:`int`: Exact height and width of the thumbnail if it is a **.WEBP** or **.PNG** in
pixels, as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
@@ -2652,11 +2659,6 @@ class TransactionPartnerType(StringEnum):
.. versionadded:: 21.9
"""
CHAT = "chat"
""":obj:`str`: Transaction with a chat.
.. versionadded:: 21.11
"""
FRAGMENT = "fragment"
""":obj:`str`: Withdrawal transaction with Fragment."""
OTHER = "other"
+5
View File
@@ -42,6 +42,7 @@ __all__ = (
"Defaults",
"DictPersistence",
"ExtBot",
"FiniteStateMachine",
"InlineQueryHandler",
"InvalidCallbackData",
"Job",
@@ -57,6 +58,9 @@ __all__ = (
"PrefixHandler",
"ShippingQueryHandler",
"SimpleUpdateProcessor",
"SingleStateMachine",
"State",
"StateInfo",
"StringCommandHandler",
"StringRegexHandler",
"TypeHandler",
@@ -77,6 +81,7 @@ from ._contexttypes import ContextTypes
from ._defaults import Defaults
from ._dictpersistence import DictPersistence
from ._extbot import ExtBot
from ._fsm import FiniteStateMachine, SingleStateMachine, State, StateInfo
from ._handlers.basehandler import BaseHandler
from ._handlers.businessconnectionhandler import BusinessConnectionHandler
from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler
+1 -1
View File
@@ -98,7 +98,7 @@ class AIORateLimiter(BaseRateLimiter[int]):
:tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second by
paying a fee in Telegram Stars.
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
This class automatically takes the
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` parameter into account and
throttles the requests accordingly.
+111 -48
View File
@@ -48,9 +48,9 @@ from telegram.error import TelegramError
from telegram.ext._basepersistence import BasePersistence
from telegram.ext._contexttypes import ContextTypes
from telegram.ext._extbot import ExtBot
from telegram.ext._fsm import SingleStateMachine, State, StateInfo
from telegram.ext._handlers.basehandler import BaseHandler
from telegram.ext._updater import Updater
from telegram.ext._utils.networkloop import network_retry_loop
from telegram.ext._utils.stack import was_called_by
from telegram.ext._utils.trackingdict import TrackingDict
from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, RT, UD, ConversationKey, HandlerCallback
@@ -60,7 +60,7 @@ if TYPE_CHECKING:
from socket import socket
from telegram import Message
from telegram.ext import ConversationHandler, JobQueue
from telegram.ext import ConversationHandler, FiniteStateMachine, JobQueue
from telegram.ext._applicationbuilder import InitApplicationBuilder
from telegram.ext._baseupdateprocessor import BaseUpdateProcessor
from telegram.ext._jobqueue import Job
@@ -267,6 +267,7 @@ class Application(
"update_queue",
"updater",
"user_data",
"fsm",
)
# Allowing '__weakref__' creation here since we need it for the JobQueue
# Currently the __weakref__ slot is already created
@@ -302,11 +303,12 @@ class Application(
stacklevel=2,
)
self.fsm: FiniteStateMachine = SingleStateMachine()
self.bot: BT = bot
self.update_queue: asyncio.Queue[object] = update_queue
self.context_types: ContextTypes[CCT, UD, CD, BD] = context_types
self.updater: Optional[Updater] = updater
self.handlers: dict[int, list[BaseHandler[Any, CCT, Any]]] = {}
self.handlers: dict[State, dict[int, list[BaseHandler[Any, CCT, Any]]]] = {}
self.error_handlers: dict[
HandlerCallback[object, CCT, None], Union[bool, DefaultValue[bool]]
] = {}
@@ -740,7 +742,11 @@ class Application(
self,
poll_interval: float = 0.0,
timeout: int = 10,
bootstrap_retries: int = 0,
bootstrap_retries: int = -1,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
allowed_updates: Optional[Sequence[str]] = None,
drop_pending_updates: Optional[bool] = None,
close_loop: bool = True,
@@ -772,29 +778,50 @@ class Application(
.. include:: inclusions/application_run_tip.rst
.. versionchanged::
Removed the deprecated parameters ``read_timeout``, ``write_timeout``,
``connect_timeout``, and ``pool_timeout``. Use the corresponding methods in
:class:`telegram.ext.ApplicationBuilder` instead.
Args:
poll_interval (:obj:`float`, optional): Time to wait between polling updates from
Telegram in seconds. Default is ``0.0``.
timeout (:obj:`int`, optional): Passed to
:paramref:`telegram.Bot.get_updates.timeout`. Default is ``10`` seconds.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase
(calling :meth:`initialize` and the boostrapping of
:meth:`telegram.ext.Updater.start_polling`)
will retry on failures on the Telegram server.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
* < 0 - retry indefinitely
* 0 - no retries (default)
* < 0 - retry indefinitely (default)
* 0 - no retries
* > 0 - retry up to X times
.. versionchanged:: 21.11
The default value will be changed to from ``-1`` to ``0``. Indefinite retries
during bootstrapping are not recommended.
read_timeout (:obj:`float`, optional): Value to pass to
:paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
.. versionchanged:: 20.7
Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of
``2``.
.. deprecated:: 20.7
Deprecated in favor of setting the timeout via
:meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout`.
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
.. deprecated:: 20.7
Deprecated in favor of setting the timeout via
:meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout`.
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
.. deprecated:: 20.7
Deprecated in favor of setting the timeout via
:meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout`.
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
.. deprecated:: 20.7
Deprecated in favor of setting the timeout via
:meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout`.
drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
Telegram servers before actually starting to poll. Default is :obj:`False`.
allowed_updates (Sequence[:obj:`str`], optional): Passed to
@@ -826,6 +853,16 @@ class Application(
"Application.run_polling is only available if the application has an Updater."
)
if (read_timeout, write_timeout, connect_timeout, pool_timeout) != ((DEFAULT_NONE,) * 4):
warn(
PTBDeprecationWarning(
"20.6",
"Setting timeouts via `Application.run_polling` is deprecated. "
"Please use `ApplicationBuilder.get_updates_*_timeout` instead.",
),
stacklevel=2,
)
def error_callback(exc: TelegramError) -> None:
self.create_task(self.process_error(error=exc, update=None))
@@ -834,13 +871,16 @@ class Application(
poll_interval=poll_interval,
timeout=timeout,
bootstrap_retries=bootstrap_retries,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
allowed_updates=allowed_updates,
drop_pending_updates=drop_pending_updates,
error_callback=error_callback, # if there is an error in fetching updates
),
stop_signals=stop_signals,
bootstrap_retries=bootstrap_retries,
close_loop=close_loop,
stop_signals=stop_signals,
)
def run_webhook(
@@ -909,10 +949,8 @@ class Application(
url_path (:obj:`str`, optional): Path inside url. Defaults to `` '' ``
cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file.
key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase
(calling :meth:`initialize` and the boostrapping of
:meth:`telegram.ext.Updater.start_polling`)
will retry on failures on the Telegram server.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
* < 0 - retry indefinitely
* 0 - no retries (default)
@@ -998,28 +1036,18 @@ class Application(
secret_token=secret_token,
unix=unix,
),
stop_signals=stop_signals,
bootstrap_retries=bootstrap_retries,
close_loop=close_loop,
)
async def _bootstrap_initialize(self, max_retries: int) -> None:
await network_retry_loop(
action_cb=self.initialize,
description="Bootstrap Initialize Application",
max_retries=max_retries,
interval=1,
stop_signals=stop_signals,
)
def __run(
self,
updater_coroutine: Coroutine,
stop_signals: ODVInput[Sequence[int]],
bootstrap_retries: int,
close_loop: bool = True,
) -> None:
# Calling get_event_loop() should still be okay even in py3.10+ as long as there is a
# running event loop, or we are in the main thread, which are the intended use cases.
# running event loop or we are in the main thread, which are the intended use cases.
# See the docs of get_event_loop() and get_running_loop() for more info
loop = asyncio.get_event_loop()
@@ -1039,7 +1067,7 @@ class Application(
)
try:
loop.run_until_complete(self._bootstrap_initialize(max_retries=bootstrap_retries))
loop.run_until_complete(self.initialize())
if self.post_init:
loop.run_until_complete(self.post_init(self))
if self.__stop_running_marker.is_set():
@@ -1253,19 +1281,46 @@ class Application(
# Processing updates before initialize() is a problem e.g. if persistence is used
self._check_initialized()
fsm_state_info = self.fsm.get_state_info(update)
for state, state_handlers in self.handlers.items():
if state.matches(fsm_state_info.state):
_LOGGER.debug("Processing in state %s", state)
was_handled = await self.__process_update_groups(
update, state_handlers, fsm_state_info
)
if was_handled:
_LOGGER.debug(
"Update was handled in state %s. Stopping further processing", state
)
return
_LOGGER.debug(
"No handlers found for key %s in state %s", fsm_state_info.key, fsm_state_info.state
)
return
async def __process_update_groups(
self,
update: object,
state_handlers: dict[int, list[BaseHandler]],
fsm_state_info: StateInfo,
) -> bool:
context = None
was_handled = False
any_blocking = False # Flag which is set to True if any handler specifies block=True
for handlers in self.handlers.values():
for handlers in state_handlers.values():
try:
for handler in handlers:
check = handler.check_update(update) # Should the handler handle this update?
if check is None or check is False:
continue
was_handled = True
if not context: # build a context if not already built
try:
context = self.context_types.context.from_update(update, self)
context.fsm_state_info = fsm_state_info
except Exception as exc:
_LOGGER.critical(
(
@@ -1275,7 +1330,7 @@ class Application(
update,
exc_info=exc,
)
return
return True
await context.refresh_data()
coroutine: Coroutine = handler.handle_update(update, self, check, context)
@@ -1315,7 +1370,14 @@ class Application(
# (in __create_task_callback)
self._mark_for_persistence_update(update=update)
def add_handler(self, handler: BaseHandler[Any, CCT, Any], group: int = DEFAULT_GROUP) -> None:
return was_handled
def add_handler(
self,
handler: BaseHandler[Any, CCT, Any],
group: int = DEFAULT_GROUP,
state: State = State.IDLE,
) -> None:
"""Register a handler.
TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of
@@ -1374,11 +1436,11 @@ class Application(
stacklevel=2,
)
if group not in self.handlers:
self.handlers[group] = []
self.handlers = dict(sorted(self.handlers.items())) # lower -> higher groups
state_handlers = self.handlers.setdefault(state, {})
if group not in state_handlers:
state_handlers[group] = []
self.handlers[group].append(handler)
state_handlers[group].append(handler)
def add_handlers(
self,
@@ -1450,10 +1512,11 @@ class Application(
group (:obj:`object`, optional): The group identifier. Default is ``0``.
"""
if handler in self.handlers[group]:
self.handlers[group].remove(handler)
if not self.handlers[group]:
del self.handlers[group]
for state_handlers in self.handlers.values():
if handler in state_handlers[group]:
state_handlers[group].remove(handler)
if not state_handlers[group]:
del state_handlers[group]
def drop_chat_data(self, chat_id: int) -> None:
"""Drops the corresponding entry from the :attr:`chat_data`. Will also be deleted from
+52 -5
View File
@@ -35,6 +35,7 @@ from telegram._utils.types import (
ODVInput,
SocketOpt,
)
from telegram._utils.warnings import warn
from telegram.ext._application import Application
from telegram.ext._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor
from telegram.ext._contexttypes import ContextTypes
@@ -44,6 +45,7 @@ from telegram.ext._updater import Updater
from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, UD
from telegram.request import BaseRequest
from telegram.request._httpxrequest import HTTPXRequest
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import Update
@@ -122,9 +124,6 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
.. seealso:: :wiki:`Your First Bot <Extensions---Your-first-Bot>`,
:wiki:`Builder Pattern <Builder-Pattern>`
.. versionchanged:: 22.0
Removed deprecated methods ``proxy_url`` and ``get_updates_proxy_url``.
.. _`builder pattern`: https://en.wikipedia.org/wiki/Builder_pattern
"""
@@ -394,7 +393,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
.. seealso:: :paramref:`telegram.Bot.base_url`,
:wiki:`Local Bot API Server <Local-Bot-API-Server>`, :meth:`base_file_url`
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
Supports callable input and string formatting.
Args:
@@ -416,7 +415,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
.. seealso:: :paramref:`telegram.Bot.base_file_url`,
:wiki:`Local Bot API Server <Local-Bot-API-Server>`, :meth:`base_url`
.. versionchanged:: 21.11
.. versionchanged:: NEXT.VERSION
Supports callable input and string formatting.
Args:
@@ -517,6 +516,30 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._connection_pool_size = connection_pool_size
return self
def proxy_url(self: BuilderType, proxy_url: str) -> BuilderType:
"""Legacy name for :meth:`proxy`, kept for backward compatibility.
.. seealso:: :meth:`get_updates_proxy`
.. deprecated:: 20.7
Args:
proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See
:paramref:`telegram.ext.ApplicationBuilder.proxy.proxy`.
Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
warn(
PTBDeprecationWarning(
"20.7",
"`ApplicationBuilder.proxy_url` is deprecated. Use `ApplicationBuilder.proxy` "
"instead.",
),
stacklevel=2,
)
return self.proxy(proxy_url)
def proxy(self: BuilderType, proxy: Union[str, httpx.Proxy, httpx.URL]) -> BuilderType:
"""Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy`
parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`.
@@ -727,6 +750,30 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._get_updates_connection_pool_size = get_updates_connection_pool_size
return self
def get_updates_proxy_url(self: BuilderType, get_updates_proxy_url: str) -> BuilderType:
"""Legacy name for :meth:`get_updates_proxy`, kept for backward compatibility.
.. seealso:: :meth:`proxy`
.. deprecated:: 20.7
Args:
get_updates_proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See
:paramref:`telegram.ext.ApplicationBuilder.get_updates_proxy.get_updates_proxy`.
Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
warn(
PTBDeprecationWarning(
"20.7",
"`ApplicationBuilder.get_updates_proxy_url` is deprecated. Use "
"`ApplicationBuilder.get_updates_proxy` instead.",
),
stacklevel=2,
)
return self.get_updates_proxy(get_updates_proxy_url)
def get_updates_proxy(
self: BuilderType, get_updates_proxy: Union[str, httpx.Proxy, httpx.URL]
) -> BuilderType:
+1 -1
View File
@@ -113,7 +113,7 @@ class BaseUpdateProcessor(AbstractAsyncContextManager["BaseUpdateProcessor"], AB
This value is a snapshot of the current number of updates being processed. It may
change immediately after being read.
.. versionadded:: 21.11
.. versionadded:: NEXT.VERSION
"""
return self.max_concurrent_updates - self._semaphore.current_value
+22 -2
View File
@@ -17,8 +17,9 @@
# 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 the CallbackContext class."""
import asyncio
from collections.abc import Awaitable, Generator
from contextlib import AbstractAsyncContextManager
from re import Match
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, TypeVar, Union
@@ -26,12 +27,13 @@ from telegram._callbackquery import CallbackQuery
from telegram._update import Update
from telegram._utils.warnings import warn
from telegram.ext._extbot import ExtBot
from telegram.ext._fsm import FiniteStateMachine, State
from telegram.ext._utils.types import BD, BT, CD, UD
if TYPE_CHECKING:
from asyncio import Future, Queue
from telegram.ext import Application, Job, JobQueue
from telegram.ext import Application, Job, JobQueue, StateInfo
from telegram.ext._utils.types import CCT
_STORING_DATA_WIKI = (
@@ -121,6 +123,7 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
"args",
"coroutine",
"error",
"fsm_state_info",
"job",
"matches",
)
@@ -141,6 +144,7 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
self.coroutine: Optional[
Union[Generator[Optional[Future[object]], None, Any], Awaitable[Any]]
] = None
self.fsm_state_info: StateInfo = None # type: ignore[assignment]
@property
def application(self) -> "Application[BT, ST, UD, CD, BD, Any]":
@@ -269,6 +273,22 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
"telegram.Bot does not allow for arbitrary callback data."
)
@property
def fsm(self) -> FiniteStateMachine:
return self.application.fsm
def fsm_semaphore(self) -> asyncio.Lock:
return self.fsm.get_lock(self.fsm_state_info.key)
async def set_state(self, state: State) -> None:
await self.fsm.set_state(self.fsm_state_info.key, state, self.fsm_state_info.version)
def set_state_nowait(self, state: State) -> None:
self.fsm.set_state_nowait(self.fsm_state_info.key, state, self.fsm_state_info.version)
def as_fsm_state(self, state: State) -> AbstractAsyncContextManager[None]:
return self.fsm.as_state(self.fsm_state_info.key, state)
@classmethod
def from_error(
cls: type["CCT"],
+80 -12
View File
@@ -18,15 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the class Defaults, which allows passing default values to Application."""
import datetime as dtm
from typing import TYPE_CHECKING, Any, NoReturn, Optional, final
from typing import Any, NoReturn, Optional, final
from telegram import LinkPreviewOptions
from telegram._utils.datetime import UTC
from telegram._utils.types import ODVInput
from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import LinkPreviewOptions
@final
class Defaults:
@@ -39,15 +38,23 @@ class Defaults:
Removed the argument and attribute ``timeout``. Specify default timeout behavior for the
networking backend directly via :class:`telegram.ext.ApplicationBuilder` instead.
.. versionchanged:: 22.0
Removed deprecated arguments and properties ``disable_web_page_preview`` and ``quote``.
Use :paramref:`link_preview_options` and :paramref:`do_quote` instead.
Parameters:
parse_mode (:obj:`str`, optional): |parse_mode|
disable_notification (:obj:`bool`, optional): |disable_notification|
disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this
message. Mutually exclusive with :paramref:`link_preview_options`.
.. deprecated:: 20.8
Use :paramref:`link_preview_options` instead. This parameter will be removed in
future versions.
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|.
Will be used for :attr:`telegram.ReplyParameters.allow_sending_without_reply`.
quote (:obj:`bool`, optional): |reply_quote|
.. deprecated:: 20.8
Use :paramref:`do_quote` instead. This parameter will be removed in future
versions.
tzinfo (:class:`datetime.tzinfo`, optional): A timezone to be used for all date(time)
inputs appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
somewhere, it will be assumed to be in :paramref:`tzinfo`. Defaults to
@@ -128,6 +135,8 @@ class Defaults:
self,
parse_mode: Optional[str] = None,
disable_notification: Optional[bool] = None,
disable_web_page_preview: Optional[bool] = None,
quote: Optional[bool] = None,
tzinfo: dtm.tzinfo = UTC,
block: bool = True,
allow_sending_without_reply: Optional[bool] = None,
@@ -155,9 +164,37 @@ class Defaults:
stacklevel=2,
)
self._link_preview_options = link_preview_options
self._do_quote = do_quote
if disable_web_page_preview is not None and link_preview_options is not None:
raise ValueError(
"`disable_web_page_preview` and `link_preview_options` are mutually exclusive."
)
if quote is not None and do_quote is not None:
raise ValueError("`quote` and `do_quote` are mutually exclusive")
if disable_web_page_preview is not None:
warn(
PTBDeprecationWarning(
"20.8",
"`Defaults.disable_web_page_preview` is deprecated. Use "
"`Defaults.link_preview_options` instead.",
),
stacklevel=2,
)
self._link_preview_options: Optional[LinkPreviewOptions] = LinkPreviewOptions(
is_disabled=disable_web_page_preview
)
else:
self._link_preview_options = link_preview_options
if quote is not None:
warn(
PTBDeprecationWarning(
"20.8", "`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead."
),
stacklevel=2,
)
self._do_quote: Optional[bool] = quote
else:
self._do_quote = do_quote
# Gather all defaults that actually have a default value
self._api_defaults = {}
for kwarg in (
@@ -186,9 +223,9 @@ class Defaults:
(
self._parse_mode,
self._disable_notification,
self._link_preview_options,
self.disable_web_page_preview,
self._allow_sending_without_reply,
self._do_quote,
self.quote,
self._tzinfo,
self._block,
self._protect_content,
@@ -292,6 +329,23 @@ class Defaults:
"You can not assign a new value to disable_notification after initialization."
)
@property
def disable_web_page_preview(self) -> ODVInput[bool]:
""":obj:`bool`: Optional. Disables link previews for links in all outgoing
messages.
.. deprecated:: 20.8
Use :attr:`link_preview_options` instead. This attribute will be removed in future
versions.
"""
return self._link_preview_options.is_disabled if self._link_preview_options else None
@disable_web_page_preview.setter
def disable_web_page_preview(self, _: object) -> NoReturn:
raise AttributeError(
"You can not assign a new value to disable_web_page_preview after initialization."
)
@property
def allow_sending_without_reply(self) -> Optional[bool]:
""":obj:`bool`: Optional. Pass :obj:`True`, if the message
@@ -305,6 +359,20 @@ class Defaults:
"You can not assign a new value to allow_sending_without_reply after initialization."
)
@property
def quote(self) -> Optional[bool]:
""":obj:`bool`: Optional. |reply_quote|
.. deprecated:: 20.8
Use :attr:`do_quote` instead. This attribute will be removed in future
versions.
"""
return self._do_quote if self._do_quote is not None else None
@quote.setter
def quote(self, _: object) -> NoReturn:
raise AttributeError("You can not assign a new value to quote after initialization.")
@property
def tzinfo(self) -> dtm.tzinfo:
""":obj:`tzinfo`: A timezone to be used for all date(time) objects appearing
+4 -14
View File
@@ -815,7 +815,6 @@ class ExtBot(Bot, Generic[RLARGS]):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -832,7 +831,6 @@ class ExtBot(Bot, Generic[RLARGS]):
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -1197,9 +1195,9 @@ class ExtBot(Bot, Generic[RLARGS]):
title: str,
description: str,
payload: str,
provider_token: Optional[str],
currency: str,
prices: Sequence["LabeledPrice"],
provider_token: Optional[str] = None,
max_tip_amount: Optional[int] = None,
suggested_tip_amounts: Optional[Sequence[int]] = None,
provider_data: Optional[Union[str, object]] = None,
@@ -1754,7 +1752,6 @@ class ExtBot(Bot, Generic[RLARGS]):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1767,7 +1764,6 @@ class ExtBot(Bot, Generic[RLARGS]):
chat_id=chat_id,
from_chat_id=from_chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
protect_content=protect_content,
message_thread_id=message_thread_id,
@@ -2772,9 +2768,9 @@ class ExtBot(Bot, Generic[RLARGS]):
title: str,
description: str,
payload: str,
provider_token: Optional[str],
currency: str,
prices: Sequence["LabeledPrice"],
provider_token: Optional[str] = None,
start_parameter: Optional[str] = None,
photo_url: Optional[str] = None,
photo_size: Optional[int] = None,
@@ -3244,8 +3240,6 @@ class ExtBot(Bot, Generic[RLARGS]):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3276,8 +3270,6 @@ class ExtBot(Bot, Generic[RLARGS]):
business_connection_id=business_connection_id,
has_spoiler=has_spoiler,
thumbnail=thumbnail,
cover=cover,
start_timestamp=start_timestamp,
filename=filename,
reply_parameters=reply_parameters,
read_timeout=read_timeout,
@@ -4476,13 +4468,12 @@ class ExtBot(Bot, Generic[RLARGS]):
async def send_gift(
self,
user_id: Optional[int] = None,
gift_id: Union[str, Gift] = None, # type: ignore
user_id: int,
gift_id: Union[str, Gift],
text: Optional[str] = None,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence["MessageEntity"]] = None,
pay_for_upgrade: Optional[bool] = None,
chat_id: Optional[Union[str, int]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -4493,7 +4484,6 @@ class ExtBot(Bot, Generic[RLARGS]):
) -> bool:
return await super().send_gift(
user_id=user_id,
chat_id=chat_id,
gift_id=gift_id,
text=text,
text_parse_mode=text_parse_mode,
+6
View File
@@ -0,0 +1,6 @@
"""Private Submbodule for finite state machine implementation."""
__all__ = ["FiniteStateMachine", "SingleStateMachine", "State", "StateInfo"]
from .machine import FiniteStateMachine, SingleStateMachine, StateInfo
from .states import State
+200
View File
@@ -0,0 +1,200 @@
"""This Module contains the FiniteStateMachine class and the built-in subclass SingleStateMachine.
"""
import abc
import asyncio
import contextlib
import datetime as dtm
import logging
import time
import weakref
from collections import defaultdict, deque
from collections.abc import AsyncIterator, Hashable, Mapping, MutableSequence, Sequence
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Generic, Literal, Optional, TypeVar, Union, overload
from telegram.ext._fsm.states import State
from telegram.ext._utils.types import JobCallback
if TYPE_CHECKING:
from collections.abc import MutableMapping
from telegram.ext import JobQueue
_KT = TypeVar("_KT", bound=Hashable)
_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.DEBUG)
class StateInfo(Generic[_KT]):
def __init__(self: "StateInfo[_KT]", key: _KT, state: State, version: int) -> None:
self.key: _KT = key
self.state: State = state
self.version: int = version
class FiniteStateMachine(abc.ABC, Generic[_KT]):
def __init__(self) -> None:
self._locks: MutableMapping[_KT, asyncio.Lock] = weakref.WeakValueDictionary()
# There is likely litte benefit for a user to customize how exactly the states are stored
# and accessed. So we make this private and only provide a read-only view.
self.__states: dict[_KT, tuple[State, int]] = defaultdict(
lambda: (State.IDLE, time.perf_counter_ns())
)
self._states = MappingProxyType(self.__states)
self.__job_queue: Optional[weakref.ReferenceType[JobQueue]] = None
self.__history: Mapping[_KT, MutableSequence[State]] = defaultdict(
lambda: deque(maxlen=10)
)
@property
def states(self) -> Mapping[_KT, tuple[State, int]]:
return self._states
def store_state_history(self, key: _KT, state: State) -> None:
# Making this public so that users can override if they want to customize the history
# E.g., they could want to store more/fewer states, also depending on the key
self.__history[key].append(state)
def get_state_history(self, key: _KT) -> Sequence[State]:
return list(self.__history[key])
def get_lock(self, key: _KT) -> asyncio.Lock:
"""Returns a lock that is unique for this key at runtime.
It can be used to prevent concurrent access to resources associated to this key.
"""
return self._locks.setdefault(key, asyncio.Lock())
@abc.abstractmethod
def get_state_info(self, update: object) -> StateInfo[_KT]:
"""Returns exactly one active state for the update.
If more than one stored key applies to the update, one must be chosen.
It's recommended to select the most specific one.
Example:
The state of a chat, a user or a user in a specific chat could be tracked.
For a message in that chat, the state of the user in that chat should be returned if
available. Otherwise, the state of the chat should be returned.
Important:
This must be an atomic operation and not e.g. wait for a lock.
Instead, if necessary, return a special state indicating that the key is currently
busy.
"""
def _do_set_state(
self, key: _KT, state: State, version: Optional[int] = None
) -> StateInfo[_KT]:
"""Protected method to set the state for the specified key.
The version can be optionally used for optimistic locking. If the version does not match
the current version, the state should not be updated.
Important:
This should be used exclusively by methods of this class and subclasses.
It should *not* be called directly by users of this class!
"""
_LOGGER.debug("Setting %s state to %s", key, state)
if state is State.ANY:
raise ValueError("State.ANY is not supported in set_state")
if version and version != self._states.get(key, (None, None))[1]:
raise ValueError("Optimistic locking failed. Not updating state.")
if jq := self._get_job_queue(raise_exception=False):
# This is a rather tight coupling between FSM and JobQueue
# Not sure if we like that. Makes it even harder to replace JobQueue
# (or the JQ implementation) with something else.
# The upside is that we don't need to maintain any additional internal state
# for the jobs and persistence is handled by the JobQueue.
cancel_jobs = jq.jobs(pattern=str(hash(key)))
for job in cancel_jobs:
_LOGGER.debug("Cancelling timeout job %s", job)
job.schedule_removal()
# important to use time.perf_counter_ns() here, as time_ns() is not monotonic
self.__states[key] = (state, time.perf_counter_ns())
# Doing this *after* do_set_state so that any exceptions are raised before the history
# is updated
self.store_state_history(key, state)
return StateInfo(key, state, self._states[key][1])
async def set_state(self, key: _KT, state: State, version: Optional[int] = None) -> None:
"""Store the state for the specified key."""
async with self.get_lock(key):
self._do_set_state(key, state, version)
def set_state_nowait(self, key: _KT, state: State, version: Optional[int] = None) -> None:
"""Store the state for the specified key without waiting for a lock."""
if self.get_lock(key).locked():
raise asyncio.InvalidStateError("Lock is locked")
self._do_set_state(key, state, version)
@contextlib.asynccontextmanager
async def as_state(self, key: _KT, state: State) -> AsyncIterator[None]:
"""Context manager to set the state for the specified key and reset it afterwards."""
async with self.get_lock(key):
current_state, current_version = self.states[key]
new_version = self._do_set_state(key, state, current_version).version
try:
yield
finally:
self._do_set_state(key, current_state, new_version)
@staticmethod
def _build_job_name(keys: Sequence[_KT]) -> str:
return f"FSM_Job_{'_'.join(str(hash(k)) for k in keys)}"
def set_job_queue(self, job_queue: "JobQueue") -> None:
self.__job_queue = weakref.ref(job_queue)
@overload
def _get_job_queue(self, raise_exception: Literal[False]) -> Optional["JobQueue"]: ...
@overload
def _get_job_queue(self) -> "JobQueue": ...
def _get_job_queue(self, raise_exception: bool = True) -> Optional["JobQueue"]:
if self.__job_queue is None:
if raise_exception:
raise RuntimeError("JobQueue not set")
return None
job_queue = self.__job_queue()
if job_queue is None:
if raise_exception:
raise RuntimeError("JobQueue was garbage collected")
return None
return job_queue
def schedule_timeout(
self,
callback: JobCallback,
when: Union[float, dtm.timedelta, dtm.datetime, dtm.time],
cancel_keys: Optional[Sequence[_KT]] = None,
job_kwargs: Optional[dict[str, Any]] = None,
) -> None:
"""Schedule a timeout job. This is a thin wrapper around JobQueue.run_once.
The callback will have to take care of resetting any state if necessary.
Pass cancel_keys to automatically cancel the job when a new state is set for any of the
keys.
"""
job_kwargs = job_kwargs or {}
if cancel_keys:
if "name" in job_kwargs:
raise ValueError("job_kwargs must not contain a 'name' key")
job_kwargs["name"] = self._build_job_name(cancel_keys)
self._get_job_queue().run_once(callback, when, **job_kwargs)
_LOGGER.debug(
"Scheduled timeout. Will be cancelled when a new set state is for either of: %s",
cancel_keys or [],
)
class SingleStateMachine(FiniteStateMachine[None]):
def get_state_info(self, update: object) -> StateInfo[None]: # noqa: ARG002
return StateInfo(None, State.IDLE, 0)
def do_set_state(self, key: None, state: State) -> None:
pass
+114
View File
@@ -0,0 +1,114 @@
"""This Module contains implementations of State classes for Finite State Machines"""
import abc
import contextlib
from typing import ClassVar, Optional
from uuid import uuid4
class State(abc.ABC):
__knows_uids: ClassVar[set[str]] = set()
__not_cache: ClassVar[dict[str, "_NOTState"]] = {}
__or_cache: ClassVar[dict[tuple[str, str], "_ORState"]] = {}
__and_cache: ClassVar[dict[tuple[str, str], "_ANDState"]] = {}
__xor_cache: ClassVar[dict[tuple[str, str], "_XORState"]] = {}
IDLE: "State"
"""Default State for all Finite State Machines"""
ANY: "State"
"""Special State that matches any other State. Useful to define fallback behavior.
*Not* supported in ``set_state`` method of FSMs.
"""
def __init__(self, uid: Optional[str] = None):
effective_uid = uid or uuid4().hex
if effective_uid in self.__knows_uids:
raise ValueError(f"Duplicate UID: {effective_uid} already registered")
self._uid = effective_uid
self.__knows_uids.add(effective_uid)
def __invert__(self) -> "_NOTState":
with contextlib.suppress(KeyError):
return self.__not_cache[self.uid]
return self.__not_cache.setdefault(self.uid, _NOTState(self))
def __or__(self, other: "State") -> "_ORState":
key = (self.uid, other.uid)
with contextlib.suppress(KeyError):
return self.__or_cache[key]
return self.__or_cache.setdefault(key, _ORState(self, other))
def __and__(self, other: "State") -> "_ANDState":
key = (self.uid, other.uid)
with contextlib.suppress(KeyError):
return self.__and_cache[key]
return self.__and_cache.setdefault(key, _ANDState(self, other))
def __xor__(self, other: "State") -> "_XORState":
key = (self.uid, other.uid)
with contextlib.suppress(KeyError):
return self.__xor_cache[key]
return self.__xor_cache.setdefault(key, _XORState(self, other))
def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {self.uid}>"
def __str__(self) -> str:
return self.uid
@property
def uid(self) -> str:
return self._uid
def matches(self, state: "State") -> bool:
if isinstance(state, (_NOTState, _ANDState, _ORState, _XORState)):
return state.matches(self)
return self.uid == state.uid
class _AnyState(State):
def matches(self, state: "State") -> bool: # noqa: ARG002
return True
State.IDLE = State("IDLE")
State.ANY = _AnyState("ANY")
class _XORState(State):
def __init__(self, state_one: State, state_two: State):
super().__init__(uid=f"({state_one.uid})^({state_two.uid})")
self._state_one = state_one
self._state_two = state_two
def matches(self, state: "State") -> bool:
return self._state_one.matches(state) ^ self._state_two.matches(state)
class _ORState(State):
def __init__(self, state_one: State, state_two: State):
super().__init__(uid=f"({state_one.uid})|({state_two.uid})")
self._state_one = state_one
self._state_two = state_two
def matches(self, state: "State") -> bool:
return self._state_one.matches(state) or self._state_two.matches(state)
class _ANDState(State):
def __init__(self, state_one: State, state_two: State):
super().__init__(uid=f"({state_one.uid})&({state_two.uid})")
self._state_one = state_one
self._state_two = state_two
def matches(self, state: "State") -> bool:
return self._state_one.matches(state) and self._state_two.matches(state)
class _NOTState(State):
def __init__(self, state: State):
super().__init__(uid=f"!({state.uid})")
self._state = state
def matches(self, state: "State") -> bool:
return not self._state.matches(state)
+1 -1
View File
@@ -97,7 +97,7 @@ class JobQueue(Generic[CCT]):
"""
__slots__ = ("_application", "_executor", "scheduler")
__slots__ = ("__weakref__", "_application", "_executor", "scheduler")
_CRON_MAPPING = ("sun", "mon", "tue", "wed", "thu", "fri", "sat")
def __init__(self) -> None:
+166 -36
View File
@@ -26,12 +26,11 @@ from pathlib import Path
from types import TracebackType
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DefaultValue
from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DEFAULT_NONE, DefaultValue
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import DVType
from telegram.error import TelegramError
from telegram.ext._utils.networkloop import network_retry_loop
from telegram._utils.types import DVType, ODVInput
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
try:
from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer
@@ -207,7 +206,11 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
self,
poll_interval: float = 0.0,
timeout: int = 10,
bootstrap_retries: int = 0,
bootstrap_retries: int = -1,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
allowed_updates: Optional[Sequence[str]] = None,
drop_pending_updates: Optional[bool] = None,
error_callback: Optional[Callable[[TelegramError], None]] = None,
@@ -217,27 +220,52 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
.. versionchanged:: 20.0
Removed the ``clean`` argument in favor of :paramref:`drop_pending_updates`.
.. versionchanged:: 22.0
Removed the deprecated arguments ``read_timeout``, ``write_timeout``,
``connect_timeout``, and ``pool_timeout`` in favor of setting the timeouts via
the corresponding methods of :class:`telegram.ext.ApplicationBuilder`. or
by specifying the timeout via :paramref:`telegram.Bot.get_updates_request`.
Args:
poll_interval (:obj:`float`, optional): Time to wait between polling updates from
Telegram in seconds. Default is ``0.0``.
timeout (:obj:`int`, optional): Passed to
:paramref:`telegram.Bot.get_updates.timeout`. Defaults to ``10`` seconds.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
will retry on failures on the Telegram server.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
* < 0 - retry indefinitely
* 0 - no retries (default)
* < 0 - retry indefinitely (default)
* 0 - no retries
* > 0 - retry up to X times
read_timeout (:obj:`float`, optional): Value to pass to
:paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
.. versionchanged:: 21.11
The default value will be changed to from ``-1`` to ``0``. Indefinite retries
during bootstrapping are not recommended.
.. versionchanged:: 20.7
Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of
``2``.
.. deprecated:: 20.7
Deprecated in favor of setting the timeout via
:meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout` or
:paramref:`telegram.Bot.get_updates_request`.
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
.. deprecated:: 20.7
Deprecated in favor of setting the timeout via
:meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout` or
:paramref:`telegram.Bot.get_updates_request`.
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
.. deprecated:: 20.7
Deprecated in favor of setting the timeout via
:meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout` or
:paramref:`telegram.Bot.get_updates_request`.
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
.. deprecated:: 20.7
Deprecated in favor of setting the timeout via
:meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout` or
:paramref:`telegram.Bot.get_updates_request`.
allowed_updates (Sequence[:obj:`str`], optional): Passed to
:meth:`telegram.Bot.get_updates`.
@@ -291,6 +319,10 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
await self._start_polling(
poll_interval=poll_interval,
timeout=timeout,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
bootstrap_retries=bootstrap_retries,
drop_pending_updates=drop_pending_updates,
allowed_updates=allowed_updates,
@@ -310,6 +342,10 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
self,
poll_interval: float,
timeout: int,
read_timeout: ODVInput[float],
write_timeout: ODVInput[float],
connect_timeout: ODVInput[float],
pool_timeout: ODVInput[float],
bootstrap_retries: int,
drop_pending_updates: Optional[bool],
allowed_updates: Optional[Sequence[str]],
@@ -335,6 +371,10 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
updates = await self.bot.get_updates(
offset=self._last_update_id,
timeout=timeout,
read_timeout=read_timeout,
connect_timeout=connect_timeout,
write_timeout=write_timeout,
pool_timeout=pool_timeout,
allowed_updates=allowed_updates,
)
except TelegramError:
@@ -369,14 +409,12 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
# updates from Telegram and inserts them in the update queue of the
# Application.
self.__polling_task = asyncio.create_task(
network_retry_loop(
is_running=lambda: self.running,
self._network_loop_retry(
action_cb=polling_action_cb,
on_err_cb=error_callback or default_error_callback,
description="Polling Updates",
description="getting Updates",
interval=poll_interval,
stop_event=self.__polling_task_stop_event,
max_retries=-1,
),
name="Updater:start_polling:polling_task",
)
@@ -395,6 +433,10 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
offset=self._last_update_id,
# We don't want to do long polling here!
timeout=0,
read_timeout=read_timeout,
connect_timeout=connect_timeout,
write_timeout=write_timeout,
pool_timeout=pool_timeout,
allowed_updates=allowed_updates,
)
except TelegramError:
@@ -465,8 +507,8 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
Telegram servers before actually starting to poll. Default is :obj:`False`.
.. versionadded :: 13.4
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
will retry on failures on the Telegram server.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
* < 0 - retry indefinitely
* 0 - no retries (default)
@@ -656,6 +698,78 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
# say differently!
return f"{protocol}://{listen}:{port}{url_path}"
async def _network_loop_retry(
self,
action_cb: Callable[..., Coroutine],
on_err_cb: Callable[[TelegramError], None],
description: str,
interval: float,
stop_event: Optional[asyncio.Event],
) -> None:
"""Perform a loop calling `action_cb`, retrying after network errors.
Stop condition for loop: `self.running` evaluates :obj:`False` or return value of
`action_cb` evaluates :obj:`False`.
Args:
action_cb (:term:`coroutine function`): Network oriented callback function to call.
on_err_cb (:obj:`callable`): Callback to call when TelegramError is caught. Receives
the exception object as a parameter.
description (:obj:`str`): Description text to use for logs and exception raised.
interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
`action_cb`.
stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the
loop. Setting the event will make the loop exit even if `action_cb` is currently
running.
"""
async def do_action() -> bool:
if not stop_event:
return await action_cb()
action_cb_task = asyncio.create_task(action_cb())
stop_task = asyncio.create_task(stop_event.wait())
done, pending = await asyncio.wait(
(action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED
)
with contextlib.suppress(asyncio.CancelledError):
for task in pending:
task.cancel()
if stop_task in done:
_LOGGER.debug("Network loop retry %s was cancelled", description)
return False
return action_cb_task.result()
_LOGGER.debug("Start network loop retry %s", description)
cur_interval = interval
while self.running:
try:
if not await do_action():
break
except RetryAfter as exc:
_LOGGER.info("%s", exc)
cur_interval = 0.5 + exc.retry_after
except TimedOut as toe:
_LOGGER.debug("Timed out %s: %s", description, toe)
# If failure is due to timeout, we should retry asap.
cur_interval = 0
except InvalidToken:
_LOGGER.exception("Invalid token; aborting")
raise
except TelegramError as telegram_exc:
on_err_cb(telegram_exc)
# increase waiting times on subsequent errors up to 30secs
cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval)
else:
cur_interval = interval
if cur_interval:
await asyncio.sleep(cur_interval)
async def _bootstrap(
self,
max_retries: int,
@@ -672,6 +786,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
updates if appropriate. If there are unsuccessful attempts, this will retry as specified by
:paramref:`max_retries`.
"""
retries = 0
async def bootstrap_del_webhook() -> bool:
_LOGGER.debug("Deleting webhook")
@@ -695,30 +810,45 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
)
return False
def bootstrap_on_err_cb(exc: Exception) -> None:
# We need this since retries is an immutable object otherwise and the changes
# wouldn't propagate outside of thi function
nonlocal retries
if not isinstance(exc, InvalidToken) and (max_retries < 0 or retries < max_retries):
retries += 1
_LOGGER.warning(
"Failed bootstrap phase; try=%s max_retries=%s", retries, max_retries
)
else:
_LOGGER.error("Failed bootstrap phase after %s retries (%s)", retries, exc)
raise exc
# Dropping pending updates from TG can be efficiently done with the drop_pending_updates
# parameter of delete/start_webhook, even in the case of polling. Also, we want to make
# sure that no webhook is configured in case of polling, so we just always call
# delete_webhook for polling
if drop_pending_updates or not webhook_url:
await network_retry_loop(
is_running=lambda: self.running,
action_cb=bootstrap_del_webhook,
description="Bootstrap delete Webhook",
interval=bootstrap_interval,
await self._network_loop_retry(
bootstrap_del_webhook,
bootstrap_on_err_cb,
"bootstrap del webhook",
bootstrap_interval,
stop_event=None,
max_retries=max_retries,
)
# Reset the retries counter for the next _network_loop_retry call
retries = 0
# Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
# so we set it anyhow.
if webhook_url:
await network_retry_loop(
is_running=lambda: self.running,
action_cb=bootstrap_set_webhook,
description="Bootstrap Set Webhook",
interval=bootstrap_interval,
await self._network_loop_retry(
bootstrap_set_webhook,
bootstrap_on_err_cb,
"bootstrap set webhook",
bootstrap_interval,
stop_event=None,
max_retries=max_retries,
)
async def stop(self) -> None:
+1 -1
View File
@@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains helper functions related to the std-lib asyncio module.
.. versionadded:: 21.11
.. versionadded:: NEXT.VERSION
Warning:
Contents of this module are intended to be used internally by the library and *not* by the
-152
View File
@@ -1,152 +0,0 @@
#!/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 a network retry loop implementation.
Its specifically tailored to handling the Telegram API and its errors.
.. versionadded:: 21.11
Hint:
It was originally part of the `Updater` class, but as part of #4657 it was extracted into its
own module to be used by other parts of the library.
Warning:
Contents of this module are intended to be used internally by the library and *not* by the
user. Changes to this module are not considered breaking changes and may not be documented in
the changelog.
"""
import asyncio
import contextlib
from collections.abc import Coroutine
from typing import Callable, Optional
from telegram._utils.logging import get_logger
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
_LOGGER = get_logger(__name__)
async def network_retry_loop(
*,
action_cb: Callable[..., Coroutine],
on_err_cb: Optional[Callable[[TelegramError], None]] = None,
description: str,
interval: float,
stop_event: Optional[asyncio.Event] = None,
is_running: Optional[Callable[[], bool]] = None,
max_retries: int,
) -> None:
"""Perform a loop calling `action_cb`, retrying after network errors.
Stop condition for loop:
* `is_running()` evaluates :obj:`False` or
* return value of `action_cb` evaluates :obj:`False`
* or `stop_event` is set.
* or `max_retries` is reached.
Args:
action_cb (:term:`coroutine function`): Network oriented callback function to call.
on_err_cb (:obj:`callable`): Optional. Callback to call when TelegramError is caught.
Receives the exception object as a parameter.
Hint:
Only required if you want to handle the error in a special way. Logging about
the error is already handled by the loop.
Important:
Must not raise exceptions! If it does, the loop will be aborted.
description (:obj:`str`): Description text to use for logs and exception raised.
interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
`action_cb`.
stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the
loop. Setting the event will make the loop exit even if `action_cb` is currently
running. Defaults to :obj:`None`.
is_running (:obj:`callable`): Function to check if the loop should continue running.
Must return a boolean value. Defaults to `lambda: True`.
max_retries (:obj:`int`): Maximum number of retries before stopping the loop.
* < 0: Retry indefinitely.
* 0: No retries.
* > 0: Number of retries.
"""
log_prefix = f"Network Retry Loop ({description}):"
effective_is_running = is_running or (lambda: True)
async def do_action() -> bool:
if not stop_event:
return await action_cb()
action_cb_task = asyncio.create_task(action_cb())
stop_task = asyncio.create_task(stop_event.wait())
done, pending = await asyncio.wait(
(action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED
)
with contextlib.suppress(asyncio.CancelledError):
for task in pending:
task.cancel()
if stop_task in done:
_LOGGER.debug("%s Cancelled", log_prefix)
return False
return action_cb_task.result()
_LOGGER.debug("%s Starting", log_prefix)
cur_interval = interval
retries = 0
while effective_is_running():
try:
if not await do_action():
break
except RetryAfter as exc:
slack_time = 0.5
_LOGGER.info(
"%s %s. Adding %s seconds to the specified time.", log_prefix, exc, slack_time
)
cur_interval = slack_time + exc.retry_after
except TimedOut as toe:
_LOGGER.debug("%s Timed out: %s. Retrying immediately.", log_prefix, toe)
# If failure is due to timeout, we should retry asap.
cur_interval = 0
except InvalidToken:
_LOGGER.exception("%s Invalid token. Aborting retry loop.", log_prefix)
raise
except TelegramError as telegram_exc:
if on_err_cb:
on_err_cb(telegram_exc)
if max_retries < 0 or retries < max_retries:
_LOGGER.debug(
"%s Failed run number %s of %s. Retrying.", log_prefix, retries, max_retries
)
else:
_LOGGER.exception(
"%s Failed run number %s of %s. Aborting.", log_prefix, retries, max_retries
)
raise
# increase waiting times on subsequent errors up to 30secs
cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval)
else:
cur_interval = interval
finally:
retries += 1
if cur_interval:
await asyncio.sleep(cur_interval)
+39 -6
View File
@@ -34,9 +34,6 @@ This module contains filters for use with :class:`telegram.ext.MessageHandler`,
* Filters which do both (like ``Filters.text``) are now split as ready-to-use version
``filters.TEXT`` and class version ``filters.Text(...)``.
.. versionchanged:: 22.0
Removed deprecated attribute `CHAT`.
"""
__all__ = (
@@ -46,6 +43,7 @@ __all__ = (
"AUDIO",
"BOOST_ADDED",
"CAPTION",
"CHAT",
"COMMAND",
"CONTACT",
"EFFECT_ID",
@@ -861,6 +859,21 @@ class Chat(_ChatUserBaseFilter):
return super()._remove_chat_ids(chat_id)
class _Chat(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.chat)
CHAT = _Chat(name="filters.CHAT")
"""This filter filters *any* message that has a :attr:`telegram.Message.chat`.
.. deprecated:: 20.8
This filter has no effect since :attr:`telegram.Message.chat` is always present.
"""
class ChatType: # A convenience namespace for Chat types.
"""Subset for filtering the type of chat.
@@ -1902,9 +1915,6 @@ class StatusUpdate:
Caution:
``filters.StatusUpdate`` itself is *not* a filter, but just a convenience namespace.
.. versionchanged:: 22.0
Removed deprecated attribute `USER_SHARED`.
"""
__slots__ = ()
@@ -1938,6 +1948,7 @@ class StatusUpdate:
or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update)
or StatusUpdate.REFUNDED_PAYMENT.check_update(update)
or StatusUpdate.USERS_SHARED.check_update(update)
or StatusUpdate.USER_SHARED.check_update(update)
or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update)
or StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update)
or StatusUpdate.VIDEO_CHAT_SCHEDULED.check_update(update)
@@ -2193,6 +2204,28 @@ class StatusUpdate:
.. versionadded:: 21.4
"""
class _UserShared(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.api_kwargs.get("user_shared"))
USER_SHARED = _UserShared(name="filters.StatusUpdate.USER_SHARED")
"""Messages that contain ``"user_shared"`` in :attr:`telegram.TelegramObject.api_kwargs`.
Warning:
This will only catch the legacy ``user_shared`` field, not the
new :attr:`telegram.Message.users_shared` attribute!
.. versionchanged:: 21.0
Now relies on :attr:`telegram.TelegramObject.api_kwargs` as the native attribute
``Message.user_shared`` was removed.
.. versionadded:: 20.1
.. deprecated:: 20.8
Use :attr:`USERS_SHARED` instead.
"""
class _UsersShared(MessageFilter):
__slots__ = ()
+33 -3
View File
@@ -29,6 +29,7 @@ from telegram._utils.defaultvalue import DefaultValue
from telegram._utils.logging import get_logger
from telegram._utils.strings import TextEncoding
from telegram._utils.types import JSONDict, ODVInput
from telegram._utils.warnings import warn
from telegram._version import __version__ as ptb_ver
from telegram.error import (
BadRequest,
@@ -41,6 +42,7 @@ from telegram.error import (
TelegramError,
)
from telegram.request._requestdata import RequestData
from telegram.warnings import PTBDeprecationWarning
RT = TypeVar("RT", bound="BaseRequest")
@@ -131,19 +133,22 @@ class BaseRequest(
await self.shutdown()
@property
@abc.abstractmethod
def read_timeout(self) -> Optional[float]:
"""This property must return the default read timeout in seconds used by this class.
More precisely, the returned value should be the one used when
:paramref:`post.read_timeout` of :meth:post` is not passed/equal to :attr:`DEFAULT_NONE`.
.. versionadded:: 20.7
.. versionchanged:: 22.0
This property is now required to be implemented by subclasses.
Warning:
For now this property does not need to be implemented by subclasses and will raise
:exc:`NotImplementedError` if accessed without being overridden. However, in future
versions, this property will be abstract and must be implemented by subclasses.
Returns:
:obj:`float` | :obj:`None`: The read timeout in seconds.
"""
raise NotImplementedError
@abc.abstractmethod
async def initialize(self) -> None:
@@ -300,6 +305,31 @@ class BaseRequest(
TelegramError
"""
# Import needs to be here since HTTPXRequest is a subclass of BaseRequest
from telegram.request import HTTPXRequest # pylint: disable=import-outside-toplevel
# 20 is the documented default value for all the media related bot methods and custom
# implementations of BaseRequest may explicitly rely on that. Hence, we follow the
# standard deprecation policy and deprecate starting with version 20.7.
# For our own implementation HTTPXRequest, we can handle that ourselves, so we skip the
# warning in that case.
has_files = request_data and request_data.multipart_data
if (
has_files
and not isinstance(self, HTTPXRequest)
and isinstance(write_timeout, DefaultValue)
):
warn(
PTBDeprecationWarning(
"20.7",
f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request"
" will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions "
"for *all* methods of the `Bot` class, including methods sending media.",
),
stacklevel=3,
)
write_timeout = 20
try:
code, payload = await self.do_request(
url=url,
+19 -3
View File
@@ -25,9 +25,11 @@ import httpx
from telegram._utils.defaultvalue import DefaultValue
from telegram._utils.logging import get_logger
from telegram._utils.types import HTTPVersion, ODVInput, SocketOpt
from telegram._utils.warnings import warn
from telegram.error import NetworkError, TimedOut
from telegram.request._baserequest import BaseRequest
from telegram.request._requestdata import RequestData
from telegram.warnings import PTBDeprecationWarning
# Note to future devs:
# Proxies are currently only tested manually. The httpx development docs have a nice guide on that:
@@ -43,9 +45,6 @@ class HTTPXRequest(BaseRequest):
.. versionadded:: 20.0
.. versionchanged:: 22.0
Removed the deprecated parameter ``proxy_url``. Use :paramref:`proxy` instead.
Args:
connection_pool_size (:obj:`int`, optional): Number of connections to keep in the
connection pool. Defaults to ``1``.
@@ -53,6 +52,10 @@ class HTTPXRequest(BaseRequest):
Note:
Independent of the value, one additional connection will be reserved for
:meth:`telegram.Bot.get_updates`.
proxy_url (:obj:`str`, optional): Legacy name for :paramref:`proxy`, kept for backward
compatibility. Defaults to :obj:`None`.
.. deprecated:: 20.7
read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
amount of time (in seconds) to wait for a response from Telegram's server.
This value is used unless a different value is passed to :meth:`do_request`.
@@ -142,6 +145,7 @@ class HTTPXRequest(BaseRequest):
def __init__(
self,
connection_pool_size: int = 1,
proxy_url: Optional[Union[str, httpx.Proxy, httpx.URL]] = None,
read_timeout: Optional[float] = 5.0,
write_timeout: Optional[float] = 5.0,
connect_timeout: Optional[float] = 5.0,
@@ -152,6 +156,18 @@ class HTTPXRequest(BaseRequest):
media_write_timeout: Optional[float] = 20.0,
httpx_kwargs: Optional[dict[str, Any]] = None,
):
if proxy_url is not None and proxy is not None:
raise ValueError("The parameters `proxy_url` and `proxy` are mutually exclusive.")
if proxy_url is not None:
proxy = proxy_url
warn(
PTBDeprecationWarning(
"20.7", "The parameter `proxy_url` is deprecated. Use `proxy` instead."
),
stacklevel=2,
)
self._http_version = http_version
self._media_write_timeout = media_write_timeout
timeout = httpx.Timeout(
+2 -24
View File
@@ -58,8 +58,6 @@ def input_media_video(class_thumb_file):
parse_mode=InputMediaVideoTestBase.parse_mode,
caption_entities=InputMediaVideoTestBase.caption_entities,
thumbnail=class_thumb_file,
cover=class_thumb_file,
start_timestamp=InputMediaVideoTestBase.start_timestamp,
supports_streaming=InputMediaVideoTestBase.supports_streaming,
has_spoiler=InputMediaVideoTestBase.has_spoiler,
show_caption_above_media=InputMediaVideoTestBase.show_caption_above_media,
@@ -132,8 +130,6 @@ def input_paid_media_video(class_thumb_file):
return InputPaidMediaVideo(
media=InputMediaVideoTestBase.media,
thumbnail=class_thumb_file,
cover=class_thumb_file,
start_timestamp=InputMediaVideoTestBase.start_timestamp,
width=InputMediaVideoTestBase.width,
height=InputMediaVideoTestBase.height,
duration=InputMediaVideoTestBase.duration,
@@ -148,7 +144,6 @@ class InputMediaVideoTestBase:
width = 3
height = 4
duration = 5
start_timestamp = 3
parse_mode = "HTML"
supports_streaming = True
caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)]
@@ -174,8 +169,6 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
assert input_media_video.caption_entities == tuple(self.caption_entities)
assert input_media_video.supports_streaming == self.supports_streaming
assert isinstance(input_media_video.thumbnail, InputFile)
assert isinstance(input_media_video.cover, InputFile)
assert input_media_video.start_timestamp == self.start_timestamp
assert input_media_video.has_spoiler == self.has_spoiler
assert input_media_video.show_caption_above_media == self.show_caption_above_media
@@ -201,8 +194,6 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
input_media_video_dict["show_caption_above_media"]
== input_media_video.show_caption_above_media
)
assert input_media_video_dict["cover"] == input_media_video.cover
assert input_media_video_dict["start_timestamp"] == input_media_video.start_timestamp
def test_with_video(self, video):
# fixture found in test_video
@@ -223,13 +214,10 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
def test_with_local_files(self):
input_media_video = InputMediaVideo(
data_file("telegram.mp4"),
thumbnail=data_file("telegram.jpg"),
cover=data_file("telegram.jpg"),
data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg")
)
assert input_media_video.media == data_file("telegram.mp4").as_uri()
assert input_media_video.thumbnail == data_file("telegram.jpg").as_uri()
assert input_media_video.cover == data_file("telegram.jpg").as_uri()
def test_type_enum_conversion(self):
# Since we have a lot of different test classes for all the input media types, we test this
@@ -577,8 +565,6 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
assert input_paid_media_video.duration == self.duration
assert input_paid_media_video.supports_streaming == self.supports_streaming
assert isinstance(input_paid_media_video.thumbnail, InputFile)
assert isinstance(input_paid_media_video.cover, InputFile)
assert input_paid_media_video.start_timestamp == self.start_timestamp
def test_to_dict(self, input_paid_media_video):
input_paid_media_video_dict = input_paid_media_video.to_dict()
@@ -592,11 +578,6 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
== input_paid_media_video.supports_streaming
)
assert input_paid_media_video_dict["thumbnail"] == input_paid_media_video.thumbnail
assert input_paid_media_video_dict["cover"] == input_paid_media_video.cover
assert (
input_paid_media_video_dict["start_timestamp"]
== input_paid_media_video.start_timestamp
)
def test_with_video(self, video):
# fixture found in test_video
@@ -615,13 +596,10 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
def test_with_local_files(self):
input_paid_media_video = InputPaidMediaVideo(
data_file("telegram.mp4"),
thumbnail=data_file("telegram.jpg"),
cover=data_file("telegram.jpg"),
data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg")
)
assert input_paid_media_video.media == data_file("telegram.mp4").as_uri()
assert input_paid_media_video.thumbnail == data_file("telegram.jpg").as_uri()
assert input_paid_media_video.cover == data_file("telegram.jpg").as_uri()
@pytest.fixture(scope="module")
+1 -16
View File
@@ -46,8 +46,6 @@ class VideoTestBase:
mime_type = "video/mp4"
supports_streaming = True
file_name = "telegram.mp4"
start_timestamp = 3
cover = (PhotoSize("file_id", "unique_id", 640, 360, file_size=0),)
thumb_width = 180
thumb_height = 320
thumb_file_size = 1767
@@ -94,8 +92,6 @@ class TestVideoWithoutRequest(VideoTestBase):
"mime_type": self.mime_type,
"file_size": self.file_size,
"file_name": self.file_name,
"start_timestamp": self.start_timestamp,
"cover": [photo_size.to_dict() for photo_size in self.cover],
}
json_video = Video.de_json(json_dict, offline_bot)
assert json_video.api_kwargs == {}
@@ -108,8 +104,6 @@ class TestVideoWithoutRequest(VideoTestBase):
assert json_video.mime_type == self.mime_type
assert json_video.file_size == self.file_size
assert json_video.file_name == self.file_name
assert json_video.start_timestamp == self.start_timestamp
assert json_video.cover == self.cover
def test_to_dict(self, video):
video_dict = video.to_dict()
@@ -229,9 +223,7 @@ class TestVideoWithoutRequest(VideoTestBase):
class TestVideoWithRequest(VideoTestBase):
@pytest.mark.parametrize("duration", [dtm.timedelta(seconds=5), 5])
async def test_send_all_args(
self, bot, chat_id, video_file, video, thumb_file, photo_file, duration
):
async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file, duration):
message = await bot.send_video(
chat_id,
video_file,
@@ -244,8 +236,6 @@ class TestVideoWithRequest(VideoTestBase):
height=video.height,
parse_mode="Markdown",
thumbnail=thumb_file,
cover=photo_file,
start_timestamp=self.start_timestamp,
has_spoiler=True,
show_caption_above_media=True,
)
@@ -266,11 +256,6 @@ class TestVideoWithRequest(VideoTestBase):
assert message.video.thumbnail.width == self.thumb_width
assert message.video.thumbnail.height == self.thumb_height
assert message.video.start_timestamp == self.start_timestamp
assert isinstance(message.video.cover, tuple)
assert isinstance(message.video.cover[0], PhotoSize)
assert message.video.file_name == self.file_name
assert message.has_protected_content
assert message.has_media_spoiler
@@ -28,6 +28,7 @@ from telegram import (
InputTextMessageContent,
)
from telegram.constants import InlineQueryResultType
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.slots import mro_slots
@@ -39,6 +40,7 @@ def inline_query_result_article():
input_message_content=InlineQueryResultArticleTestBase.input_message_content,
reply_markup=InlineQueryResultArticleTestBase.reply_markup,
url=InlineQueryResultArticleTestBase.url,
hide_url=InlineQueryResultArticleTestBase.hide_url,
description=InlineQueryResultArticleTestBase.description,
thumbnail_url=InlineQueryResultArticleTestBase.thumbnail_url,
thumbnail_height=InlineQueryResultArticleTestBase.thumbnail_height,
@@ -53,6 +55,7 @@ class InlineQueryResultArticleTestBase:
input_message_content = InputTextMessageContent("input_message_content")
reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]])
url = "url"
hide_url = True
description = "description"
thumbnail_url = "thumb url"
thumbnail_height = 10
@@ -76,6 +79,7 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
)
assert inline_query_result_article.reply_markup.to_dict() == self.reply_markup.to_dict()
assert inline_query_result_article.url == self.url
assert inline_query_result_article.hide_url == self.hide_url
assert inline_query_result_article.description == self.description
assert inline_query_result_article.thumbnail_url == self.thumbnail_url
assert inline_query_result_article.thumbnail_height == self.thumbnail_height
@@ -97,6 +101,7 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
== inline_query_result_article.reply_markup.to_dict()
)
assert inline_query_result_article_dict["url"] == inline_query_result_article.url
assert inline_query_result_article_dict["hide_url"] == inline_query_result_article.hide_url
assert (
inline_query_result_article_dict["description"]
== inline_query_result_article.description
@@ -153,3 +158,31 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
assert a != e
assert hash(a) != hash(e)
def test_deprecation_warning_for_hide_url(self):
with pytest.warns(PTBDeprecationWarning, match="The argument `hide_url`") as record:
InlineQueryResultArticle(
self.id_, self.title, self.input_message_content, hide_url=True
)
assert record[0].filename == __file__, "wrong stacklevel!"
with pytest.warns(PTBDeprecationWarning, match="The argument `hide_url`") as record:
InlineQueryResultArticle(
self.id_, self.title, self.input_message_content, hide_url=False
)
assert record[0].filename == __file__, "wrong stacklevel!"
assert (
InlineQueryResultArticle(
self.id_, self.title, self.input_message_content, hide_url=True
).hide_url
is True
)
assert (
InlineQueryResultArticle(
self.id_, self.title, self.input_message_content, hide_url=False
).hide_url
is False
)
@@ -282,10 +282,10 @@ class TestInputInvoiceMessageContentWithoutRequest(InputInvoiceMessageContentTes
self.title,
self.description,
self.payload,
self.provider_token,
self.currency,
# the first prices amount & the second lebal changed
[LabeledPrice("label1", 24), LabeledPrice("label22", 314)],
self.provider_token,
)
d = InputInvoiceMessageContent(
self.title,
@@ -19,6 +19,7 @@
import pytest
from telegram import PassportElementErrorFiles, PassportElementErrorSelfie
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.slots import mro_slots
@@ -48,8 +49,8 @@ class TestPassportElementErrorFilesWithoutRequest(PassportElementErrorFilesTestB
def test_expected_values(self, passport_element_error_files):
assert passport_element_error_files.source == self.source
assert passport_element_error_files.type == self.type_
assert isinstance(passport_element_error_files.file_hashes, tuple)
assert passport_element_error_files.file_hashes == tuple(self.file_hashes)
assert isinstance(passport_element_error_files.file_hashes, list)
assert passport_element_error_files.file_hashes == self.file_hashes
assert passport_element_error_files.message == self.message
def test_to_dict(self, passport_element_error_files):
@@ -59,8 +60,9 @@ class TestPassportElementErrorFilesWithoutRequest(PassportElementErrorFilesTestB
assert passport_element_error_files_dict["source"] == passport_element_error_files.source
assert passport_element_error_files_dict["type"] == passport_element_error_files.type
assert passport_element_error_files_dict["message"] == passport_element_error_files.message
assert passport_element_error_files_dict["file_hashes"] == list(
passport_element_error_files.file_hashes
assert (
passport_element_error_files_dict["file_hashes"]
== passport_element_error_files.file_hashes
)
def test_equality(self):
@@ -86,3 +88,13 @@ class TestPassportElementErrorFilesWithoutRequest(PassportElementErrorFilesTestB
assert a != f
assert hash(a) != hash(f)
def test_file_hashes_deprecated(self, passport_element_error_files, recwarn):
passport_element_error_files.file_hashes
assert len(recwarn) == 1
assert (
"The attribute `file_hashes` will return a tuple instead of a list in future major"
" versions." in str(recwarn[0].message)
)
assert recwarn[0].category is PTBDeprecationWarning
assert recwarn[0].filename == __file__
@@ -19,6 +19,7 @@
import pytest
from telegram import PassportElementErrorSelfie, PassportElementErrorTranslationFiles
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.slots import mro_slots
@@ -50,8 +51,8 @@ class TestPassportElementErrorTranslationFilesWithoutRequest(
def test_expected_values(self, passport_element_error_translation_files):
assert passport_element_error_translation_files.source == self.source
assert passport_element_error_translation_files.type == self.type_
assert isinstance(passport_element_error_translation_files.file_hashes, tuple)
assert passport_element_error_translation_files.file_hashes == tuple(self.file_hashes)
assert isinstance(passport_element_error_translation_files.file_hashes, list)
assert passport_element_error_translation_files.file_hashes == self.file_hashes
assert passport_element_error_translation_files.message == self.message
def test_to_dict(self, passport_element_error_translation_files):
@@ -72,8 +73,9 @@ class TestPassportElementErrorTranslationFilesWithoutRequest(
passport_element_error_translation_files_dict["message"]
== passport_element_error_translation_files.message
)
assert passport_element_error_translation_files_dict["file_hashes"] == list(
passport_element_error_translation_files.file_hashes
assert (
passport_element_error_translation_files_dict["file_hashes"]
== passport_element_error_translation_files.file_hashes
)
def test_equality(self):
@@ -99,3 +101,13 @@ class TestPassportElementErrorTranslationFilesWithoutRequest(
assert a != f
assert hash(a) != hash(f)
def test_file_hashes_deprecated(self, passport_element_error_translation_files, recwarn):
passport_element_error_translation_files.file_hashes
assert len(recwarn) == 1
assert (
"The attribute `file_hashes` will return a tuple instead of a list in future major"
" versions." in str(recwarn[0].message)
)
assert recwarn[0].category is PTBDeprecationWarning
assert recwarn[0].filename == __file__
+13 -25
View File
@@ -16,12 +16,10 @@
#
# 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 Bot, File, PassportElementError, PassportFile
from telegram._utils.datetime import UTC, to_timestamp
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.bot_method_checks import (
check_defaults_handling,
check_shortcut_call,
@@ -46,7 +44,7 @@ class PassportFileTestBase:
file_id = "data"
file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e"
file_size = 50
file_date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
file_date = 1532879128
class TestPassportFileWithoutRequest(PassportFileTestBase):
@@ -69,27 +67,7 @@ class TestPassportFileWithoutRequest(PassportFileTestBase):
assert passport_file_dict["file_id"] == passport_file.file_id
assert passport_file_dict["file_unique_id"] == passport_file.file_unique_id
assert passport_file_dict["file_size"] == passport_file.file_size
assert passport_file_dict["file_date"] == to_timestamp(passport_file.file_date)
def test_de_json_localization(self, passport_file, tz_bot, offline_bot, raw_bot):
json_dict = {
"file_id": self.file_id,
"file_unique_id": self.file_unique_id,
"file_size": self.file_size,
"file_date": to_timestamp(self.file_date),
}
pf = PassportFile.de_json(json_dict, offline_bot)
pf_raw = PassportFile.de_json(json_dict, raw_bot)
pf_tz = PassportFile.de_json(json_dict, tz_bot)
# comparing utcoffsets because comparing timezones is unpredicatable
date_offset = pf_tz.file_date.utcoffset()
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(pf_tz.file_date.replace(tzinfo=None))
assert pf_raw.file_date.tzinfo == UTC
assert pf.file_date.tzinfo == UTC
assert date_offset == tz_bot_offset
assert passport_file_dict["file_date"] == passport_file.file_date
def test_equality(self):
a = PassportFile(self.file_id, self.file_unique_id, self.file_size, self.file_date)
@@ -111,6 +89,16 @@ class TestPassportFileWithoutRequest(PassportFileTestBase):
assert a != e
assert hash(a) != hash(e)
def test_file_date_deprecated(self, passport_file, recwarn):
passport_file.file_date
assert len(recwarn) == 1
assert (
"The attribute `file_date` will return a datetime instead of an integer in future"
" major versions." in str(recwarn[0].message)
)
assert recwarn[0].category is PTBDeprecationWarning
assert recwarn[0].filename == __file__
async def test_get_file_instance_method(self, monkeypatch, passport_file):
async def make_assertion(*_, **kwargs):
result = kwargs["file_id"] == passport_file.file_id
@@ -22,14 +22,12 @@ import pytest
from telegram import (
AffiliateInfo,
Chat,
Gift,
PaidMediaVideo,
RevenueWithdrawalStatePending,
Sticker,
TransactionPartner,
TransactionPartnerAffiliateProgram,
TransactionPartnerChat,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
@@ -97,10 +95,6 @@ class TransactionPartnerTestBase:
amount=42,
)
request_count = 42
chat = Chat(
id=3,
type=Chat.CHANNEL,
)
class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
@@ -129,7 +123,6 @@ class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
("telegram_ads", TransactionPartnerTelegramAds),
("telegram_api", TransactionPartnerTelegramApi),
("other", TransactionPartnerOther),
("chat", TransactionPartnerChat),
],
)
def test_subclass(self, offline_bot, tp_type, subclass):
@@ -457,58 +450,3 @@ class TestTransactionPartnerTelegramApiWithoutRequest(TransactionPartnerTestBase
assert a != d
assert hash(a) != hash(d)
@pytest.fixture
def transaction_partner_chat():
return TransactionPartnerChat(
chat=TransactionPartnerTestBase.chat,
gift=TransactionPartnerTestBase.gift,
)
class TestTransactionPartnerChatWithoutRequest(TransactionPartnerTestBase):
type = TransactionPartnerType.CHAT
def test_slot_behaviour(self, transaction_partner_chat):
inst = transaction_partner_chat
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"chat": self.chat.to_dict(),
"gift": self.gift.to_dict(),
}
tp = TransactionPartnerChat.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.type == "chat"
assert tp.chat == self.chat
assert tp.gift == self.gift
def test_to_dict(self, transaction_partner_chat):
json_dict = transaction_partner_chat.to_dict()
assert json_dict["type"] == self.type
assert json_dict["chat"] == self.chat.to_dict()
assert json_dict["gift"] == self.gift.to_dict()
def test_equality(self, transaction_partner_chat):
a = transaction_partner_chat
b = TransactionPartnerChat(
chat=self.chat,
gift=self.gift,
)
c = TransactionPartnerChat(
chat=Chat(id=1, type=Chat.CHANNEL),
)
d = Chat(id=1, type=Chat.CHANNEL)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
+5 -4
View File
@@ -269,9 +269,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
self.title,
self.description,
self.payload,
provider_token,
self.currency,
self.prices,
provider_token,
**kwargs,
)
for kwargs in ({}, {"protect_content": False})
@@ -301,6 +301,7 @@ class TestInvoiceWithRequest(InvoiceTestBase):
self.title,
self.description,
self.payload,
"", # using tg stars
"XTR",
[self.prices[0]],
allow_sending_without_reply=custom,
@@ -314,9 +315,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
self.title,
self.description,
self.payload,
provider_token,
self.currency,
self.prices,
provider_token,
reply_to_message_id=reply_to_message.message_id,
)
assert message.reply_to_message is None
@@ -327,9 +328,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
self.title,
self.description,
self.payload,
provider_token,
self.currency,
self.prices,
provider_token,
reply_to_message_id=reply_to_message.message_id,
)
@@ -339,9 +340,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
self.title,
self.description,
self.payload,
provider_token,
self.currency,
self.prices,
provider_token=provider_token,
max_tip_amount=self.max_tip_amount,
suggested_tip_amounts=self.suggested_tip_amounts,
start_parameter=self.start_parameter,
+2 -3
View File
@@ -351,9 +351,6 @@ def build_kwargs(
allow_sending_without_reply=manually_passed_value,
quote_parse_mode=manually_passed_value,
)
# TODO remove when gift_id isnt marked as optional anymore, tags: deprecated 21.11
elif name == "gift_id":
kws[name] = "GIFT-ID"
return kws
@@ -619,6 +616,8 @@ async def check_defaults_handling(
defaults_no_custom_defaults = Defaults()
kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters}
kwargs["tzinfo"] = zoneinfo.ZoneInfo("America/New_York")
kwargs.pop("disable_web_page_preview") # mutually exclusive with link_preview_options
kwargs.pop("quote") # mutually exclusive with do_quote
kwargs["link_preview_options"] = LinkPreviewOptions(
url="custom_default", show_above_text="custom_default"
)
-4
View File
@@ -71,10 +71,6 @@ class OfflineRequest(BaseRequest):
async def shutdown(self) -> None:
pass
@property
def read_timeout(self):
return 1
def __init__(self, *args, **kwargs):
pass
+44 -42
View File
@@ -38,7 +38,7 @@ from typing import Optional
import pytest
from telegram import Bot, Chat, Message, MessageEntity, User
from telegram.error import InvalidToken, TelegramError
from telegram.error import TelegramError
from telegram.ext import (
Application,
ApplicationBuilder,
@@ -1516,6 +1516,49 @@ class TestApplication:
found_log = True
assert found_log
@pytest.mark.parametrize(
"timeout_name",
["read_timeout", "connect_timeout", "write_timeout", "pool_timeout", "poll_interval"],
)
@pytest.mark.skipif(
platform.system() == "Windows",
reason="Can't send signals without stopping whole process on windows",
)
def test_run_polling_timeout_deprecation_warnings(
self, timeout_name, monkeypatch, recwarn, app
):
def thread_target():
waited = 0
while not app.running:
time.sleep(0.05)
waited += 0.05
if waited > 5:
pytest.fail("App apparently won't start")
time.sleep(0.05)
os.kill(os.getpid(), signal.SIGINT)
monkeypatch.setattr(app.bot, "get_updates", empty_get_updates)
thread = Thread(target=thread_target)
thread.start()
kwargs = {timeout_name: 42}
app.run_polling(drop_pending_updates=True, close_loop=False, **kwargs)
thread.join()
if timeout_name == "poll_interval":
assert len(recwarn) == 0
return
assert len(recwarn) == 1
assert "Setting timeouts via `Application.run_polling` is deprecated." in str(
recwarn[0].message
)
assert recwarn[0].category is PTBDeprecationWarning
assert recwarn[0].filename == __file__, "wrong stacklevel"
@pytest.mark.skipif(
platform.system() == "Windows",
reason="Can't send signals without stopping whole process on windows",
@@ -2316,47 +2359,6 @@ class TestApplication:
for record in recwarn:
assert not str(record.message).startswith("Could not add signal handlers for the stop")
@pytest.mark.parametrize("exception_class", [InvalidToken, TelegramError])
@pytest.mark.parametrize("retries", [3, 0])
@pytest.mark.parametrize("method_name", ["run_polling", "run_webhook"])
async def test_run_polling_webhook_bootstrap_retries(
self, monkeypatch, exception_class, retries, offline_bot, method_name
):
"""This doesn't test all of the internals of the network retry loop. We do that quite
intensively for the `Updater` and here we just want to make sure that the `Application`
does do the retries.
"""
def thread_target():
asyncio.set_event_loop(asyncio.new_event_loop())
app = (
ApplicationBuilder().bot(offline_bot).application_class(PytestApplication).build()
)
async def initialize(*args, **kwargs):
self.count += 1
raise exception_class(str(self.count))
monkeypatch.setattr(app, "initialize", initialize)
method = functools.partial(
getattr(app, method_name),
bootstrap_retries=retries,
close_loop=False,
stop_signals=None,
)
if exception_class == InvalidToken:
with pytest.raises(InvalidToken, match="1"):
method()
else:
with pytest.raises(TelegramError, match=str(retries + 1)):
method()
thread = Thread(target=thread_target)
thread.start()
thread.join(timeout=10)
assert not thread.is_alive(), "Test took to long to run. Aborting"
@pytest.mark.flaky(3, 1) # loop.call_later will error the test when a flood error is received
def test_signal_handlers(self, app, monkeypatch):
# this test should make sure that signal handlers are set by default on Linux + Mac,
+40 -10
View File
@@ -41,6 +41,7 @@ from telegram.ext import (
from telegram.ext._applicationbuilder import _BOT_CHECKS
from telegram.ext._baseupdateprocessor import SimpleUpdateProcessor
from telegram.request import HTTPXRequest
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.constants import PRIVATE_KEY
from tests.auxil.envvars import TEST_WITH_OPT_DEPS
from tests.auxil.files import data_file
@@ -206,6 +207,7 @@ class TestApplicationBuilder:
"write_timeout",
"media_write_timeout",
"proxy",
"proxy_url",
"socket_options",
"bot",
"updater",
@@ -215,8 +217,9 @@ class TestApplicationBuilder:
def test_mutually_exclusive_for_request(self, builder, method):
builder.request(1)
method_name = method.replace("proxy_url", "proxy")
with pytest.raises(
RuntimeError, match=f"`{method}` may only be set, if no request instance"
RuntimeError, match=f"`{method_name}` may only be set, if no request instance"
):
getattr(builder, method)(data_file("private.key"))
@@ -234,6 +237,7 @@ class TestApplicationBuilder:
"get_updates_read_timeout",
"get_updates_write_timeout",
"get_updates_proxy",
"get_updates_proxy_url",
"get_updates_socket_options",
"get_updates_http_version",
"bot",
@@ -243,9 +247,10 @@ class TestApplicationBuilder:
def test_mutually_exclusive_for_get_updates_request(self, builder, method):
builder.get_updates_request(1)
method_name = method.replace("proxy_url", "proxy")
with pytest.raises(
RuntimeError,
match=f"`{method}` may only be set, if no get_updates_request instance",
match=f"`{method_name}` may only be set, if no get_updates_request instance",
):
getattr(builder, method)(data_file("private.key"))
@@ -262,6 +267,7 @@ class TestApplicationBuilder:
"get_updates_pool_timeout",
"get_updates_read_timeout",
"get_updates_write_timeout",
"get_updates_proxy_url",
"get_updates_proxy",
"get_updates_socket_options",
"get_updates_http_version",
@@ -272,6 +278,7 @@ class TestApplicationBuilder:
"write_timeout",
"media_write_timeout",
"proxy",
"proxy_url",
"socket_options",
"http_version",
"bot",
@@ -283,15 +290,17 @@ class TestApplicationBuilder:
def test_mutually_exclusive_for_updater(self, builder, method):
builder.updater(1)
method_name = method.replace("proxy_url", "proxy")
with pytest.raises(
RuntimeError,
match=f"`{method}` may only be set, if no updater",
match=f"`{method_name}` may only be set, if no updater",
):
getattr(builder, method)(data_file("private.key"))
builder = ApplicationBuilder()
getattr(builder, method)(data_file("private.key"))
method = method.replace("proxy_url", "proxy")
with pytest.raises(RuntimeError, match=f"`updater` may only be set, if no {method}"):
builder.updater(1)
@@ -304,6 +313,7 @@ class TestApplicationBuilder:
"get_updates_read_timeout",
"get_updates_write_timeout",
"get_updates_proxy",
"get_updates_proxy_url",
"get_updates_socket_options",
"get_updates_http_version",
"connection_pool_size",
@@ -313,6 +323,7 @@ class TestApplicationBuilder:
"write_timeout",
"media_write_timeout",
"proxy",
"proxy_url",
"socket_options",
"bot",
"http_version",
@@ -330,11 +341,14 @@ class TestApplicationBuilder:
getattr(builder, method)(data_file("private.key"))
builder.updater(None)
# We test with bot the new & legacy version to ensure that the legacy version still works
@pytest.mark.parametrize(
("proxy_method", "get_updates_proxy_method"),
[("proxy", "get_updates_proxy"), ("proxy_url", "get_updates_proxy_url")],
ids=["new", "legacy"],
)
def test_all_bot_args_custom(
self,
builder,
bot,
monkeypatch,
self, builder, bot, monkeypatch, proxy_method, get_updates_proxy_method
):
# Only socket_options is tested in a standalone test, since that's easier
defaults = Defaults()
@@ -389,7 +403,8 @@ class TestApplicationBuilder:
builder = ApplicationBuilder().token(bot.token)
builder.connection_pool_size(1).connect_timeout(2).pool_timeout(3).read_timeout(
4
).write_timeout(5).media_write_timeout(6).http_version("1.1").proxy("proxy")
).write_timeout(5).media_write_timeout(6).http_version("1.1")
getattr(builder, proxy_method)("proxy")
app = builder.build()
client = app.bot.request._client
@@ -408,9 +423,8 @@ class TestApplicationBuilder:
5
).get_updates_http_version(
"1.1"
).get_updates_proxy(
"get_updates_proxy"
)
getattr(builder, get_updates_proxy_method)("get_updates_proxy")
app = builder.build()
client = app.bot._request[0]._client
@@ -571,6 +585,22 @@ class TestApplicationBuilder:
assert isinstance(app.update_queue, asyncio.Queue)
assert isinstance(app.updater, Updater)
def test_proxy_url_deprecation_warning(self, bot, builder, recwarn):
builder.token(bot.token).proxy_url("proxy_url")
assert len(recwarn) == 1
assert "`ApplicationBuilder.proxy_url` is deprecated" in str(recwarn[0].message)
assert recwarn[0].category is PTBDeprecationWarning
assert recwarn[0].filename == __file__, "wrong stacklevel"
def test_get_updates_proxy_url_deprecation_warning(self, bot, builder, recwarn):
builder.token(bot.token).get_updates_proxy_url("get_updates_proxy_url")
assert len(recwarn) == 1
assert "`ApplicationBuilder.get_updates_proxy_url` is deprecated" in str(
recwarn[0].message
)
assert recwarn[0].category is PTBDeprecationWarning
assert recwarn[0].filename == __file__, "wrong stacklevel"
@pytest.mark.parametrize(
("read_timeout", "timeout", "expected"),
[
+30 -4
View File
@@ -22,7 +22,7 @@ import inspect
import pytest
from telegram import User
from telegram import LinkPreviewOptions, User
from telegram.ext import Defaults
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.envvars import TEST_WITH_OPT_DEPS
@@ -31,7 +31,7 @@ from tests.auxil.slots import mro_slots
class TestDefaults:
def test_slot_behaviour(self):
a = Defaults(parse_mode="HTML", do_quote=True)
a = Defaults(parse_mode="HTML", quote=True)
for attr in a.__slots__:
assert getattr(a, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(a)) == len(set(mro_slots(a))), "duplicate slot"
@@ -63,8 +63,8 @@ class TestDefaults:
c = Defaults(parse_mode="HTML", do_quote=True, protect_content=True)
d = Defaults(parse_mode="HTML", protect_content=True)
e = User(123, "test_user", False)
f = Defaults(parse_mode="HTML", block=True)
g = Defaults(parse_mode="HTML", block=True)
f = Defaults(parse_mode="HTML", disable_web_page_preview=True)
g = Defaults(parse_mode="HTML", disable_web_page_preview=True)
assert a == b
assert hash(a) == hash(b)
@@ -81,3 +81,29 @@ class TestDefaults:
assert f == g
assert hash(f) == hash(g)
def test_mutually_exclusive(self):
with pytest.raises(ValueError, match="mutually exclusive"):
Defaults(disable_web_page_preview=True, link_preview_options=LinkPreviewOptions(False))
with pytest.raises(ValueError, match="mutually exclusive"):
Defaults(quote=True, do_quote=False)
def test_deprecation_warning_for_disable_web_page_preview(self):
with pytest.warns(
PTBDeprecationWarning, match="`Defaults.disable_web_page_preview` is "
) as record:
Defaults(disable_web_page_preview=True)
assert record[0].filename == __file__, "wrong stacklevel!"
assert Defaults(disable_web_page_preview=True).link_preview_options.is_disabled is True
assert Defaults(disable_web_page_preview=False).disable_web_page_preview is False
def test_deprecation_warning_for_quote(self):
with pytest.warns(PTBDeprecationWarning, match="`Defaults.quote` is ") as record:
Defaults(quote=True)
assert record[0].filename == __file__, "wrong stacklevel!"
assert Defaults(quote=True).do_quote is True
assert Defaults(quote=False).quote is False
+8
View File
@@ -1068,6 +1068,11 @@ class TestFilters:
assert filters.StatusUpdate.WRITE_ACCESS_ALLOWED.check_update(update)
update.message.write_access_allowed = None
update.message.api_kwargs = {"user_shared": "user_shared"}
assert filters.StatusUpdate.ALL.check_update(update)
assert filters.StatusUpdate.USER_SHARED.check_update(update)
update.message.api_kwargs = {}
update.message.users_shared = "users_shared"
assert filters.StatusUpdate.ALL.check_update(update)
assert filters.StatusUpdate.USERS_SHARED.check_update(update)
@@ -1329,12 +1334,15 @@ class TestFilters:
def test_filters_chat_id(self, update):
assert not filters.Chat(chat_id=1).check_update(update)
assert filters.CHAT.check_update(update)
update.message.chat.id = 1
assert filters.Chat(chat_id=1).check_update(update)
assert filters.CHAT.check_update(update)
update.message.chat.id = 2
assert filters.Chat(chat_id=[1, 2]).check_update(update)
assert not filters.Chat(chat_id=[3, 4]).check_update(update)
update.message.chat = None
assert not filters.CHAT.check_update(update)
assert not filters.Chat(chat_id=[3, 4]).check_update(update)
def test_filters_chat_username(self, update):

Some files were not shown because too many files have changed in this diff Show More