From 44e8292838014fefca5ac91d05ea4b77d4cbf7af Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 15 Jun 2024 10:29:19 +0200 Subject: [PATCH 01/26] Drop `python-telegram-bot-raw` And Switch to `pyproject.toml` Based Packaging (#4288) --- .github/CONTRIBUTING.rst | 11 +- .github/workflows/docs.yml | 2 +- .../pre-commit_dependencies_notifier.yml | 19 -- .github/workflows/readme_notifier.yml | 18 -- .github/workflows/test_official.yml | 5 +- .github/workflows/type_completeness.yml | 3 +- .github/workflows/unit_tests.yml | 11 +- .pre-commit-config.yaml | 2 +- MANIFEST.in | 1 - README.rst | 3 +- README_RAW.rst | 213 ------------------ docs/requirements-docs.txt | 2 +- pyproject.toml | 109 ++++++++- requirements-all.txt | 4 - requirements-dev-all.txt | 5 + requirements-dev.txt | 11 - requirements-opts.txt | 27 --- requirements-unit-tests.txt | 19 ++ requirements.txt | 10 - setup.cfg | 5 +- setup.py | 131 ----------- setup_raw.py | 8 - telegram/__init__.py | 32 +-- telegram/_version.py | 11 +- tests/README.rst | 2 +- tests/test_meta.py | 7 +- 26 files changed, 153 insertions(+), 518 deletions(-) delete mode 100644 .github/workflows/pre-commit_dependencies_notifier.yml delete mode 100644 .github/workflows/readme_notifier.yml delete mode 100644 MANIFEST.in delete mode 100644 README_RAW.rst delete mode 100644 requirements-all.txt create mode 100644 requirements-dev-all.txt delete mode 100644 requirements-dev.txt delete mode 100644 requirements-opts.txt create mode 100644 requirements-unit-tests.txt delete mode 100644 requirements.txt delete mode 100644 setup.py delete mode 100644 setup_raw.py diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index b5ce5921484..635cdb23ebc 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -26,7 +26,7 @@ Setting things up .. code-block:: bash - $ pip install -r requirements-all.txt + $ pip install -r requirements-dev-all.txt 5. Install pre-commit hooks: @@ -210,13 +210,8 @@ doc strings don't have a separate documentation site they generate, instead, the User facing documentation ------------------------- -We use `sphinx`_ to generate static HTML docs. To build them, first make sure you're running Python 3.9 or above and have the required dependencies: - -.. code-block:: bash - - $ pip install -r docs/requirements-docs.txt - -then run the following from the PTB root directory: +We use `sphinx`_ to generate static HTML docs. To build them, first make sure you're running Python 3.9 or above and have the required dependencies installed as explained above. +Then, run the following from the PTB root directory: .. code-block:: bash diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ea1173d6986..73e123e17ea 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip - python -W ignore -m pip install -r requirements-all.txt + 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 - name: Build docs diff --git a/.github/workflows/pre-commit_dependencies_notifier.yml b/.github/workflows/pre-commit_dependencies_notifier.yml deleted file mode 100644 index 6f6428faf36..00000000000 --- a/.github/workflows/pre-commit_dependencies_notifier.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Warning maintainers -on: - pull_request_target: - paths: - - requirements.txt - - requirements-opts.txt - - .pre-commit-config.yaml -permissions: - pull-requests: write -jobs: - job: - runs-on: ubuntu-latest - name: about pre-commit and dependency change - steps: - - name: running the check - uses: Poolitzer/notifier-action@master - with: - notify-message: Hey! Looks like you edited the (optional) requirements or the pre-commit hooks. I'm just a friendly reminder to keep the additional dependencies for the hooks in sync with the requirements :) - repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/readme_notifier.yml b/.github/workflows/readme_notifier.yml deleted file mode 100644 index 4ec7d458760..00000000000 --- a/.github/workflows/readme_notifier.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Warning maintainers -on: - pull_request_target: - paths: - - README.rst - - README_RAW.rst -permissions: - pull-requests: write -jobs: - job: - runs-on: ubuntu-latest - name: about readme change - steps: - - name: running the check - uses: Poolitzer/notifier-action@master - with: - notify-message: Hey! Looks like you edited README.rst or README_RAW.rst. I'm just a friendly reminder to apply relevant changes to both of those files :) - repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test_official.yml b/.github/workflows/test_official.yml index 0510d334f3f..5de11471eaa 100644 --- a/.github/workflows/test_official.yml +++ b/.github/workflows/test_official.yml @@ -29,9 +29,8 @@ jobs: - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip - python -W ignore -m pip install -r requirements.txt - python -W ignore -m pip install -r requirements-opts.txt - python -W ignore -m pip install -r requirements-dev.txt + 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 diff --git a/.github/workflows/type_completeness.yml b/.github/workflows/type_completeness.yml index b2ebd45e303..74087e3e8dd 100644 --- a/.github/workflows/type_completeness.yml +++ b/.github/workflows/type_completeness.yml @@ -3,8 +3,7 @@ on: pull_request: paths: - telegram/** - - requirements.txt - - requirements-opts.txt + - pyproject.toml push: branches: - master diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index fffe4573ddb..8e1c7bb06d2 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -4,9 +4,8 @@ on: paths: - telegram/** - tests/** - - requirements.txt - - requirements-opts.txt - - requirements-dev.txt + - pyproject.toml + - requirements-unit-tests.txt push: branches: - master @@ -35,8 +34,8 @@ jobs: run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -U pytest-cov - python -W ignore -m pip install -r requirements.txt - python -W ignore -m pip install -r requirements-dev.txt + python -W ignore -m pip install . + python -W ignore -m pip install -r requirements-unit-tests.txt python -W ignore -m pip install pytest-xdist[psutil] - name: Test with pytest @@ -65,7 +64,7 @@ jobs: # Test the rest export TEST_WITH_OPT_DEPS='true' - pip install -r requirements-opts.txt + pip install .[all] # `-n auto --dist loadfile` uses pytest-xdist to run each test file on a different CPU # worker. Increasing number of workers has little effect on test duration, but it seems # to increase flakyness, specially on python 3.7 with --dist=loadgroup. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5760c9eac06..08c51942a0b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Make sure that the additional_dependencies here match requirements(-opts).txt +# Make sure that the additional_dependencies here match pyproject.toml ci: autofix_prs: false diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 0efb3c7eca4..00000000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include LICENSE LICENSE.lesser requirements.txt requirements-opts.txt README_RAW.rst telegram/py.typed diff --git a/README.rst b/README.rst index c3b29aa626f..82e272c3f8b 100644 --- a/README.rst +++ b/README.rst @@ -98,7 +98,8 @@ You can also install ``python-telegram-bot`` from source, though this is usually $ git clone https://github.com/python-telegram-bot/python-telegram-bot $ cd python-telegram-bot - $ python setup.py install + $ pip install build + $ python -m build Verifying Releases ------------------ diff --git a/README_RAW.rst b/README_RAW.rst deleted file mode 100644 index e82270959f1..00000000000 --- a/README_RAW.rst +++ /dev/null @@ -1,213 +0,0 @@ -.. image:: https://github.com/python-telegram-bot/logos/blob/master/logo-text/png/ptb-raw-logo-text_768.png?raw=true - :align: center - :target: https://python-telegram-bot.org - :alt: python-telegram-bot-raw Logo - -.. image:: https://img.shields.io/pypi/v/python-telegram-bot-raw.svg - :target: https://pypi.org/project/python-telegram-bot-raw/ - :alt: PyPi Package Version - -.. image:: https://img.shields.io/pypi/pyversions/python-telegram-bot-raw.svg - :target: https://pypi.org/project/python-telegram-bot-raw/ - :alt: Supported Python versions - -.. image:: https://img.shields.io/badge/Bot%20API-7.4-blue?logo=telegram - :target: https://core.telegram.org/bots/api-changelog - :alt: Supported Bot API version - -.. image:: https://img.shields.io/pypi/dm/python-telegram-bot-raw - :target: https://pypistats.org/packages/python-telegram-bot-raw - :alt: PyPi Package Monthly Download - -.. image:: https://readthedocs.org/projects/python-telegram-bot/badge/?version=stable - :target: https://docs.python-telegram-bot.org/ - :alt: Documentation Status - -.. image:: https://img.shields.io/pypi/l/python-telegram-bot-raw.svg - :target: https://www.gnu.org/licenses/lgpl-3.0.html - :alt: LGPLv3 License - -.. image:: https://github.com/python-telegram-bot/python-telegram-bot/actions/workflows/unit_tests.yml/badge.svg?branch=master - :target: https://github.com/python-telegram-bot/python-telegram-bot/ - :alt: Github Actions workflow - -.. image:: https://codecov.io/gh/python-telegram-bot/python-telegram-bot/branch/master/graph/badge.svg - :target: https://app.codecov.io/gh/python-telegram-bot/python-telegram-bot - :alt: Code coverage - -.. image:: https://isitmaintained.com/badge/resolution/python-telegram-bot/python-telegram-bot.svg - :target: https://isitmaintained.com/project/python-telegram-bot/python-telegram-bot - :alt: Median time to resolve an issue - -.. image:: https://api.codacy.com/project/badge/Grade/99d901eaa09b44b4819aec05c330c968 - :target: https://app.codacy.com/gh/python-telegram-bot/python-telegram-bot/dashboard - :alt: Code quality: Codacy - -.. image:: https://results.pre-commit.ci/badge/github/python-telegram-bot/python-telegram-bot/master.svg - :target: https://results.pre-commit.ci/latest/github/python-telegram-bot/python-telegram-bot/master - :alt: pre-commit.ci status - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code Style: Black - -.. image:: https://img.shields.io/badge/Telegram-Channel-blue.svg?logo=telegram - :target: https://t.me/pythontelegrambotchannel - :alt: Telegram Channel - -.. image:: https://img.shields.io/badge/Telegram-Group-blue.svg?logo=telegram - :target: https://telegram.me/pythontelegrambotgroup - :alt: Telegram Group - -⚠️ Deprecation Notice -===================== - -The ``python-telegram-bot-raw`` library will no longer be updated after 21.3. -Please instead use the ``python-telegram-bot`` `library `_. -The change requires no changes in your code and requires no additional dependencies. -For additional information, please see this `channel post `_. - ----- - -We have made you a wrapper you can't refuse - -We have a vibrant community of developers helping each other in our `Telegram group `_. Join us! - -*Stay tuned for library updates and new releases on our* `Telegram Channel `_. - -Introduction -============ - -This library provides a pure Python, asynchronous interface for the -`Telegram Bot API `_. -It's compatible with Python versions **3.8+**. - -``python-telegram-bot-raw`` is part of the `python-telegram-bot `_ ecosystem and provides the pure API functionality extracted from PTB. It therefore does not have independent release schedules, changelogs or documentation. - -Note ----- - -Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conjunction will result in undesired side-effects, so only install *one* of both. - -Telegram API support -==================== - -All types and methods of the Telegram Bot API **7.4** are supported. - -Installing -========== - -You can install or upgrade ``python-telegram-bot`` via - -.. code:: shell - - $ pip install python-telegram-bot-raw --upgrade - -To install a pre-release, use the ``--pre`` `flag `_ in addition. - -You can also install ``python-telegram-bot-raw`` from source, though this is usually not necessary. - -.. code:: shell - - $ git clone https://github.com/python-telegram-bot/python-telegram-bot - $ cd python-telegram-bot - $ python setup_raw.py install - -Note ----- - -Installing the ``.tar.gz`` archive available on PyPi directly via ``pip`` will *not* work as expected, as ``pip`` does not recognize that it should use ``setup_raw.py`` instead of ``setup.py``. - -Verifying Releases ------------------- - -We sign all the releases with a GPG key. -The signatures are uploaded to both the `GitHub releases page `_ and the `PyPI project `_ and end with a suffix ``.asc``. -Please find the public keys `here `_. -The keys are named in the format ``-.gpg`` or ``-current.gpg`` if the key is currently being used for new releases. - -In addition, the GitHub release page also contains the sha1 hashes of the release files in the files with the suffix ``.sha1``. - -This allows you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team. - -Dependencies & Their Versions ------------------------------ - -``python-telegram-bot`` tries to use as few 3rd party dependencies as possible. -However, for some features using a 3rd party library is more sane than implementing the functionality again. -As these features are *optional*, the corresponding 3rd party dependencies are not installed by default. -Instead, they are listed as optional dependencies. -This allows to avoid unnecessary dependency conflicts for users who don't need the optional features. - -The only required dependency is `httpx ~= 0.27 `_ for -``telegram.request.HTTPXRequest``, the default networking backend. - -``python-telegram-bot`` is most useful when used along with additional libraries. -To minimize dependency conflicts, we try to be liberal in terms of version requirements on the (optional) dependencies. -On the other hand, we have to ensure stability of ``python-telegram-bot``, which is why we do apply version bounds. -If you encounter dependency conflicts due to these bounds, feel free to reach out. - -Optional Dependencies -##################### - -PTB can be installed with optional dependencies: - -* ``pip install "python-telegram-bot-raw[passport]"`` installs the `cryptography>=39.0.1 `_ library. Use this, if you want to use Telegram Passport related functionality. -* ``pip install "python-telegram-bot-raw[socks]"`` installs `httpx[socks] `_. Use this, if you want to work behind a Socks5 server. -* ``pip install "python-telegram-bot-raw[http2]"`` installs `httpx[http2] `_. Use this, if you want to use HTTP/2. - -To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot-raw[passport,socks]"``. - -Additionally, the shortcut ``pip install "python-telegram-bot-raw[all]"`` installs all optional dependencies. - -Quick Start -=========== - -Our Wiki contains an `Introduction to the API `_ explaining how the pure Bot API can be accessed via ``python-telegram-bot``. - -Resources -========= - -- The `package documentation `_ is the technical reference for ``python-telegram-bot``. - It contains descriptions of all available classes, modules, methods and arguments as well as the `changelog `_. -- The `wiki `_ is home to number of more elaborate introductions of the different features of ``python-telegram-bot`` and other useful resources that go beyond the technical documentation. -- Our `examples section `_ contains several examples that showcase the different features of both the Bot API and ``python-telegram-bot``. - Even if it is not your approach for learning, please take a look at ``echobot.py``. It is the de facto base for most of the bots out there. - The code for these examples is released to the public domain, so you can start by grabbing the code and building on top of it. -- The `official Telegram Bot API documentation `_ is of course always worth a read. - -Getting help -============ - -If the resources mentioned above don't answer your questions or simply overwhelm you, there are several ways of getting help. - -1. We have a vibrant community of developers helping each other in our `Telegram group `_. Join us! Asking a question here is often the quickest way to get a pointer in the right direction. - -2. Ask questions by opening `a discussion `_. - -3. You can even ask for help on Stack Overflow using the `python-telegram-bot tag `_. - -Concurrency -=========== - -Since v20.0, ``python-telegram-bot`` is built on top of Pythons ``asyncio`` module. -Because ``asyncio`` is in general single-threaded, ``python-telegram-bot`` does currently not aim to be thread-safe. - -Contributing -============ - -Contributions of all sizes are welcome. -Please review our `contribution guidelines `_ to get started. -You can also help by `reporting bugs or feature requests `_. - -Donating -======== -Occasionally we are asked if we accept donations to support the development. -While we appreciate the thought, maintaining PTB is our hobby, and we have almost no running costs for it. We therefore have nothing set up to accept donations. -If you still want to donate, we kindly ask you to donate to another open source project/initiative of your choice instead. - -License -======= - -You may copy, distribute and modify the software provided that modifications are described and licensed for free under `LGPL-3 `_. -Derivatives works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 13bface7d25..08fba15d32f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -4,4 +4,4 @@ furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0. sphinx-paramlinks==0.6.0 sphinxcontrib-mermaid==0.9.2 sphinx-copybutton==0.5.2 -sphinx-inline-tabs==2023.4.21 +sphinx-inline-tabs==2023.4.21 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b02870776ca..4f40bd9100b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,113 @@ +# PACKAGING +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +dynamic = ["version"] +name = "python-telegram-bot" +description = "We have made you a wrapper you can't refuse" +readme = "README.rst" +requires-python = ">=3.8" +license = "LGPL-3.0-only" +license-files = { paths = ["LICENSE", "LICENSE.dual", "LICENSE.lesser"] } +authors = [ + { name = "Leandro Toledo", email = "devs@python-telegram-bot.org" } +] +keywords = [ + "python", + "telegram", + "bot", + "api", + "wrapper", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Communications :: Chat", + "Topic :: Internet", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "httpx ~= 0.27", +] + +[project.urls] +"Homepage" = "https://python-telegram-bot.org" +"Documentation" = "https://docs.python-telegram-bot.org" +"Bug Tracker" = "https://github.com/python-telegram-bot/python-telegram-bot/issues" +"Source Code" = "https://github.com/python-telegram-bot/python-telegram-bot" +"News" = "https://t.me/pythontelegrambotchannel" +"Changelog" = "https://docs.python-telegram-bot.org/en/stable/changelog.html" +"Support" = "https://t.me/pythontelegrambotgroup" + +[project.optional-dependencies] +# Make sure to install those as additional_dependencies in the +# pre-commit hooks for pylint & mypy +# Also update the readme accordingly +# +# When dependencies release new versions and tests succeed, we should try to expand the allowed +# versions and only increase the lower bound if necessary +# +# When adding new groups, make sure to update `ext` and `all` accordingly + +# Optional dependencies for production +all = [ + "python-telegram-bot[ext,http2,passport,socks]", +] +callback-data = [ + # Cachetools doesn't have a strict stability policy. Let's be cautious for now. + "cachetools~=5.3.3", +] +ext = [ + "python-telegram-bot[callback-data,job-queue,rate-limiter,webhooks]", +] +http2 = [ + "httpx[http2]", +] +job-queue = [ + # APS doesn't have a strict stability policy. Let's be cautious for now. + "APScheduler~=3.10.4", + # pytz is required by APS and just needs the lower bound due to #2120 + "pytz>=2018.6", +] +passport = [ + "cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1", +] +rate-limiter = [ + "aiolimiter~=1.1.0", +] +socks = [ + "httpx[socks]", +] +webhooks = [ + # tornado is rather stable, but let's not allow the next major release without prior testing + "tornado~=6.4", +] + + +# HATCH +[tool.hatch.version] +# dynamically evaluates the `__version__` variable in that file +source = "code" +path = "telegram/_version.py" +search-paths = ["telegram"] + +[tool.hatch.build] +packages = ["telegram"] + # BLACK: [tool.black] line-length = 99 -target-version = ['py38', 'py39', 'py310', 'py311'] # ISORT: [tool.isort] # black config @@ -11,7 +117,6 @@ line_length = 99 # RUFF: [tool.ruff] line-length = 99 -target-version = "py38" show-fixes = true [tool.ruff.lint] diff --git a/requirements-all.txt b/requirements-all.txt deleted file mode 100644 index d38ad669196..00000000000 --- a/requirements-all.txt +++ /dev/null @@ -1,4 +0,0 @@ --r requirements.txt --r requirements-dev.txt --r requirements-opts.txt --r docs/requirements-docs.txt \ No newline at end of file diff --git a/requirements-dev-all.txt b/requirements-dev-all.txt new file mode 100644 index 00000000000..995e067c420 --- /dev/null +++ b/requirements-dev-all.txt @@ -0,0 +1,5 @@ +-e .[all] +# needed for pre-commit hooks in the git commit command +pre-commit +-r requirements-unit-tests.txt +-r docs/requirements-docs.txt diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 63f6432ad4a..00000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,11 +0,0 @@ -pre-commit # needed for pre-commit hooks in the git commit command - -# For the test suite -setuptools # required for test_meta -pytest==8.2.1 -pytest-asyncio==0.21.2 # needed because pytest doesn't come with native support for coroutines as tests -pytest-xdist==3.6.1 # xdist runs tests in parallel -flaky # Used for flaky tests (flaky decorator) -beautifulsoup4 # used in test_official for parsing tg docs - -wheel # required for building the wheels for releases diff --git a/requirements-opts.txt b/requirements-opts.txt deleted file mode 100644 index 05ac0d8c718..00000000000 --- a/requirements-opts.txt +++ /dev/null @@ -1,27 +0,0 @@ -# Format: -# package_name==version # req-1, req-2, req-3!ext -# `pip install ptb-raw[req-1/2]` will install `package_name` -# `pip install ptb[req-1/2/3]` will also install `package_name` - -# Make sure to install those as additional_dependencies in the -# pre-commit hooks for pylint & mypy -# Also update the readme accordingly - -# When dependencies release new versions and tests succeed, we should try to expand the allowed -# versions and only increase the lower bound if necessary - -httpx[socks] # socks -httpx[http2] # http2 -cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1 # passport -aiolimiter~=1.1.0 # rate-limiter!ext - -# tornado is rather stable, but let's not allow the next mayor release without prior testing -tornado~=6.4 # webhooks!ext - -# Cachetools and APS don't have a strict stability policy. -# Let's be cautious for now. -cachetools~=5.3.3 # callback-data!ext -APScheduler~=3.10.4 # job-queue!ext - -# pytz is required by APS and just needs the lower bound due to #2120 -pytz>=2018.6 # job-queue!ext diff --git a/requirements-unit-tests.txt b/requirements-unit-tests.txt new file mode 100644 index 00000000000..26951dafa44 --- /dev/null +++ b/requirements-unit-tests.txt @@ -0,0 +1,19 @@ +-e . + +# required for building the wheels for releases +build + +# For the test suite +pytest==8.2.1 + +# needed because pytest doesn't come with native support for coroutines as tests +pytest-asyncio==0.21.2 + +# xdist runs tests in parallel +pytest-xdist==3.6.1 + +# Used for flaky tests (flaky decorator) +flaky + +# used in test_official for parsing tg docs +beautifulsoup4 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 90203e49733..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Make sure to install those as additional_dependencies in the -# pre-commit hooks for pylint & mypy -# Also update the readme accordingly - -# When dependencies release new versions and tests succeed, we should try to expand the allowed -# versions and only increase the lower bound if necessary - -# httpx has no stable release yet, but we've had no stability problems since v20.0a0 either -# Since there have been requests to relax the bound a bit, we allow versions < 1.0.0 -httpx ~= 0.27 diff --git a/setup.cfg b/setup.cfg index 278056b06d2..c24e78bc4e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,5 @@ -[metadata] -license_files = LICENSE, LICENSE.dual, LICENSE.lesser - [flake8] max-line-length = 99 ignore = W503, W605 extend-ignore = E203, E704 -exclude = setup.py, setup_raw.py docs/source/conf.py +exclude = docs/source/conf.py diff --git a/setup.py b/setup.py deleted file mode 100644 index d62ad39ed32..00000000000 --- a/setup.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python -"""The setup and build script for the python-telegram-bot library.""" -import subprocess -import sys -from collections import defaultdict -from pathlib import Path -from typing import Any, Dict, List, Tuple - -from setuptools import find_packages, setup - - -def get_requirements() -> List[str]: - """Build the requirements list for this project""" - requirements_list = [] - - with Path("requirements.txt").open(encoding="utf-8") as reqs: - for install in reqs: - if install.startswith("#"): - continue - requirements_list.append(install.strip()) - - return requirements_list - - -def get_packages_requirements(raw: bool = False) -> Tuple[List[str], List[str]]: - """Build the package & requirements list for this project""" - reqs = get_requirements() - - exclude = ["tests*", "docs*"] - if raw: - exclude.append("telegram.ext*") - - packs = find_packages(exclude=exclude) - - return packs, reqs - - -def get_optional_requirements(raw: bool = False) -> Dict[str, List[str]]: - """Build the optional dependencies""" - requirements = defaultdict(list) - - with Path("requirements-opts.txt").open(encoding="utf-8") as reqs: - for line in reqs: - effective_line = line.strip() - if not effective_line or effective_line.startswith("#"): - continue - dependency, names = effective_line.split("#") - dependency = dependency.strip() - for name in names.split(","): - effective_name = name.strip() - if effective_name.endswith("!ext"): - if raw: - continue - effective_name = effective_name[:-4] - requirements["ext"].append(dependency) - requirements[effective_name].append(dependency) - requirements["all"].append(dependency) - - return requirements - - -def get_setup_kwargs(raw: bool = False) -> Dict[str, Any]: - """Builds a dictionary of kwargs for the setup function""" - packages, requirements = get_packages_requirements(raw=raw) - - raw_ext = "-raw" if raw else "" - readme = Path(f'README{"_RAW" if raw else ""}.rst') - - version_file = Path("telegram/_version.py").read_text(encoding="utf-8") - first_part = version_file.split("# SETUP.PY MARKER")[0] - exec(first_part) # pylint: disable=exec-used - - return { - "script_name": f"setup{raw_ext}.py", - "name": f"python-telegram-bot{raw_ext}", - "version": locals()["__version__"], - "author": "Leandro Toledo", - "author_email": "devs@python-telegram-bot.org", - "license": "LGPLv3", - "url": "https://python-telegram-bot.org/", - # Keywords supported by PyPI can be found at - # https://github.com/pypa/warehouse/blob/aafc5185e57e67d43487ce4faa95913dd4573e14/ - # warehouse/templates/packaging/detail.html#L20-L58 - "project_urls": { - "Documentation": "https://docs.python-telegram-bot.org", - "Bug Tracker": "https://github.com/python-telegram-bot/python-telegram-bot/issues", - "Source Code": "https://github.com/python-telegram-bot/python-telegram-bot", - "News": "https://t.me/pythontelegrambotchannel", - "Changelog": "https://docs.python-telegram-bot.org/en/stable/changelog.html", - }, - "download_url": f"https://pypi.org/project/python-telegram-bot{raw_ext}/", - "keywords": "python telegram bot api wrapper", - "description": "We have made you a wrapper you can't refuse", - "long_description": readme.read_text(encoding="utf-8"), - "long_description_content_type": "text/x-rst", - "packages": packages, - "install_requires": requirements, - "extras_require": get_optional_requirements(raw=raw), - "include_package_data": True, - "classifiers": [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Communications :: Chat", - "Topic :: Internet", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - "python_requires": ">=3.8", - } - - -def main() -> None: - # If we're building, build ptb-raw as well - if set(sys.argv[1:]) in [{"bdist_wheel"}, {"sdist"}, {"sdist", "bdist_wheel"}]: - args = ["python", "setup_raw.py"] - args.extend(sys.argv[1:]) - subprocess.run(args, check=True, capture_output=True) - - setup(**get_setup_kwargs(raw=False)) - - -if __name__ == "__main__": - main() diff --git a/setup_raw.py b/setup_raw.py deleted file mode 100644 index 0e99fb68559..00000000000 --- a/setup_raw.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -"""The setup and build script for the python-telegram-bot-raw library.""" - -from setuptools import setup - -from setup import get_setup_kwargs - -setup(**get_setup_kwargs(raw=True)) diff --git a/telegram/__init__.py b/telegram/__init__.py index 6105c9780c8..675a60e9835 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -242,8 +242,6 @@ "warnings", ) -from pathlib import Path - from . import _version, constants, error, helpers, request, warnings from ._birthdate import Birthdate from ._bot import Bot @@ -443,7 +441,6 @@ from ._update import Update from ._user import User from ._userprofilephotos import UserProfilePhotos -from ._utils.warnings import warn from ._videochat import ( VideoChatEnded, VideoChatParticipantsInvited, @@ -472,33 +469,8 @@ #: #: .. versionchanged:: 20.0 #: This constant was previously named ``bot_api_version``. -__bot_api_version__: str = _version.__bot_api_version__ +__bot_api_version__: str = constants.BOT_API_VERSION #: :class:`typing.NamedTuple`: Shortcut for :const:`telegram.constants.BOT_API_VERSION_INFO`. #: #: .. versionadded:: 20.0 -__bot_api_version_info__: constants._BotAPIVersion = _version.__bot_api_version_info__ - - -if not (Path(__file__).parent.resolve().absolute() / "ext").exists(): - _MESSAGE = ( - "Hey. You seem to be using the `python-telegram-bot-raw` library. " - "Please note that this libray has been deprecated and will no longer be updated. " - "Please instead use the `python-telegram-bot` library. The change requires no " - "changes in your code and requires no additional dependencies. For additional " - "information, please see the channel post at " - "https://t.me/pythontelegrambotchannel/145." - ) - - # DeprecationWarning is ignored by default in Python 3.7 and later by default outside - # __main__ modules. We use both warning categories to increase the chance of the user - # seeing the warning. - - warn( - warnings.PTBDeprecationWarning(version="21.3", message=_MESSAGE), - stacklevel=2, - ) - warn( - message=_MESSAGE, - category=warnings.PTBUserWarning, - stacklevel=2, - ) +__bot_api_version_info__: constants._BotAPIVersion = constants.BOT_API_VERSION_INFO diff --git a/telegram/_version.py b/telegram/_version.py index 34849536283..557a1ab9022 100644 --- a/telegram/_version.py +++ b/telegram/_version.py @@ -19,7 +19,7 @@ # pylint: disable=missing-module-docstring from typing import Final, NamedTuple -__all__ = ("__bot_api_version__", "__bot_api_version_info__", "__version__", "__version_info__") +__all__ = ("__version__", "__version_info__") class Version(NamedTuple): @@ -54,12 +54,3 @@ def __str__(self) -> str: major=21, minor=3, micro=0, releaselevel="final", serial=0 ) __version__: Final[str] = str(__version_info__) - -# # SETUP.PY MARKER -# Lines above this line will be `exec`-cuted in setup.py. Make sure that this only contains -# std-lib imports! - -from telegram import constants # noqa: E402 # pylint: disable=wrong-import-position - -__bot_api_version__: Final[str] = constants.BOT_API_VERSION -__bot_api_version_info__: Final[constants._BotAPIVersion] = constants.BOT_API_VERSION_INFO diff --git a/tests/README.rst b/tests/README.rst index 753dd6a16ca..69591953bc3 100644 --- a/tests/README.rst +++ b/tests/README.rst @@ -4,7 +4,7 @@ Testing in PTB PTB uses `pytest`_ for testing. To run the tests, you need to have pytest installed along with a few other dependencies. You can find the list of dependencies -in the ``requirements-dev.txt`` file in the root of the repository. +in the ``pyproject.toml`` file in the root of the repository. Running tests ============= diff --git a/tests/test_meta.py b/tests/test_meta.py index fd698585dbb..7b83e7bb93a 100644 --- a/tests/test_meta.py +++ b/tests/test_meta.py @@ -35,9 +35,4 @@ def _change_test_dir(request, monkeypatch): @skip_disabled def test_build(): - assert os.system("python setup.py bdist_dumb") == 0 # pragma: no cover - - -@skip_disabled -def test_build_raw(): - assert os.system("python setup_raw.py bdist_dumb") == 0 # pragma: no cover + assert os.system("python -m build") == 0 # pragma: no cover From a83046e1ec9aafc1bbccf84fb6ffad3df4ffa613 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:32:47 -0400 Subject: [PATCH 02/26] Add `mise-en-place` to `.gitignore` (#4300) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index fa3d7aa52c9..470d2a2aac1 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,6 @@ telegram.jpg # virtual env venv* + +# environment manager: +.mise.toml \ No newline at end of file From 5b1e7399a48e802418ed38719d89414198793bdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:20:47 +0200 Subject: [PATCH 03/26] Bump `pytest` from 8.2.1 to 8.2.2 (#4294) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- requirements-unit-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-unit-tests.txt b/requirements-unit-tests.txt index 26951dafa44..02f80fe0803 100644 --- a/requirements-unit-tests.txt +++ b/requirements-unit-tests.txt @@ -4,7 +4,7 @@ build # For the test suite -pytest==8.2.1 +pytest==8.2.2 # needed because pytest doesn't come with native support for coroutines as tests pytest-asyncio==0.21.2 From 9ce0f498823c142599b6f722be83d212f17c3283 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:25:02 -0400 Subject: [PATCH 04/26] Add Support for Python 3.13 Beta (#4253) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/type_completeness.yml | 4 +- .github/workflows/unit_tests.yml | 2 +- pyproject.toml | 3 + telegram/_chatfullinfo.py | 8 +-- telegram/_giveaway.py | 2 +- telegram/_inline/inputtextmessagecontent.py | 2 +- telegram/_update.py | 4 +- telegram/ext/_application.py | 69 +++++++++++---------- tests/auxil/pytest_classes.py | 6 +- tests/conftest.py | 6 +- tests/ext/test_application.py | 62 +++++++++++++++--- tests/request/test_request.py | 11 ++-- tests/test_bot.py | 4 +- tests/test_message.py | 6 +- 14 files changed, 123 insertions(+), 66 deletions(-) diff --git a/.github/workflows/type_completeness.yml b/.github/workflows/type_completeness.yml index 74087e3e8dd..4a98c0b30a8 100644 --- a/.github/workflows/type_completeness.yml +++ b/.github/workflows/type_completeness.yml @@ -18,12 +18,12 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 cache: 'pip' cache-dependency-path: '**/requirements*.txt' - name: Install Pyright run: | - python -W ignore -m pip install pyright~=1.1.316 + python -W ignore -m pip install pyright~=1.1.367 - name: Get PR Completeness # Must run before base completeness, as base completeness will checkout the base branch # And we can't go back to the PR branch after that in case the PR is coming from a fork diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 8e1c7bb06d2..214eca12b30 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-beta.2'] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: False steps: diff --git a/pyproject.toml b/pyproject.toml index 4f40bd9100b..13ae98395c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ "httpx ~= 0.27", @@ -82,6 +83,8 @@ job-queue = [ ] passport = [ "cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1", + # cffi is a dependency of cryptography and added support for python 3.13 in 1.17.0rc1 + "cffi >= 1.17.0rc1; python_version > '3.12'" ] rate-limiter = [ "aiolimiter~=1.1.0", diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index 213baed7ef2..3458f6fa6b3 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -490,10 +490,10 @@ def __init__( self.unrestrict_boost_count: Optional[int] = unrestrict_boost_count self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name self.birthdate: Optional[Birthdate] = birthdate - self.personal_chat: Optional["Chat"] = personal_chat - self.business_intro: Optional["BusinessIntro"] = business_intro - self.business_location: Optional["BusinessLocation"] = business_location - self.business_opening_hours: Optional["BusinessOpeningHours"] = business_opening_hours + self.personal_chat: Optional[Chat] = personal_chat + self.business_intro: Optional[BusinessIntro] = business_intro + self.business_location: Optional[BusinessLocation] = business_location + self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatFullInfo"]: diff --git a/telegram/_giveaway.py b/telegram/_giveaway.py index 3251898031d..ed6d4a28895 100644 --- a/telegram/_giveaway.py +++ b/telegram/_giveaway.py @@ -313,7 +313,7 @@ def __init__( self.winner_count: int = winner_count self.unclaimed_prize_count: Optional[int] = unclaimed_prize_count - self.giveaway_message: Optional["Message"] = giveaway_message + self.giveaway_message: Optional[Message] = giveaway_message self._id_attrs = ( self.winner_count, diff --git a/telegram/_inline/inputtextmessagecontent.py b/telegram/_inline/inputtextmessagecontent.py index 0e127ce70a7..475f9c5bb28 100644 --- a/telegram/_inline/inputtextmessagecontent.py +++ b/telegram/_inline/inputtextmessagecontent.py @@ -108,7 +108,7 @@ def __init__( # Optionals self.parse_mode: ODVInput[str] = parse_mode self.entities: Tuple[MessageEntity, ...] = parse_sequence_arg(entities) - self.link_preview_options: ODVInput["LinkPreviewOptions"] = parse_lpo_and_dwpp( + self.link_preview_options: ODVInput[LinkPreviewOptions] = parse_lpo_and_dwpp( disable_web_page_preview, link_preview_options ) diff --git a/telegram/_update.py b/telegram/_update.py index 784dea52aba..68ff52649d2 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -446,7 +446,7 @@ def __init__( ) self._effective_user: Optional[User] = None - self._effective_sender: Optional[Union["User", "Chat"]] = None + self._effective_sender: Optional[Union[User, Chat]] = None self._effective_chat: Optional[Chat] = None self._effective_message: Optional[Message] = None @@ -568,7 +568,7 @@ def effective_sender(self) -> Optional[Union["User", "Chat"]]: if self._effective_sender: return self._effective_sender - sender: Optional[Union["User", "Chat"]] = None + sender: Optional[Union[User, Chat]] = None if message := ( self.message diff --git a/telegram/ext/_application.py b/telegram/ext/_application.py index f5a9d6df49a..4f623ed3695 100644 --- a/telegram/ext/_application.py +++ b/telegram/ext/_application.py @@ -251,39 +251,44 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica """ __slots__ = ( - "__create_task_tasks", - "__update_fetcher_task", - "__update_persistence_event", - "__update_persistence_lock", - "__update_persistence_task", + ( # noqa: RUF005 + "__create_task_tasks", + "__update_fetcher_task", + "__update_persistence_event", + "__update_persistence_lock", + "__update_persistence_task", + "__stop_running_marker", + "_chat_data", + "_chat_ids_to_be_deleted_in_persistence", + "_chat_ids_to_be_updated_in_persistence", + "_conversation_handler_conversations", + "_initialized", + "_job_queue", + "_running", + "_update_processor", + "_user_data", + "_user_ids_to_be_deleted_in_persistence", + "_user_ids_to_be_updated_in_persistence", + "bot", + "bot_data", + "chat_data", + "context_types", + "error_handlers", + "handlers", + "persistence", + "post_init", + "post_shutdown", + "post_stop", + "update_queue", + "updater", + "user_data", + ) # Allowing '__weakref__' creation here since we need it for the JobQueue - # Uncomment if necessary - currently the __weakref__ slot is already created - # in the AsyncContextManager base class - # "__weakref__", - "_chat_data", - "_chat_ids_to_be_deleted_in_persistence", - "_chat_ids_to_be_updated_in_persistence", - "_conversation_handler_conversations", - "_initialized", - "_job_queue", - "_running", - "_update_processor", - "_user_data", - "_user_ids_to_be_deleted_in_persistence", - "_user_ids_to_be_updated_in_persistence", - "bot", - "bot_data", - "chat_data", - "context_types", - "error_handlers", - "handlers", - "persistence", - "post_init", - "post_shutdown", - "post_stop", - "update_queue", - "updater", - "user_data", + # Currently the __weakref__ slot is already created + # in the AsyncContextManager base class for pythons < 3.13 + + ("__weakref__",) + if sys.version_info >= (3, 13) + else () ) def __init__( diff --git a/tests/auxil/pytest_classes.py b/tests/auxil/pytest_classes.py index 5586a8ea0b7..1b976b02e6c 100644 --- a/tests/auxil/pytest_classes.py +++ b/tests/auxil/pytest_classes.py @@ -21,7 +21,7 @@ pytest framework. A common change is to allow monkeypatching of the class members by not enforcing slots in the subclasses.""" from telegram import Bot, Message, User -from telegram.ext import Application, ExtBot +from telegram.ext import Application, ExtBot, Updater from tests.auxil.ci_bots import BOT_INFO_PROVIDER from tests.auxil.constants import PRIVATE_KEY from tests.auxil.envvars import TEST_WITH_OPT_DEPS @@ -89,6 +89,10 @@ class PytestMessage(Message): pass +class PytestUpdater(Updater): + pass + + def make_bot(bot_info=None, **kwargs): """ Tests are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot diff --git a/tests/conftest.py b/tests/conftest.py index 213bcff4a23..a9ef3e68641 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ import datetime import logging import sys +from pathlib import Path from typing import Dict, List from uuid import uuid4 @@ -291,6 +292,5 @@ def timezone(tzinfo): @pytest.fixture() -def tmp_file(tmp_path): - with tmp_path / uuid4().hex as file: - yield file +def tmp_file(tmp_path) -> Path: + return tmp_path / uuid4().hex diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index 714abf8537a..acfce013acc 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -61,7 +61,7 @@ from tests.auxil.build_messages import make_message_update from tests.auxil.files import PROJECT_ROOT_PATH from tests.auxil.networking import send_webhook_message -from tests.auxil.pytest_classes import make_bot +from tests.auxil.pytest_classes import PytestApplication, PytestUpdater, make_bot from tests.auxil.slots import mro_slots @@ -1581,7 +1581,13 @@ def thread_target(): async def post_init(app: Application) -> None: events.append("post_init") - app = Application.builder().bot(one_time_bot).post_init(post_init).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_init(post_init) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr( @@ -1624,7 +1630,13 @@ def thread_target(): async def post_shutdown(app: Application) -> None: events.append("post_shutdown") - app = Application.builder().bot(one_time_bot).post_shutdown(post_shutdown).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_shutdown(post_shutdown) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr( @@ -1650,7 +1662,7 @@ async def post_shutdown(app: Application) -> None: platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) - def test_run_polling_post_stop(self, bot, monkeypatch): + def test_run_polling_post_stop(self, one_time_bot, monkeypatch): events = [] async def get_updates(*args, **kwargs): @@ -1671,7 +1683,13 @@ def thread_target(): async def post_stop(app: Application) -> None: events.append("post_stop") - app = Application.builder().token(bot.token).post_stop(post_stop).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_stop(post_stop) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr(app, "stop", call_after(app.stop, lambda _: events.append("stop"))) @@ -1863,7 +1881,13 @@ def thread_target(): async def post_init(app: Application) -> None: events.append("post_init") - app = Application.builder().bot(one_time_bot).post_init(post_init).build() + app = ( + Application.builder() + .post_init(post_init) + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) @@ -1923,7 +1947,13 @@ def thread_target(): async def post_shutdown(app: Application) -> None: events.append("post_shutdown") - app = Application.builder().bot(one_time_bot).post_shutdown(post_shutdown).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_shutdown(post_shutdown) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) @@ -1960,7 +1990,7 @@ async def post_shutdown(app: Application) -> None: platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) - def test_run_webhook_post_stop(self, bot, monkeypatch): + def test_run_webhook_post_stop(self, one_time_bot, monkeypatch): events = [] async def delete_webhook(*args, **kwargs): @@ -1987,7 +2017,13 @@ def thread_target(): async def post_stop(app: Application) -> None: events.append("post_stop") - app = Application.builder().token(bot.token).post_stop(post_stop).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_stop(post_stop) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) @@ -2480,7 +2516,13 @@ async def task(app): app.create_task(task(app)) - app = ApplicationBuilder().bot(one_time_bot).post_init(post_init).build() + app = ( + ApplicationBuilder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_init(post_init) + .build() + ) monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) diff --git a/tests/request/test_request.py b/tests/request/test_request.py index 47e7d2125f6..ecfb65ece36 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -47,6 +47,7 @@ from telegram.request._requestparameter import RequestParameter from telegram.warnings import PTBDeprecationWarning from tests.auxil.envvars import TEST_WITH_OPT_DEPS +from tests.auxil.networking import NonchalantHttpxRequest from tests.auxil.slots import mro_slots # We only need mixed_rqs fixture, but it uses the others, so pytest needs us to import them as well @@ -72,7 +73,7 @@ async def make_assertion(*args, **kwargs): @pytest.fixture() async def httpx_request(): - async with HTTPXRequest() as rq: + async with NonchalantHttpxRequest() as rq: yield rq @@ -137,7 +138,7 @@ async def initialize(): async def shutdown(): self.test_flag.append("stop") - httpx_request = HTTPXRequest() + httpx_request = NonchalantHttpxRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx_request, "shutdown", shutdown) @@ -154,7 +155,7 @@ async def initialize(): async def shutdown(): self.test_flag = "stop" - httpx_request = HTTPXRequest() + httpx_request = NonchalantHttpxRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx_request, "shutdown", shutdown) @@ -545,7 +546,7 @@ async def initialize(): async def aclose(*args): self.test_flag.append("stop") - httpx_request = HTTPXRequest() + httpx_request = NonchalantHttpxRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx.AsyncClient, "aclose", aclose) @@ -562,7 +563,7 @@ async def initialize(): async def aclose(*args): self.test_flag = "stop" - httpx_request = HTTPXRequest() + httpx_request = NonchalantHttpxRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx.AsyncClient, "aclose", aclose) diff --git a/tests/test_bot.py b/tests/test_bot.py index 8fa53628193..d22ea96db2e 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -97,7 +97,7 @@ from tests.auxil.ci_bots import FALLBACKS from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS from tests.auxil.files import data_file -from tests.auxil.networking import expect_bad_request +from tests.auxil.networking import NonchalantHttpxRequest, expect_bad_request from tests.auxil.pytest_classes import PytestBot, PytestExtBot, make_bot from tests.auxil.slots import mro_slots @@ -253,7 +253,7 @@ async def initialize(*args, **kwargs): async def stop(*args, **kwargs): self.test_flag.append("stop") - temp_bot = PytestBot(token=bot.token) + temp_bot = PytestBot(token=bot.token, request=NonchalantHttpxRequest()) orig_stop = temp_bot.request.shutdown try: diff --git a/tests/test_message.py b/tests/test_message.py index c51e3a92a68..075d7089d3a 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -2624,7 +2624,9 @@ async def make_assertion(*args, **kwargs): async def test_default_do_quote( self, bot, message, default_quote, chat_type, expected, monkeypatch ): - message.set_bot(PytestExtBot(token=bot.token, defaults=Defaults(do_quote=default_quote))) + original_bot = message.get_bot() + temp_bot = PytestExtBot(token=bot.token, defaults=Defaults(do_quote=default_quote)) + message.set_bot(temp_bot) async def make_assertion(*_, **kwargs): reply_parameters = kwargs.get("reply_parameters") or ReplyParameters(message_id=False) @@ -2637,7 +2639,7 @@ async def make_assertion(*_, **kwargs): message.chat.type = chat_type assert await message.reply_text("test") finally: - message.get_bot()._defaults = None + message.set_bot(original_bot) async def test_edit_forum_topic(self, monkeypatch, message): async def make_assertion(*_, **kwargs): From 51ef571a0761bbfea136c455bbfa67d47b20585c Mon Sep 17 00:00:00 2001 From: Palaptin <100526200+Palaptin@users.noreply.github.com> Date: Sun, 23 Jun 2024 20:15:37 +0200 Subject: [PATCH 05/26] Add Lower Bound for `flaky` Dependency (#4322) --- requirements-unit-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-unit-tests.txt b/requirements-unit-tests.txt index 02f80fe0803..eb69f9d9283 100644 --- a/requirements-unit-tests.txt +++ b/requirements-unit-tests.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.21.2 pytest-xdist==3.6.1 # Used for flaky tests (flaky decorator) -flaky +flaky>=3.8.1 # used in test_official for parsing tg docs beautifulsoup4 \ No newline at end of file From cfc75bb08ba07aac21cedd7d946fcb3bcaf4cdef Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 30 Jun 2024 12:22:12 -0400 Subject: [PATCH 06/26] Bump `ruff` and Add New Rules (#4329) --- .pre-commit-config.yaml | 2 +- examples/passportbot.py | 20 ++++++++++---------- pyproject.toml | 7 +++---- telegram/__main__.py | 1 + telegram/ext/_jobqueue.py | 4 +++- tests/request/test_request.py | 4 ++-- tests/test_bot.py | 14 +++++++------- 7 files changed, 27 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08c51942a0b..a15b1f1bf4a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.4.3' + rev: 'v0.5.0' hooks: - id: ruff name: ruff diff --git a/examples/passportbot.py b/examples/passportbot.py index e6d783240fb..c883e0f76fe 100644 --- a/examples/passportbot.py +++ b/examples/passportbot.py @@ -47,9 +47,9 @@ async def msg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: # Files will be downloaded to current directory for data in passport_data.decrypted_data: # This is where the data gets decrypted if data.type == "phone_number": - print("Phone: ", data.phone_number) + logger.info("Phone: %s", data.phone_number) elif data.type == "email": - print("Email: ", data.email) + logger.info("Email: %s", data.email) if data.type in ( "personal_details", "passport", @@ -58,7 +58,7 @@ async def msg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: "internal_passport", "address", ): - print(data.type, data.data) + logger.info(data.type, data.data) if data.type in ( "utility_bill", "bank_statement", @@ -66,28 +66,28 @@ async def msg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: "passport_registration", "temporary_registration", ): - print(data.type, len(data.files), "files") + logger.info(data.type, len(data.files), "files") for file in data.files: actual_file = await file.get_file() - print(actual_file) + logger.info(actual_file) await actual_file.download_to_drive() if ( data.type in ("passport", "driver_license", "identity_card", "internal_passport") and data.front_side ): front_file = await data.front_side.get_file() - print(data.type, front_file) + logger.info(data.type, front_file) await front_file.download_to_drive() if data.type in ("driver_license" and "identity_card") and data.reverse_side: reverse_file = await data.reverse_side.get_file() - print(data.type, reverse_file) + logger.info(data.type, reverse_file) await reverse_file.download_to_drive() if ( data.type in ("passport", "driver_license", "identity_card", "internal_passport") and data.selfie ): selfie_file = await data.selfie.get_file() - print(data.type, selfie_file) + logger.info(data.type, selfie_file) await selfie_file.download_to_drive() if data.translation and data.type in ( "passport", @@ -100,10 +100,10 @@ async def msg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: "passport_registration", "temporary_registration", ): - print(data.type, len(data.translation), "translation") + logger.info(data.type, len(data.translation), "translation") for file in data.translation: actual_file = await file.get_file() - print(actual_file) + logger.info(actual_file) await actual_file.download_to_drive() diff --git a/pyproject.toml b/pyproject.toml index 13ae98395c0..2484250c3d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,17 +124,16 @@ show-fixes = true [tool.ruff.lint] preview = true -explicit-preview-rules = true +explicit-preview-rules = true # TODO: Drop this when RUF022 and RUF023 are out of preview ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PERF203"] select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE", "G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022", - "RUF023", "Q", "INP", "W", "YTT", "DTZ", "ARG"] -# Add "FURB" after it's out of preview + "RUF023", "Q", "INP", "W", "YTT", "DTZ", "ARG", "T20", "FURB"] # Add "A (flake8-builtins)" after we drop pylint [tool.ruff.lint.per-file-ignores] "tests/*.py" = ["B018"] -"tests/**.py" = ["RUF012", "ASYNC101", "DTZ", "ARG"] +"tests/**.py" = ["RUF012", "ASYNC230", "DTZ", "ARG", "T201"] "docs/**.py" = ["INP001", "ARG"] "examples/**.py" = ["ARG"] diff --git a/telegram/__main__.py b/telegram/__main__.py index 7f79dc0278a..2491a330ac6 100644 --- a/telegram/__main__.py +++ b/telegram/__main__.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. # pylint: disable=missing-module-docstring +# ruff: noqa: T201 import subprocess import sys from typing import Optional diff --git a/telegram/ext/_jobqueue.py b/telegram/ext/_jobqueue.py index 6edd5a892ea..db84841d5be 100644 --- a/telegram/ext/_jobqueue.py +++ b/telegram/ext/_jobqueue.py @@ -106,7 +106,9 @@ def __init__(self) -> None: self._application: Optional[weakref.ReferenceType[Application]] = None self._executor = AsyncIOExecutor() - self.scheduler: "AsyncIOScheduler" = AsyncIOScheduler(**self.scheduler_configuration) + self.scheduler: "AsyncIOScheduler" = AsyncIOScheduler( # noqa: UP037 + **self.scheduler_configuration + ) def __repr__(self) -> str: """Give a string representation of the JobQueue in the form ``JobQueue[application=...]``. diff --git a/tests/request/test_request.py b/tests/request/test_request.py index ecfb65ece36..0f664cbdbcf 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -605,9 +605,9 @@ async def make_assertion(_, **kwargs): read_timeout=default_timeouts.read, write_timeout=default_timeouts.write, pool_timeout=default_timeouts.pool, - ) as httpx_request: + ) as httpx_request_ctx: monkeypatch.setattr(httpx.AsyncClient, "request", make_assertion) - await httpx_request.do_request( + await httpx_request_ctx.do_request( method="GET", url="URL", connect_timeout=manual_timeouts.connect, diff --git a/tests/test_bot.py b/tests/test_bot.py index d22ea96db2e..f3902f33a8d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -559,11 +559,11 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): copied_result = copy.copy(result) ext_bot = bot - for bot in (ext_bot, raw_bot): + for bot_type in (ext_bot, raw_bot): # We need to test 1) below both the bot and raw_bot and setting this up with # pytest.parametrize appears to be difficult ... - monkeypatch.setattr(bot.request, "post", make_assertion) - web_app_msg = await bot.answer_web_app_query("12345", result) + monkeypatch.setattr(bot_type.request, "post", make_assertion) + web_app_msg = await bot_type.answer_web_app_query("12345", result) assert params, "something went wrong with passing arguments to the request" assert isinstance(web_app_msg, SentWebAppMessage) assert web_app_msg.inline_message_id == "321" @@ -761,11 +761,11 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): copied_results = copy.copy(results) ext_bot = bot - for bot in (ext_bot, raw_bot): + for bot_type in (ext_bot, raw_bot): # We need to test 1) below both the bot and raw_bot and setting this up with # pytest.parametrize appears to be difficult ... - monkeypatch.setattr(bot.request, "post", make_assertion) - assert await bot.answer_inline_query( + monkeypatch.setattr(bot_type.request, "post", make_assertion) + assert await bot_type.answer_inline_query( 1234, results=results, cache_time=300, @@ -790,7 +790,7 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): copied_results[idx].input_message_content, "disable_web_page_preview", None ) - monkeypatch.delattr(bot.request, "post") + monkeypatch.delattr(bot_type.request, "post") async def test_answer_inline_query_no_default_parse_mode(self, monkeypatch, bot): async def make_assertion(url, request_data: RequestData, *args, **kwargs): From 4ccc80f9c1304b530b140f12566cd97f8164a31b Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:59:54 +0200 Subject: [PATCH 07/26] Make Argument `bot` of `TelegramObject.de_json` Optional (#4320) --- telegram/_botcommandscope.py | 10 ++++++-- telegram/_business.py | 20 +++++++++++---- telegram/_callbackquery.py | 4 ++- telegram/_chatbackground.py | 12 ++++++--- telegram/_chatboost.py | 20 +++++++++++---- telegram/_chatfullinfo.py | 4 ++- telegram/_chatinvitelink.py | 4 ++- telegram/_chatjoinrequest.py | 4 ++- telegram/_chatlocation.py | 4 ++- telegram/_chatmember.py | 4 ++- telegram/_chatmemberupdated.py | 4 ++- telegram/_chatpermissions.py | 4 ++- telegram/_choseninlineresult.py | 4 ++- telegram/_files/_basethumbedmedium.py | 2 +- telegram/_files/sticker.py | 6 +++-- telegram/_files/venue.py | 2 +- telegram/_games/game.py | 2 +- telegram/_games/gamehighscore.py | 4 ++- telegram/_giveaway.py | 12 ++++++--- telegram/_inline/inlinekeyboardbutton.py | 4 ++- telegram/_inline/inlinekeyboardmarkup.py | 4 ++- telegram/_inline/inlinequery.py | 4 ++- telegram/_inline/inlinequeryresultsbutton.py | 4 ++- .../_inline/inputinvoicemessagecontent.py | 2 +- telegram/_keyboardbutton.py | 4 ++- telegram/_keyboardbuttonrequest.py | 2 +- telegram/_menubutton.py | 14 ++++++++--- telegram/_message.py | 7 ++++-- telegram/_messageentity.py | 4 ++- telegram/_messageorigin.py | 4 ++- telegram/_messagereactionupdated.py | 6 +++-- telegram/_passport/credentials.py | 12 ++++++--- .../_passport/encryptedpassportelement.py | 15 ++++++++--- telegram/_passport/passportdata.py | 4 ++- telegram/_passport/passportfile.py | 25 ++++++++++++++++--- telegram/_payment/orderinfo.py | 4 ++- telegram/_payment/precheckoutquery.py | 4 ++- telegram/_payment/shippingquery.py | 4 ++- telegram/_payment/successfulpayment.py | 4 ++- telegram/_poll.py | 14 ++++++++--- telegram/_proximityalerttriggered.py | 4 ++- telegram/_reaction.py | 8 ++++-- telegram/_reply.py | 12 ++++++--- telegram/_shared.py | 12 ++++++--- telegram/_story.py | 2 +- telegram/_telegramobject.py | 20 +++++++++++---- telegram/_update.py | 2 +- telegram/_userprofilephotos.py | 4 ++- telegram/_utils/datetime.py | 5 +++- telegram/_videochat.py | 6 +++-- telegram/_webhookinfo.py | 4 ++- tests/test_telegramobject.py | 5 ++++ 52 files changed, 266 insertions(+), 89 deletions(-) diff --git a/telegram/_botcommandscope.py b/telegram/_botcommandscope.py index 2cac2f50a5b..53e65610c0a 100644 --- a/telegram/_botcommandscope.py +++ b/telegram/_botcommandscope.py @@ -84,13 +84,19 @@ def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None): self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BotCommandScope"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BotCommandScope"]: """Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes care of selecting the correct subclass. Args: data (Dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with this object. + bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to + :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: NEXT.VERSION + :paramref:`bot` is now optional and defaults to :obj:`None` Returns: The Telegram object. diff --git a/telegram/_business.py b/telegram/_business.py index ab1fdb91b51..22c89e024b4 100644 --- a/telegram/_business.py +++ b/telegram/_business.py @@ -106,7 +106,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessConnection"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BusinessConnection"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -175,7 +177,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessMessagesDeleted"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BusinessMessagesDeleted"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -232,7 +236,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessIntro"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BusinessIntro"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -284,7 +290,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessLocation"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BusinessLocation"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -431,7 +439,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessOpeningHours"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BusinessOpeningHours"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_callbackquery.py b/telegram/_callbackquery.py index af89c784b10..d1cdc236be2 100644 --- a/telegram/_callbackquery.py +++ b/telegram/_callbackquery.py @@ -148,7 +148,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["CallbackQuery"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["CallbackQuery"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatbackground.py b/telegram/_chatbackground.py index 66b58c92a25..f9c77619f4c 100644 --- a/telegram/_chatbackground.py +++ b/telegram/_chatbackground.py @@ -78,7 +78,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BackgroundFill"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BackgroundFill"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -267,7 +269,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BackgroundType"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BackgroundType"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -528,7 +532,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatBackground"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatBackground"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatboost.py b/telegram/_chatboost.py index cb7010a3cd4..7b972eec6d8 100644 --- a/telegram/_chatboost.py +++ b/telegram/_chatboost.py @@ -110,7 +110,9 @@ def __init__(self, source: str, *, api_kwargs: Optional[JSONDict] = None): self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatBoostSource"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatBoostSource"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -275,7 +277,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatBoost"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatBoost"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -325,7 +329,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatBoostUpdated"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatBoostUpdated"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -382,7 +388,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatBoostRemoved"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatBoostRemoved"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -429,7 +437,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["UserChatBoosts"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["UserChatBoosts"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index 3458f6fa6b3..349f61318de 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -496,7 +496,9 @@ def __init__( self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatFullInfo"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatFullInfo"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatinvitelink.py b/telegram/_chatinvitelink.py index 18799d9028a..43e7e8ab62d 100644 --- a/telegram/_chatinvitelink.py +++ b/telegram/_chatinvitelink.py @@ -151,7 +151,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatInviteLink"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatInviteLink"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatjoinrequest.py b/telegram/_chatjoinrequest.py index 4d6fdf88581..9c444d97b4d 100644 --- a/telegram/_chatjoinrequest.py +++ b/telegram/_chatjoinrequest.py @@ -129,7 +129,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatJoinRequest"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatJoinRequest"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatlocation.py b/telegram/_chatlocation.py index 3a3c561fc07..04f9854a23a 100644 --- a/telegram/_chatlocation.py +++ b/telegram/_chatlocation.py @@ -68,7 +68,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatLocation"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatLocation"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index b399af30e28..0cc06bf5804 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -104,7 +104,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatMember"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatMember"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatmemberupdated.py b/telegram/_chatmemberupdated.py index c51b88b7b3b..1aacb218533 100644 --- a/telegram/_chatmemberupdated.py +++ b/telegram/_chatmemberupdated.py @@ -141,7 +141,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatMemberUpdated"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatMemberUpdated"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py index 1bda731072e..c4e9e94b7a9 100644 --- a/telegram/_chatpermissions.py +++ b/telegram/_chatpermissions.py @@ -231,7 +231,9 @@ def no_permissions(cls) -> "ChatPermissions": return cls(*(14 * (False,))) @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatPermissions"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatPermissions"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_choseninlineresult.py b/telegram/_choseninlineresult.py index bef8fbb3164..76380e95839 100644 --- a/telegram/_choseninlineresult.py +++ b/telegram/_choseninlineresult.py @@ -92,7 +92,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChosenInlineResult"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChosenInlineResult"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_files/_basethumbedmedium.py b/telegram/_files/_basethumbedmedium.py index 6212e38f69a..ba35ea2e53e 100644 --- a/telegram/_files/_basethumbedmedium.py +++ b/telegram/_files/_basethumbedmedium.py @@ -82,7 +82,7 @@ def __init__( @classmethod def de_json( - cls: Type[ThumbedMT_co], data: Optional[JSONDict], bot: "Bot" + cls: Type[ThumbedMT_co], data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional[ThumbedMT_co]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_files/sticker.py b/telegram/_files/sticker.py index b1194deeeea..3c3c1cd7e72 100644 --- a/telegram/_files/sticker.py +++ b/telegram/_files/sticker.py @@ -193,7 +193,7 @@ def __init__( """:const:`telegram.constants.StickerType.CUSTOM_EMOJI`""" @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Sticker"]: + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Sticker"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -305,7 +305,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["StickerSet"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["StickerSet"]: """See :meth:`telegram.TelegramObject.de_json`.""" if not data: return None diff --git a/telegram/_files/venue.py b/telegram/_files/venue.py index caf60355533..443bd009c17 100644 --- a/telegram/_files/venue.py +++ b/telegram/_files/venue.py @@ -103,7 +103,7 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Venue"]: + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Venue"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_games/game.py b/telegram/_games/game.py index 8eff71a0a61..93b3f0161cc 100644 --- a/telegram/_games/game.py +++ b/telegram/_games/game.py @@ -122,7 +122,7 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Game"]: + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Game"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_games/gamehighscore.py b/telegram/_games/gamehighscore.py index 991255fe1d5..40f93fadd49 100644 --- a/telegram/_games/gamehighscore.py +++ b/telegram/_games/gamehighscore.py @@ -61,7 +61,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["GameHighScore"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["GameHighScore"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_giveaway.py b/telegram/_giveaway.py index ed6d4a28895..0008dc9dd4c 100644 --- a/telegram/_giveaway.py +++ b/telegram/_giveaway.py @@ -123,7 +123,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Giveaway"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["Giveaway"]: """See :obj:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -257,7 +259,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["GiveawayWinners"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["GiveawayWinners"]: """See :obj:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -323,7 +327,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["GiveawayCompleted"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["GiveawayCompleted"]: """See :obj:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_inline/inlinekeyboardbutton.py b/telegram/_inline/inlinekeyboardbutton.py index d88f222cad7..1a3e1675664 100644 --- a/telegram/_inline/inlinekeyboardbutton.py +++ b/telegram/_inline/inlinekeyboardbutton.py @@ -284,7 +284,9 @@ def _set_id_attrs(self) -> None: ) @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineKeyboardButton"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InlineKeyboardButton"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_inline/inlinekeyboardmarkup.py b/telegram/_inline/inlinekeyboardmarkup.py index c806318246a..efb181e8aa5 100644 --- a/telegram/_inline/inlinekeyboardmarkup.py +++ b/telegram/_inline/inlinekeyboardmarkup.py @@ -90,7 +90,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineKeyboardMarkup"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InlineKeyboardMarkup"]: """See :meth:`telegram.TelegramObject.de_json`.""" if not data: return None diff --git a/telegram/_inline/inlinequery.py b/telegram/_inline/inlinequery.py index cc9044beb18..ba29a8646fe 100644 --- a/telegram/_inline/inlinequery.py +++ b/telegram/_inline/inlinequery.py @@ -125,7 +125,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineQuery"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InlineQuery"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_inline/inlinequeryresultsbutton.py b/telegram/_inline/inlinequeryresultsbutton.py index da850340cac..ae0b404e1f8 100644 --- a/telegram/_inline/inlinequeryresultsbutton.py +++ b/telegram/_inline/inlinequeryresultsbutton.py @@ -97,7 +97,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineQueryResultsButton"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InlineQueryResultsButton"]: """See :meth:`telegram.TelegramObject.de_json`.""" if not data: return None diff --git a/telegram/_inline/inputinvoicemessagecontent.py b/telegram/_inline/inputinvoicemessagecontent.py index 74ea97de297..be539bcb38c 100644 --- a/telegram/_inline/inputinvoicemessagecontent.py +++ b/telegram/_inline/inputinvoicemessagecontent.py @@ -255,7 +255,7 @@ def __init__( @classmethod def de_json( - cls, data: Optional[JSONDict], bot: "Bot" + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["InputInvoiceMessageContent"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_keyboardbutton.py b/telegram/_keyboardbutton.py index 0cb4cd82e65..ad08f2f98ad 100644 --- a/telegram/_keyboardbutton.py +++ b/telegram/_keyboardbutton.py @@ -168,7 +168,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["KeyboardButton"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["KeyboardButton"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_keyboardbuttonrequest.py b/telegram/_keyboardbuttonrequest.py index fa94433ebd9..07f2c3a99aa 100644 --- a/telegram/_keyboardbuttonrequest.py +++ b/telegram/_keyboardbuttonrequest.py @@ -264,7 +264,7 @@ def __init__( @classmethod def de_json( - cls, data: Optional[JSONDict], bot: "Bot" + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["KeyboardButtonRequestChat"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_menubutton.py b/telegram/_menubutton.py index 2044fb551fe..5856fc8d10e 100644 --- a/telegram/_menubutton.py +++ b/telegram/_menubutton.py @@ -69,13 +69,19 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["MenuButton"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MenuButton"]: """Converts JSON data to the appropriate :class:`MenuButton` object, i.e. takes care of selecting the correct subclass. Args: data (Dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with this object. + bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to + :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: NEXT.VERSION + :paramref:`bot` is now optional and defaults to :obj:`None` Returns: The Telegram object. @@ -161,7 +167,9 @@ def __init__(self, text: str, web_app: WebAppInfo, *, api_kwargs: Optional[JSOND self._id_attrs = (self.type, self.text, self.web_app) @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["MenuButtonWebApp"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MenuButtonWebApp"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_message.py b/telegram/_message.py index b0605cd094d..0a8884794f5 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -178,7 +178,10 @@ def is_accessible(self) -> bool: @classmethod def _de_json( - cls, data: Optional[JSONDict], bot: "Bot", api_kwargs: Optional[JSONDict] = None + cls, + data: Optional[JSONDict], + bot: Optional["Bot"] = None, + api_kwargs: Optional[JSONDict] = None, ) -> Optional["MaybeInaccessibleMessage"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -1205,7 +1208,7 @@ def link(self) -> Optional[str]: return None @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Message"]: + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Message"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_messageentity.py b/telegram/_messageentity.py index 2f7fb7d6179..bbea88d10ae 100644 --- a/telegram/_messageentity.py +++ b/telegram/_messageentity.py @@ -129,7 +129,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["MessageEntity"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MessageEntity"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_messageorigin.py b/telegram/_messageorigin.py index 3577e6091f8..534583adb8b 100644 --- a/telegram/_messageorigin.py +++ b/telegram/_messageorigin.py @@ -94,7 +94,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["MessageOrigin"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MessageOrigin"]: """Converts JSON data to the appropriate :class:`MessageOrigin` object, i.e. takes care of selecting the correct subclass. """ diff --git a/telegram/_messagereactionupdated.py b/telegram/_messagereactionupdated.py index 8366d1c083c..d4d4033a647 100644 --- a/telegram/_messagereactionupdated.py +++ b/telegram/_messagereactionupdated.py @@ -86,7 +86,7 @@ def __init__( @classmethod def de_json( - cls, data: Optional[JSONDict], bot: "Bot" + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["MessageReactionCountUpdated"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -186,7 +186,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["MessageReactionUpdated"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MessageReactionUpdated"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_passport/credentials.py b/telegram/_passport/credentials.py index 525dd473e17..514f7fffb6c 100644 --- a/telegram/_passport/credentials.py +++ b/telegram/_passport/credentials.py @@ -232,7 +232,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Credentials"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["Credentials"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -342,7 +344,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SecureData"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["SecureData"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -448,7 +452,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SecureValue"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["SecureValue"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_passport/encryptedpassportelement.py b/telegram/_passport/encryptedpassportelement.py index f0d869fe2e3..76eb8e51f54 100644 --- a/telegram/_passport/encryptedpassportelement.py +++ b/telegram/_passport/encryptedpassportelement.py @@ -193,7 +193,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["EncryptedPassportElement"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["EncryptedPassportElement"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -210,14 +212,21 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["EncryptedPas @classmethod def de_json_decrypted( - cls, data: Optional[JSONDict], bot: "Bot", credentials: "Credentials" + cls, data: Optional[JSONDict], bot: Optional["Bot"], credentials: "Credentials" ) -> Optional["EncryptedPassportElement"]: """Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account passport credentials. Args: data (Dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with this object. + bot (:class:`telegram.Bot` | :obj:`None`): The bot associated with these object. + May be :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: NEXT.VERSION + :paramref:`bot` is now optional and defaults to :obj:`None` + + .. deprecated:: NEXT.VERSION + This argument will be converted to an optional argument in future versions. credentials (:class:`telegram.FileCredentials`): The credentials Returns: diff --git a/telegram/_passport/passportdata.py b/telegram/_passport/passportdata.py index 0dae4ba68c8..32e3879bc4d 100644 --- a/telegram/_passport/passportdata.py +++ b/telegram/_passport/passportdata.py @@ -81,7 +81,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PassportData"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PassportData"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_passport/passportfile.py b/telegram/_passport/passportfile.py index 3c69e9eb570..3ae4a42e81f 100644 --- a/telegram/_passport/passportfile.py +++ b/telegram/_passport/passportfile.py @@ -118,14 +118,21 @@ def file_date(self) -> int: @classmethod def de_json_decrypted( - cls, data: Optional[JSONDict], bot: "Bot", credentials: "FileCredentials" + cls, data: Optional[JSONDict], bot: Optional["Bot"], credentials: "FileCredentials" ) -> Optional["PassportFile"]: """Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account passport credentials. Args: data (Dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with this object. + bot (:class:`telegram.Bot` | :obj:`None`): The bot associated with these object. + May be :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: NEXT.VERSION + :paramref:`bot` is now optional and defaults to :obj:`None` + + .. deprecated:: NEXT.VERSION + This argument will be converted to an optional argument in future versions. credentials (:class:`telegram.FileCredentials`): The credentials Returns: @@ -143,7 +150,10 @@ def de_json_decrypted( @classmethod def de_list_decrypted( - cls, data: Optional[List[JSONDict]], bot: "Bot", credentials: List["FileCredentials"] + cls, + data: Optional[List[JSONDict]], + bot: Optional["Bot"], + credentials: List["FileCredentials"], ) -> Tuple[Optional["PassportFile"], ...]: """Variant of :meth:`telegram.TelegramObject.de_list` that also takes into account passport credentials. @@ -155,7 +165,14 @@ def de_list_decrypted( Args: data (List[Dict[:obj:`str`, ...]]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with these objects. + bot (:class:`telegram.Bot` | :obj:`None`): The bot associated with these object. + May be :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: NEXT.VERSION + :paramref:`bot` is now optional and defaults to :obj:`None` + + .. deprecated:: NEXT.VERSION + This argument will be converted to an optional argument in future versions. credentials (:class:`telegram.FileCredentials`): The credentials Returns: diff --git a/telegram/_payment/orderinfo.py b/telegram/_payment/orderinfo.py index b3a41b54345..7d3ee84a37b 100644 --- a/telegram/_payment/orderinfo.py +++ b/telegram/_payment/orderinfo.py @@ -71,7 +71,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["OrderInfo"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["OrderInfo"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_payment/precheckoutquery.py b/telegram/_payment/precheckoutquery.py index fb57127fc11..1e7dca7bf33 100644 --- a/telegram/_payment/precheckoutquery.py +++ b/telegram/_payment/precheckoutquery.py @@ -110,7 +110,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PreCheckoutQuery"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PreCheckoutQuery"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_payment/shippingquery.py b/telegram/_payment/shippingquery.py index ab7e5a1b2f4..47a62192489 100644 --- a/telegram/_payment/shippingquery.py +++ b/telegram/_payment/shippingquery.py @@ -77,7 +77,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ShippingQuery"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ShippingQuery"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_payment/successfulpayment.py b/telegram/_payment/successfulpayment.py index a7424feba22..5298f66801e 100644 --- a/telegram/_payment/successfulpayment.py +++ b/telegram/_payment/successfulpayment.py @@ -105,7 +105,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SuccessfulPayment"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["SuccessfulPayment"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_poll.py b/telegram/_poll.py index cd6397cf733..01ec75ca5fa 100644 --- a/telegram/_poll.py +++ b/telegram/_poll.py @@ -90,7 +90,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InputPollOption"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InputPollOption"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -154,7 +156,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PollOption"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PollOption"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -301,7 +305,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PollAnswer"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PollAnswer"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -467,7 +473,7 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Poll"]: + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Poll"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_proximityalerttriggered.py b/telegram/_proximityalerttriggered.py index dd05c1ddd95..0880ca9a6f6 100644 --- a/telegram/_proximityalerttriggered.py +++ b/telegram/_proximityalerttriggered.py @@ -67,7 +67,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ProximityAlertTriggered"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ProximityAlertTriggered"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_reaction.py b/telegram/_reaction.py index 60bdbebc489..d1ba718f0d6 100644 --- a/telegram/_reaction.py +++ b/telegram/_reaction.py @@ -65,7 +65,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ReactionType"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ReactionType"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -192,7 +194,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ReactionCount"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ReactionCount"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_reply.py b/telegram/_reply.py index 973cee5ddfe..3ca342d067b 100644 --- a/telegram/_reply.py +++ b/telegram/_reply.py @@ -231,7 +231,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ExternalReplyInfo"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ExternalReplyInfo"]: """See :obj:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -329,7 +331,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["TextQuote"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["TextQuote"]: """See :obj:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -434,7 +438,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ReplyParameters"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ReplyParameters"]: """See :obj:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_shared.py b/telegram/_shared.py index 8c791154e2f..b4ce2c4d5a0 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -83,7 +83,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["UsersShared"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["UsersShared"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -172,7 +174,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatShared"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatShared"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -250,7 +254,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SharedUser"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["SharedUser"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_story.py b/telegram/_story.py index 4cb4a8454b6..40d17cdb16d 100644 --- a/telegram/_story.py +++ b/telegram/_story.py @@ -71,7 +71,7 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Story"]: + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Story"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index 2f61dace88e..04275b9a928 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -403,7 +403,7 @@ def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]: def _de_json( cls: Type[Tele_co], data: Optional[JSONDict], - bot: "Bot", + bot: Optional["Bot"], api_kwargs: Optional[JSONDict] = None, ) -> Optional[Tele_co]: if data is None: @@ -432,12 +432,18 @@ def _de_json( return obj @classmethod - def de_json(cls: Type[Tele_co], data: Optional[JSONDict], bot: "Bot") -> Optional[Tele_co]: + def de_json( + cls: Type[Tele_co], data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional[Tele_co]: """Converts JSON data to a Telegram object. Args: data (Dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with this object. + bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to + :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: NEXT.VERSION + :paramref:`bot` is now optional and defaults to :obj:`None` Returns: The Telegram object. @@ -447,7 +453,7 @@ def de_json(cls: Type[Tele_co], data: Optional[JSONDict], bot: "Bot") -> Optiona @classmethod def de_list( - cls: Type[Tele_co], data: Optional[List[JSONDict]], bot: "Bot" + cls: Type[Tele_co], data: Optional[List[JSONDict]], bot: Optional["Bot"] = None ) -> Tuple[Tele_co, ...]: """Converts a list of JSON objects to a tuple of Telegram objects. @@ -458,7 +464,11 @@ def de_list( Args: data (List[Dict[:obj:`str`, ...]]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with these objects. + bot (:class:`telegram.Bot`, optional): The bot associated with these object. Defaults + to :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: NEXT.VERSION + :paramref:`bot` is now optional and defaults to :obj:`None` Returns: A tuple of Telegram objects. diff --git a/telegram/_update.py b/telegram/_update.py index 68ff52649d2..579cb008580 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -729,7 +729,7 @@ def effective_message(self) -> Optional[Message]: return message @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Update"]: + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Update"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_userprofilephotos.py b/telegram/_userprofilephotos.py index 36e260da9f2..9a5e4a066ef 100644 --- a/telegram/_userprofilephotos.py +++ b/telegram/_userprofilephotos.py @@ -70,7 +70,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["UserProfilePhotos"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["UserProfilePhotos"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_utils/datetime.py b/telegram/_utils/datetime.py index 1c0da085434..40d931efffe 100644 --- a/telegram/_utils/datetime.py +++ b/telegram/_utils/datetime.py @@ -188,13 +188,16 @@ def from_timestamp( return dtm.datetime.fromtimestamp(unixtime, tz=UTC if tzinfo is None else tzinfo) -def extract_tzinfo_from_defaults(bot: "Bot") -> Union[dtm.tzinfo, None]: +def extract_tzinfo_from_defaults(bot: Optional["Bot"]) -> Union[dtm.tzinfo, None]: """ Extracts the timezone info from the default values of the bot. If the bot has no default values, :obj:`None` is returned. """ # We don't use `ininstance(bot, ExtBot)` here so that this works # without the job-queue extra dependencies as well + if bot is None: + return None + if hasattr(bot, "defaults") and bot.defaults: return bot.defaults.tzinfo return None diff --git a/telegram/_videochat.py b/telegram/_videochat.py index 3e5027c99fd..b392fa6d65b 100644 --- a/telegram/_videochat.py +++ b/telegram/_videochat.py @@ -125,7 +125,7 @@ def __init__( @classmethod def de_json( - cls, data: Optional[JSONDict], bot: "Bot" + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["VideoChatParticipantsInvited"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) @@ -177,7 +177,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["VideoChatScheduled"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["VideoChatScheduled"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/telegram/_webhookinfo.py b/telegram/_webhookinfo.py index c1b049b7109..a6f309a930d 100644 --- a/telegram/_webhookinfo.py +++ b/telegram/_webhookinfo.py @@ -162,7 +162,9 @@ def __init__( self._freeze() @classmethod - def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["WebhookInfo"]: + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["WebhookInfo"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 39f3aaff4aa..ca893dec4d8 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -90,6 +90,11 @@ def test_de_json_api_kwargs(self, bot): assert to.api_kwargs == {"foo": "bar"} assert to.get_bot() is bot + def test_de_json_optional_bot(self): + to = TelegramObject.de_json(data={}) + with pytest.raises(RuntimeError, match="no bot associated with it"): + to.get_bot() + def test_de_list(self, bot): class SubClass(TelegramObject): def __init__(self, arg: int, **kwargs): From df8aae0a38aac8b26a6189b80fb1f2314788b821 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:43:54 +0200 Subject: [PATCH 08/26] Fix Link-Check Workflow (#4332) --- .github/workflows/docs-linkcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-linkcheck.yml b/.github/workflows/docs-linkcheck.yml index 32f451c72b6..82fafe3e53f 100644 --- a/.github/workflows/docs-linkcheck.yml +++ b/.github/workflows/docs-linkcheck.yml @@ -22,6 +22,6 @@ jobs: - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip - python -W ignore -m pip install -r requirements-all.txt + 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 From 146ec54a00466bbe0fd16b16791a850a7a9ef594 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:45:37 -0400 Subject: [PATCH 09/26] API 7.5 (#4312, #4311, #4315, #4328, #4316) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- README.rst | 4 +- docs/source/inclusions/bot_methods.rst | 2 + docs/source/telegram.at-tree.rst | 10 + .../telegram.revenuewithdrawalstate.rst | 7 + .../telegram.revenuewithdrawalstatefailed.rst | 7 + ...telegram.revenuewithdrawalstatepending.rst | 7 + ...legram.revenuewithdrawalstatesucceeded.rst | 7 + docs/source/telegram.startransaction.rst | 7 + docs/source/telegram.startransactions.rst | 8 + docs/source/telegram.transactionpartner.rst | 7 + .../telegram.transactionpartnerfragment.rst | 7 + .../telegram.transactionpartnerother.rst | 7 + .../telegram.transactionpartneruser.rst | 7 + docs/substitutions/global.rst | 4 + telegram/__init__.py | 22 + telegram/_bot.py | 94 ++- telegram/_callbackquery.py | 12 + telegram/_chatfullinfo.py | 6 +- telegram/_inline/inlinekeyboardbutton.py | 6 +- telegram/_message.py | 63 +- telegram/_stars.py | 477 ++++++++++++++ telegram/constants.py | 58 +- telegram/ext/_extbot.py | 38 ++ tests/test_bot.py | 32 +- tests/test_callbackquery.py | 16 +- tests/test_message.py | 36 +- tests/test_official/exceptions.py | 4 + tests/test_stars.py | 580 ++++++++++++++++++ 28 files changed, 1489 insertions(+), 46 deletions(-) create mode 100644 docs/source/telegram.revenuewithdrawalstate.rst create mode 100644 docs/source/telegram.revenuewithdrawalstatefailed.rst create mode 100644 docs/source/telegram.revenuewithdrawalstatepending.rst create mode 100644 docs/source/telegram.revenuewithdrawalstatesucceeded.rst create mode 100644 docs/source/telegram.startransaction.rst create mode 100644 docs/source/telegram.startransactions.rst create mode 100644 docs/source/telegram.transactionpartner.rst create mode 100644 docs/source/telegram.transactionpartnerfragment.rst create mode 100644 docs/source/telegram.transactionpartnerother.rst create mode 100644 docs/source/telegram.transactionpartneruser.rst create mode 100644 telegram/_stars.py create mode 100644 tests/test_stars.py diff --git a/README.rst b/README.rst index 82e272c3f8b..1e3570e95be 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.4-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.5-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -79,7 +79,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -All types and methods of the Telegram Bot API **7.4** are supported. +All types and methods of the Telegram Bot API **7.5** are supported. Installing ========== diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index bece5296e22..f79f5bd959c 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -369,6 +369,8 @@ - Used for getting basic info about a file * - :meth:`~telegram.Bot.get_me` - Used for getting basic information about the bot + * - :meth:`~telegram.Bot.get_star_transactions` + - Used for obtaining the bot's Telegram Stars transactions * - :meth:`~telegram.Bot.refund_star_payment` - Used for refunding a payment in Telegram Stars diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index f9ac8dd6702..077b124aba4 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -125,12 +125,22 @@ Available Types telegram.replykeyboardmarkup telegram.replykeyboardremove telegram.replyparameters + telegram.revenuewithdrawalstate + telegram.revenuewithdrawalstatefailed + telegram.revenuewithdrawalstatepending + telegram.revenuewithdrawalstatesucceeded telegram.sentwebappmessage telegram.shareduser + telegram.startransaction + telegram.startransactions telegram.story telegram.switchinlinequerychosenchat telegram.telegramobject telegram.textquote + telegram.transactionpartner + telegram.transactionpartnerfragment + telegram.transactionpartnerother + telegram.transactionpartneruser telegram.update telegram.user telegram.userchatboosts diff --git a/docs/source/telegram.revenuewithdrawalstate.rst b/docs/source/telegram.revenuewithdrawalstate.rst new file mode 100644 index 00000000000..d3f7eef81cc --- /dev/null +++ b/docs/source/telegram.revenuewithdrawalstate.rst @@ -0,0 +1,7 @@ +RevenueWithdrawalState +====================== + +.. autoclass:: telegram.RevenueWithdrawalState + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/source/telegram.revenuewithdrawalstatefailed.rst b/docs/source/telegram.revenuewithdrawalstatefailed.rst new file mode 100644 index 00000000000..ac319c6a67a --- /dev/null +++ b/docs/source/telegram.revenuewithdrawalstatefailed.rst @@ -0,0 +1,7 @@ +RevenueWithdrawalStateFailed +============================= + +.. autoclass:: telegram.RevenueWithdrawalStateFailed + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/source/telegram.revenuewithdrawalstatepending.rst b/docs/source/telegram.revenuewithdrawalstatepending.rst new file mode 100644 index 00000000000..19a74e5f28c --- /dev/null +++ b/docs/source/telegram.revenuewithdrawalstatepending.rst @@ -0,0 +1,7 @@ +RevenueWithdrawalStatePending +============================= + +.. autoclass:: telegram.RevenueWithdrawalStatePending + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/source/telegram.revenuewithdrawalstatesucceeded.rst b/docs/source/telegram.revenuewithdrawalstatesucceeded.rst new file mode 100644 index 00000000000..7f7980e799f --- /dev/null +++ b/docs/source/telegram.revenuewithdrawalstatesucceeded.rst @@ -0,0 +1,7 @@ +RevenueWithdrawalStateSucceeded +=============================== + +.. autoclass:: telegram.RevenueWithdrawalStateSucceeded + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/source/telegram.startransaction.rst b/docs/source/telegram.startransaction.rst new file mode 100644 index 00000000000..42f84e39b67 --- /dev/null +++ b/docs/source/telegram.startransaction.rst @@ -0,0 +1,7 @@ +StarTransaction +=============== + +.. autoclass:: telegram.StarTransaction + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/source/telegram.startransactions.rst b/docs/source/telegram.startransactions.rst new file mode 100644 index 00000000000..1f1860920b5 --- /dev/null +++ b/docs/source/telegram.startransactions.rst @@ -0,0 +1,8 @@ +StarTransactions +================ + +.. autoclass:: telegram.StarTransactions + :members: + :show-inheritance: + :inherited-members: TelegramObject + diff --git a/docs/source/telegram.transactionpartner.rst b/docs/source/telegram.transactionpartner.rst new file mode 100644 index 00000000000..9ccca02cec0 --- /dev/null +++ b/docs/source/telegram.transactionpartner.rst @@ -0,0 +1,7 @@ +TransactionPartner +================== + +.. autoclass:: telegram.TransactionPartner + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/source/telegram.transactionpartnerfragment.rst b/docs/source/telegram.transactionpartnerfragment.rst new file mode 100644 index 00000000000..0845b4a800b --- /dev/null +++ b/docs/source/telegram.transactionpartnerfragment.rst @@ -0,0 +1,7 @@ +TransactionPartnerFragment +========================== + +.. autoclass:: telegram.TransactionPartnerFragment + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/source/telegram.transactionpartnerother.rst b/docs/source/telegram.transactionpartnerother.rst new file mode 100644 index 00000000000..c3ffddc7de0 --- /dev/null +++ b/docs/source/telegram.transactionpartnerother.rst @@ -0,0 +1,7 @@ +TransactionPartnerOther +======================= + +.. autoclass:: telegram.TransactionPartnerOther + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/source/telegram.transactionpartneruser.rst b/docs/source/telegram.transactionpartneruser.rst new file mode 100644 index 00000000000..d2e145e1866 --- /dev/null +++ b/docs/source/telegram.transactionpartneruser.rst @@ -0,0 +1,7 @@ +TransactionPartnerUser +====================== + +.. autoclass:: telegram.TransactionPartnerUser + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index 37edc74a446..c4e9e493bb3 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -16,6 +16,8 @@ .. |editreplymarkup| replace:: It is currently only possible to edit messages without :attr:`telegram.Message.reply_markup` or with inline keyboards. +.. |bcid_edit_time| replace:: Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within *48 hours* from the time they were sent. + .. |toapikwargsbase| replace:: These arguments are also considered by :meth:`~telegram.TelegramObject.to_dict` and :meth:`~telegram.TelegramObject.to_json`, i.e. when passing objects to Telegram. Passing them to Telegram is however not guaranteed to work for all kinds of objects, e.g. this will fail for objects that can not directly be JSON serialized. .. |toapikwargsarg| replace:: Arbitrary keyword arguments. Can be used to store data for which there are no dedicated attributes. |toapikwargsbase| @@ -82,6 +84,8 @@ .. |business_id_str| replace:: Unique identifier of the business connection on behalf of which the message will be sent. +.. |business_id_str_edit| replace:: Unique identifier of the business connection on behalf of which the message to be edited was sent + .. |message_effect_id| replace:: Unique identifier of the message effect to be added to the message; for private chats only. .. |show_cap_above_med| replace:: :obj:`True`, if the caption must be shown above the message media. diff --git a/telegram/__init__.py b/telegram/__init__.py index 675a60e9835..48ad57298c6 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -200,6 +200,10 @@ "ReplyKeyboardRemove", "ReplyParameters", "ResidentialAddress", + "RevenueWithdrawalState", + "RevenueWithdrawalStateFailed", + "RevenueWithdrawalStatePending", + "RevenueWithdrawalStateSucceeded", "SecureData", "SecureValue", "SentWebAppMessage", @@ -207,6 +211,8 @@ "ShippingAddress", "ShippingOption", "ShippingQuery", + "StarTransaction", + "StarTransactions", "Sticker", "StickerSet", "Story", @@ -214,6 +220,10 @@ "SwitchInlineQueryChosenChat", "TelegramObject", "TextQuote", + "TransactionPartner", + "TransactionPartnerFragment", + "TransactionPartnerOther", + "TransactionPartnerUser", "Update", "User", "UserChatBoosts", @@ -435,6 +445,18 @@ from ._replykeyboardremove import ReplyKeyboardRemove from ._sentwebappmessage import SentWebAppMessage from ._shared import ChatShared, SharedUser, UsersShared +from ._stars import ( + RevenueWithdrawalState, + RevenueWithdrawalStateFailed, + RevenueWithdrawalStatePending, + RevenueWithdrawalStateSucceeded, + StarTransaction, + StarTransactions, + TransactionPartner, + TransactionPartnerFragment, + TransactionPartnerOther, + TransactionPartnerUser, +) from ._story import Story from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject diff --git a/telegram/_bot.py b/telegram/_bot.py index ebc7817b9d2..85cf417911d 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -88,6 +88,7 @@ from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji from telegram._reply import ReplyParameters from telegram._sentwebappmessage import SentWebAppMessage +from telegram._stars import StarTransactions from telegram._telegramobject import TelegramObject from telegram._update import Update from telegram._user import User @@ -2802,6 +2803,7 @@ async def edit_message_live_location( heading: Optional[int] = None, proximity_alert_radius: Optional[int] = None, live_period: Optional[int] = None, + business_connection_id: Optional[str] = None, *, location: Optional[Location] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2849,6 +2851,9 @@ async def edit_message_live_location( remains unchanged .. versionadded:: 21.2. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: NEXT.VERSION Keyword Args: location (:class:`telegram.Location`, optional): The location to send. @@ -2888,6 +2893,7 @@ async def edit_message_live_location( "editMessageLiveLocation", data, reply_markup=reply_markup, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2901,6 +2907,7 @@ async def stop_message_live_location( message_id: Optional[int] = None, inline_message_id: Optional[str] = None, reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2920,6 +2927,9 @@ async def stop_message_live_location( :paramref:`message_id` are not specified. Identifier of the inline message. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: NEXT.VERSION Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the @@ -2935,6 +2945,7 @@ async def stop_message_live_location( "stopMessageLiveLocation", data, reply_markup=reply_markup, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -3945,6 +3956,7 @@ async def edit_message_text( reply_markup: Optional["InlineKeyboardMarkup"] = None, entities: Optional[Sequence["MessageEntity"]] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + business_connection_id: Optional[str] = None, *, disable_web_page_preview: Optional[bool] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3957,7 +3969,8 @@ async def edit_message_text( Use this method to edit text and game messages. Note: - |editreplymarkup|. + * |editreplymarkup|. + * |bcid_edit_time| .. seealso:: :attr:`telegram.Game.text` @@ -3988,6 +4001,9 @@ async def edit_message_text( reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: NEXT.VERSION Keyword Args: disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in @@ -4029,6 +4045,7 @@ async def edit_message_text( reply_markup=reply_markup, parse_mode=parse_mode, link_preview_options=link_preview_options, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -4046,6 +4063,7 @@ async def edit_message_caption( parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Optional[Sequence["MessageEntity"]] = None, show_caption_above_media: Optional[bool] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -4057,7 +4075,8 @@ async def edit_message_caption( Use this method to edit captions of messages. Note: - |editreplymarkup| + * |editreplymarkup| + * |bcid_edit_time| Args: chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not @@ -4080,6 +4099,9 @@ async def edit_message_caption( show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: NEXT.VERSION Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the @@ -4103,6 +4125,7 @@ async def edit_message_caption( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -4117,6 +4140,7 @@ async def edit_message_media( message_id: Optional[int] = None, inline_message_id: Optional[str] = None, reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -4132,7 +4156,8 @@ async def edit_message_media( :attr:`~telegram.File.file_id` or specify a URL. Note: - |editreplymarkup| + * |editreplymarkup| + * |bcid_edit_time| .. seealso:: :wiki:`Working with Files and Media ` @@ -4147,6 +4172,9 @@ async def edit_message_media( specified. Identifier of the inline message. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: NEXT.VERSION Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the @@ -4166,6 +4194,7 @@ async def edit_message_media( "editMessageMedia", data, reply_markup=reply_markup, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -4179,6 +4208,7 @@ async def edit_message_reply_markup( message_id: Optional[int] = None, inline_message_id: Optional[str] = None, reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -4191,7 +4221,8 @@ async def edit_message_reply_markup( (for inline bots). Note: - |editreplymarkup| + * |editreplymarkup| + * |bcid_edit_time| Args: chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not @@ -4202,6 +4233,9 @@ async def edit_message_reply_markup( specified. Identifier of the inline message. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: NEXT.VERSION Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the @@ -4221,6 +4255,7 @@ async def edit_message_reply_markup( "editMessageReplyMarkup", data, reply_markup=reply_markup, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -7119,6 +7154,7 @@ async def stop_poll( chat_id: Union[int, str], message_id: int, reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -7134,6 +7170,9 @@ async def stop_poll( message_id (:obj:`int`): Identifier of the original message with the poll. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new message inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: NEXT.VERSION Returns: :class:`telegram.Poll`: On success, the stopped Poll is returned. @@ -7146,6 +7185,7 @@ async def stop_poll( "chat_id": chat_id, "message_id": message_id, "reply_markup": reply_markup, + "business_connection_id": business_connection_id, } result = await self._post( @@ -9070,6 +9110,50 @@ async def refund_star_payment( api_kwargs=api_kwargs, ) + async def get_star_transactions( + self, + offset: Optional[int] = None, + limit: Optional[int] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> StarTransactions: + """Returns the bot's Telegram Star transactions in chronological order. + + .. versionadded:: NEXT.VERSION + + Args: + offset (:obj:`int`, optional): Number of transactions to skip in the response. + limit (:obj:`int`, optional): The maximum number of transactions to be retrieved. + Values between :tg-const:`telegram.constants.StarTransactionsLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.StarTransactionsLimit.MAX_LIMIT` are accepted. + Defaults to :tg-const:`telegram.constants.StarTransactionsLimit.MAX_LIMIT`. + + Returns: + :class:`telegram.StarTransactions`: On success. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = {"offset": offset, "limit": limit} + + return StarTransactions.de_json( # type: ignore[return-value] + await self._post( + "getStarTransactions", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -9322,3 +9406,5 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`replace_sticker_in_set`""" refundStarPayment = refund_star_payment """Alias for :meth:`refund_star_payment`""" + getStarTransactions = get_star_transactions + """Alias for :meth:`get_star_transactions`""" diff --git a/telegram/_callbackquery.py b/telegram/_callbackquery.py index d1cdc236be2..94661a10fad 100644 --- a/telegram/_callbackquery.py +++ b/telegram/_callbackquery.py @@ -262,6 +262,8 @@ async def edit_message_text( entities=entities, chat_id=None, message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, ) return await self._get_message().edit_text( text=text, @@ -330,6 +332,8 @@ async def edit_message_caption( chat_id=None, message_id=None, show_caption_above_media=show_caption_above_media, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, ) return await self._get_message().edit_caption( caption=caption, @@ -390,6 +394,8 @@ async def edit_message_reply_markup( api_kwargs=api_kwargs, chat_id=None, message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, ) return await self._get_message().edit_reply_markup( reply_markup=reply_markup, @@ -447,6 +453,8 @@ async def edit_message_media( api_kwargs=api_kwargs, chat_id=None, message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, ) return await self._get_message().edit_media( media=media, @@ -518,6 +526,8 @@ async def edit_message_live_location( live_period=live_period, chat_id=None, message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, ) return await self._get_message().edit_live_location( latitude=latitude, @@ -581,6 +591,8 @@ async def stop_message_live_location( api_kwargs=api_kwargs, chat_id=None, message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, ) return await self._get_message().stop_live_location( reply_markup=reply_markup, diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index 349f61318de..fbdc9d6842f 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -121,7 +121,8 @@ class ChatFullInfo(_ChatBase): .. versionadded:: 20.0 emoji_status_expiration_date (:class:`datetime.datetime`, optional): Expiration date of - emoji status of the chat or the other party in a private chat, in seconds. + emoji status of the chat or the other party in a private chat, as a datetime object, + if any. |datetime_localization| @@ -270,7 +271,8 @@ class ChatFullInfo(_ChatBase): .. versionadded:: 20.0 emoji_status_expiration_date (:class:`datetime.datetime`): Optional. Expiration date of - emoji status of the chat or the other party in a private chat, in seconds. + emoji status of the chat or the other party in a private chat, as a datetime object, + if any. |datetime_localization| diff --git a/telegram/_inline/inlinekeyboardbutton.py b/telegram/_inline/inlinekeyboardbutton.py index 1a3e1675664..bbd53ec06a9 100644 --- a/telegram/_inline/inlinekeyboardbutton.py +++ b/telegram/_inline/inlinekeyboardbutton.py @@ -89,10 +89,9 @@ class InlineKeyboardButton(TelegramObject): Caution: Only ``HTTPS`` links are allowed after Bot API 6.1. callback_data (:obj:`str` | :obj:`object`, optional): Data to be sent in a callback query - to the bot when button is pressed, UTF-8 + to the bot when the button is pressed, UTF-8 :tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`- :tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes. - Not supported for messages sent on behalf of a Telegram Business account. If the bot instance allows arbitrary callback data, anything can be passed. Tip: @@ -165,10 +164,9 @@ class InlineKeyboardButton(TelegramObject): Caution: Only ``HTTPS`` links are allowed after Bot API 6.1. callback_data (:obj:`str` | :obj:`object`): Optional. Data to be sent in a callback query - to the bot when button is pressed, UTF-8 + to the bot when the button is pressed, UTF-8 :tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`- :tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes. - Not supported for messages sent on behalf of a Telegram Business account. web_app (:obj:`telegram.WebAppInfo`): Optional. Description of the `Web App `_ that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user diff --git a/telegram/_message.py b/telegram/_message.py index 0a8884794f5..f5626279a93 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -3581,7 +3581,10 @@ async def edit_text( """Shortcut for:: await bot.edit_message_text( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see :meth:`telegram.Bot.edit_message_text`. @@ -3591,6 +3594,9 @@ async def edit_text( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id`. + Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise ``True`` is returned. @@ -3611,6 +3617,7 @@ async def edit_text( api_kwargs=api_kwargs, entities=entities, inline_message_id=None, + business_connection_id=self.business_connection_id, ) async def edit_caption( @@ -3630,7 +3637,10 @@ async def edit_caption( """Shortcut for:: await bot.edit_message_caption( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see @@ -3641,6 +3651,9 @@ async def edit_caption( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id`. + Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise ``True`` is returned. @@ -3660,6 +3673,7 @@ async def edit_caption( caption_entities=caption_entities, inline_message_id=None, show_caption_above_media=show_caption_above_media, + business_connection_id=self.business_connection_id, ) async def edit_media( @@ -3676,7 +3690,10 @@ async def edit_media( """Shortcut for:: await bot.edit_message_media( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see @@ -3687,6 +3704,9 @@ async def edit_media( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id`. + Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the edited Message is returned, otherwise ``True`` is returned. @@ -3703,6 +3723,7 @@ async def edit_media( pool_timeout=pool_timeout, api_kwargs=api_kwargs, inline_message_id=None, + business_connection_id=self.business_connection_id, ) async def edit_reply_markup( @@ -3718,7 +3739,10 @@ async def edit_reply_markup( """Shortcut for:: await bot.edit_message_reply_markup( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see @@ -3729,6 +3753,9 @@ async def edit_reply_markup( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id`. + Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise ``True`` is returned. @@ -3743,6 +3770,7 @@ async def edit_reply_markup( pool_timeout=pool_timeout, api_kwargs=api_kwargs, inline_message_id=None, + business_connection_id=self.business_connection_id, ) async def edit_live_location( @@ -3765,7 +3793,10 @@ async def edit_live_location( """Shortcut for:: await bot.edit_message_live_location( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see @@ -3776,6 +3807,9 @@ async def edit_live_location( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id`. + Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. @@ -3797,6 +3831,7 @@ async def edit_live_location( proximity_alert_radius=proximity_alert_radius, live_period=live_period, inline_message_id=None, + business_connection_id=self.business_connection_id, ) async def stop_live_location( @@ -3812,7 +3847,10 @@ async def stop_live_location( """Shortcut for:: await bot.stop_message_live_location( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see @@ -3823,6 +3861,9 @@ async def stop_live_location( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id`. + Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. @@ -3837,6 +3878,7 @@ async def stop_live_location( pool_timeout=pool_timeout, api_kwargs=api_kwargs, inline_message_id=None, + business_connection_id=self.business_connection_id, ) async def set_game_score( @@ -3967,11 +4009,17 @@ async def stop_poll( """Shortcut for:: await bot.stop_poll( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see :meth:`telegram.Bot.stop_poll`. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id`. + Returns: :class:`telegram.Poll`: On success, the stopped Poll with the final results is returned. @@ -3986,6 +4034,7 @@ async def stop_poll( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, ) async def pin( diff --git a/telegram/_stars.py b/telegram/_stars.py new file mode 100644 index 00000000000..8cb6ac1311f --- /dev/null +++ b/telegram/_stars.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# 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/]. +# pylint: disable=redefined-builtin +"""This module contains the classes for Telegram Stars transactions.""" + +from datetime import datetime +from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils import enum +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class RevenueWithdrawalState(TelegramObject): + """This object escribes the state of a revenue withdrawal operation. Currently, it can be one + of: + + * :class:`telegram.RevenueWithdrawalStatePending` + * :class:`telegram.RevenueWithdrawalStateSucceeded` + * :class:`telegram.RevenueWithdrawalStateFailed` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): The type of the state. + + Attributes: + type (:obj:`str`): The type of the state. + """ + + __slots__ = ("type",) + + PENDING: Final[str] = constants.RevenueWithdrawalStateType.PENDING + """:const:`telegram.constants.RevenueWithdrawalStateType.PENDING`""" + SUCCEEDED: Final[str] = constants.RevenueWithdrawalStateType.SUCCEEDED + """:const:`telegram.constants.RevenueWithdrawalStateType.SUCCEEDED`""" + FAILED: Final[str] = constants.RevenueWithdrawalStateType.FAILED + """:const:`telegram.constants.RevenueWithdrawalStateType.FAILED`""" + + def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.RevenueWithdrawalStateType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["RevenueWithdrawalState"]: + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type[RevenueWithdrawalState]] = { + cls.PENDING: RevenueWithdrawalStatePending, + cls.SUCCEEDED: RevenueWithdrawalStateSucceeded, + cls.FAILED: RevenueWithdrawalStateFailed, + } + + if cls is RevenueWithdrawalState and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) + + return super().de_json(data=data, bot=bot) + + +class RevenueWithdrawalStatePending(RevenueWithdrawalState): + """The withdrawal is in progress. + + .. versionadded:: NEXT.VERSION + + Attributes: + type (:obj:`str`): The type of the state, always + :tg-const:`telegram.RevenueWithdrawalState.PENDING`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(type=RevenueWithdrawalState.PENDING, api_kwargs=api_kwargs) + self._freeze() + + +class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState): + """The withdrawal succeeded. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`date` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object. + url (:obj:`str`): An HTTPS URL that can be used to see transaction details. + + Attributes: + type (:obj:`str`): The type of the state, always + :tg-const:`telegram.RevenueWithdrawalState.SUCCEEDED`. + date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object. + url (:obj:`str`): An HTTPS URL that can be used to see transaction details. + """ + + __slots__ = ("date", "url") + + def __init__( + self, + date: datetime, + url: str, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=RevenueWithdrawalState.SUCCEEDED, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.date: datetime = date + self.url: str = url + self._id_attrs = ( + self.type, + self.date, + ) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["RevenueWithdrawalStateSucceeded"]: + data = cls._parse_data(data) + + if not data: + return None + + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo) + + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class RevenueWithdrawalStateFailed(RevenueWithdrawalState): + """The withdrawal failed and the transaction was refunded. + + .. versionadded:: NEXT.VERSION + + Attributes: + type (:obj:`str`): The type of the state, always + :tg-const:`telegram.RevenueWithdrawalState.FAILED`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(type=RevenueWithdrawalState.FAILED, api_kwargs=api_kwargs) + self._freeze() + + +class TransactionPartner(TelegramObject): + """This object describes the source of a transaction, or its recipient for outgoing + transactions. Currently, it can be one of: + + * :class:`TransactionPartnerFragment` + * :class:`TransactionPartnerUser` + * :class:`TransactionPartnerOther` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): The type of the transaction partner. + + Attributes: + type (:obj:`str`): The type of the transaction partner. + """ + + __slots__ = ("type",) + + FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT + """:const:`telegram.constants.TransactionPartnerType.FRAGMENT`""" + USER: Final[str] = constants.TransactionPartnerType.USER + """:const:`telegram.constants.TransactionPartnerType.USER`""" + OTHER: Final[str] = constants.TransactionPartnerType.OTHER + """:const:`telegram.constants.TransactionPartnerType.OTHER`""" + + def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.TransactionPartnerType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["TransactionPartner"]: + """Converts JSON data to the appropriate :class:`TransactionPartner` object, i.e. takes + care of selecting the correct subclass. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with this object. + + Returns: + The Telegram object. + + """ + data = cls._parse_data(data) + + if data is None: + return None + + if not data and cls is TransactionPartner: + return None + + _class_mapping: Dict[str, Type[TransactionPartner]] = { + cls.FRAGMENT: TransactionPartnerFragment, + cls.USER: TransactionPartnerUser, + cls.OTHER: TransactionPartnerOther, + } + + if cls is TransactionPartner and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) + + return super().de_json(data=data, bot=bot) + + +class TransactionPartnerFragment(TransactionPartner): + """Describes a withdrawal transaction with Fragment. + + .. versionadded:: NEXT.VERSION + + Args: + withdrawal_state (:obj:`telegram.RevenueWithdrawalState`, optional): State of the + transaction if the transaction is outgoing. + + Attributes: + type (:obj:`str`): The type of the transaction partner, + always :tg-const:`telegram.TransactionPartner.FRAGMENT`. + withdrawal_state (:obj:`telegram.RevenueWithdrawalState`): Optional. State of the + transaction if the transaction is outgoing. + """ + + __slots__ = ("withdrawal_state",) + + def __init__( + self, + withdrawal_state: Optional["RevenueWithdrawalState"] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=TransactionPartner.FRAGMENT, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.withdrawal_state: Optional[RevenueWithdrawalState] = withdrawal_state + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["TransactionPartnerFragment"]: + data = cls._parse_data(data) + + if not data: + return None + + data["withdrawal_state"] = RevenueWithdrawalState.de_json( + data.get("withdrawal_state"), bot + ) + + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class TransactionPartnerUser(TransactionPartner): + """Describes a transaction with a user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`user` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + user (:class:`telegram.User`): Information about the user. + + Attributes: + type (:obj:`str`): The type of the transaction partner, + always :tg-const:`telegram.TransactionPartner.USER`. + user (:class:`telegram.User`): Information about the user. + """ + + __slots__ = ("user",) + + def __init__(self, user: "User", *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.user: User = user + self._id_attrs = ( + self.type, + self.user, + ) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["TransactionPartnerUser"]: + data = cls._parse_data(data) + + if not data: + return None + + data["user"] = User.de_json(data.get("user"), bot) + + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class TransactionPartnerOther(TransactionPartner): + """Describes a transaction with an unknown partner. + + .. versionadded:: NEXT.VERSION + + Attributes: + type (:obj:`str`): The type of the transaction partner, + always :tg-const:`telegram.TransactionPartner.OTHER`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(type=TransactionPartner.OTHER, api_kwargs=api_kwargs) + self._freeze() + + +class StarTransaction(TelegramObject): + """Describes a Telegram Star transaction. + + 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. + + .. versionadded:: NEXT.VERSION + + Args: + id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer + of the original transaction for refund transactions. + Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for + successful incoming payments from users. + amount (:obj:`int`): Number of Telegram Stars transferred by the transaction. + date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object. + source (:class:`telegram.TransactionPartner`, optional): Source of an incoming transaction + (e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal). + Only for incoming transactions. + receiver (:class:`telegram.TransactionPartner`, optional): Receiver of an outgoing + transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for + outgoing transactions. + + Attributes: + id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer + of the original transaction for refund transactions. + Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for + successful incoming payments from users. + amount (:obj:`int`): Number of Telegram Stars transferred by the transaction. + date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object. + source (:class:`telegram.TransactionPartner`): Optional. Source of an incoming transaction + (e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal). + Only for incoming transactions. + receiver (:class:`telegram.TransactionPartner`): Optional. Receiver of an outgoing + transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for + outgoing transactions. + """ + + __slots__ = ("amount", "date", "id", "receiver", "source") + + def __init__( + self, + id: str, + amount: int, + date: datetime, + source: Optional[TransactionPartner] = None, + receiver: Optional[TransactionPartner] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.id: str = id + self.amount: int = amount + self.date: datetime = date + self.source: Optional[TransactionPartner] = source + self.receiver: Optional[TransactionPartner] = receiver + + self._id_attrs = ( + self.id, + self.source, + self.receiver, + ) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["StarTransaction"]: + data = cls._parse_data(data) + + if not data: + return None + + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo) + + data["source"] = TransactionPartner.de_json(data.get("source"), bot) + data["receiver"] = TransactionPartner.de_json(data.get("receiver"), bot) + + return super().de_json(data=data, bot=bot) + + +class StarTransactions(TelegramObject): + """ + Contains a list of Telegram Star transactions. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`transactions` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + transactions (Sequence[:class:`telegram.StarTransaction`]): The list of transactions. + + Attributes: + transactions (Tuple[:class:`telegram.StarTransaction`]): The list of transactions. + """ + + __slots__ = ("transactions",) + + def __init__( + self, transactions: Sequence[StarTransaction], *, api_kwargs: Optional[JSONDict] = None + ): + super().__init__(api_kwargs=api_kwargs) + self.transactions: Tuple[StarTransaction, ...] = parse_sequence_arg(transactions) + + self._id_attrs = (self.transactions,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["StarTransactions"]: + data = cls._parse_data(data) + + if data is None: + return None + + data["transactions"] = StarTransaction.de_list(data.get("transactions"), bot) + return super().de_json(data=data, bot=bot) diff --git a/telegram/constants.py b/telegram/constants.py index 5e2c853baa7..5cd6e7ffc2c 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -90,10 +90,13 @@ "ReactionEmoji", "ReactionType", "ReplyLimit", + "RevenueWithdrawalStateType", + "StarTransactionsLimit", "StickerFormat", "StickerLimit", "StickerSetLimit", "StickerType", + "TransactionPartnerType", "UpdateType", "UserProfilePhotosLimit", "WebhookLimit", @@ -146,7 +149,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=4) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=5) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -2303,6 +2306,42 @@ class ReplyLimit(IntEnum): """ +class RevenueWithdrawalStateType(StringEnum): + """This enum contains the available types of :class:`telegram.RevenueWithdrawalState`. + The enum members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + PENDING = "pending" + """:obj:`str`: A withdrawal in progress.""" + SUCCEEDED = "succeeded" + """:obj:`str`: A withdrawal succeeded.""" + FAILED = "failed" + """:obj:`str`: A withdrawal failed and the transaction was refunded.""" + + +class StarTransactionsLimit(IntEnum): + """This enum contains limitations for :class:`telegram.Bot.get_star_transactions`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MIN_LIMIT = 1 + """:obj:`int`: Minimum value allowed for the + :paramref:`~telegram.Bot.get_star_transactions.limit` parameter of + :meth:`telegram.Bot.get_star_transactions`.""" + MAX_LIMIT = 100 + """:obj:`int`: Maximum value allowed for the + :paramref:`~telegram.Bot.get_star_transactions.limit` parameter of + :meth:`telegram.Bot.get_star_transactions`.""" + + class StickerFormat(StringEnum): """This enum contains the available formats of :class:`telegram.Sticker` in the set. The enum members of this enumeration are instances of :class:`str` and can be treated as such. @@ -2436,6 +2475,23 @@ class StickerType(StringEnum): """:obj:`str`: Custom emoji sticker.""" +class TransactionPartnerType(StringEnum): + """This enum contains the available types of :class:`telegram.TransactionPartner`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + FRAGMENT = "fragment" + """:obj:`str`: Withdrawal transaction with Fragment.""" + USER = "user" + """:obj:`str`: Transaction with a user.""" + OTHER = "other" + """:obj:`str`: Transaction with unknown source or recipient.""" + + class ParseMode(StringEnum): """This enum contains the available parse modes. The enum members of this enumeration are instances of :class:`str` and can be treated as such. diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 917e9d8ef97..3cd4ab389e7 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -76,6 +76,7 @@ ReactionType, ReplyParameters, SentWebAppMessage, + StarTransactions, Sticker, StickerSet, TelegramObject, @@ -767,6 +768,7 @@ async def stop_poll( chat_id: Union[int, str], message_id: int, reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -780,6 +782,7 @@ async def stop_poll( chat_id=chat_id, message_id=message_id, reply_markup=self._replace_keyboard(reply_markup), + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1511,6 +1514,7 @@ async def edit_message_caption( parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Optional[Sequence["MessageEntity"]] = None, show_caption_above_media: Optional[bool] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1527,6 +1531,7 @@ async def edit_message_caption( reply_markup=reply_markup, parse_mode=parse_mode, caption_entities=caption_entities, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1547,6 +1552,7 @@ async def edit_message_live_location( heading: Optional[int] = None, proximity_alert_radius: Optional[int] = None, live_period: Optional[int] = None, + business_connection_id: Optional[str] = None, *, location: Optional[Location] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1568,6 +1574,7 @@ async def edit_message_live_location( proximity_alert_radius=proximity_alert_radius, live_period=live_period, location=location, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1582,6 +1589,7 @@ async def edit_message_media( message_id: Optional[int] = None, inline_message_id: Optional[str] = None, reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1596,6 +1604,7 @@ async def edit_message_media( message_id=message_id, inline_message_id=inline_message_id, reply_markup=reply_markup, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1609,6 +1618,7 @@ async def edit_message_reply_markup( message_id: Optional[int] = None, inline_message_id: Optional[str] = None, reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1622,6 +1632,7 @@ async def edit_message_reply_markup( message_id=message_id, inline_message_id=inline_message_id, reply_markup=reply_markup, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1639,6 +1650,7 @@ async def edit_message_text( reply_markup: Optional["InlineKeyboardMarkup"] = None, entities: Optional[Sequence["MessageEntity"]] = None, link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + business_connection_id: Optional[str] = None, *, disable_web_page_preview: Optional[bool] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1657,6 +1669,7 @@ async def edit_message_text( disable_web_page_preview=disable_web_page_preview, reply_markup=reply_markup, entities=entities, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -3633,6 +3646,7 @@ async def stop_message_live_location( message_id: Optional[int] = None, inline_message_id: Optional[str] = None, reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3646,6 +3660,7 @@ async def stop_message_live_location( message_id=message_id, inline_message_id=inline_message_id, reply_markup=reply_markup, + business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -4179,6 +4194,28 @@ async def refund_star_payment( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_star_transactions( + self, + offset: Optional[int] = None, + limit: Optional[int] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> StarTransactions: + return await super().get_star_transactions( + offset=offset, + limit=limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message @@ -4301,3 +4338,4 @@ async def refund_star_payment( getBusinessConnection = get_business_connection replaceStickerInSet = replace_sticker_in_set refundStarPayment = refund_star_payment + getStarTransactions = get_star_transactions diff --git a/tests/test_bot.py b/tests/test_bot.py index f3902f33a8d..05d655450f3 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -73,6 +73,8 @@ ReplyParameters, SentWebAppMessage, ShippingOption, + StarTransaction, + StarTransactions, Update, User, WebAppInfo, @@ -2171,14 +2173,17 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): async def test_business_connection_id_argument(self, bot, monkeypatch): """We can't connect to a business acc, so we just test that the correct data is passed. - We also can't test every single method easily, so we just test one. Our linting will catch - any unused args with the others.""" + We also can't test every single method easily, so we just test a few. Our linting will + catch any unused args with the others.""" async def make_assertion(url, request_data: RequestData, *args, **kwargs): - return request_data.parameters.get("business_connection_id") == 42 + assert request_data.parameters.get("business_connection_id") == 42 + return {} monkeypatch.setattr(bot.request, "post", make_assertion) - assert await bot.send_message(2, "text", business_connection_id=42) + + await bot.send_message(2, "text", business_connection_id=42) + await bot.stop_poll(chat_id=1, message_id=2, business_connection_id=42) async def test_message_effect_id_argument(self, bot, monkeypatch): """We can't test every single method easily, so we just test one. Our linting will catch @@ -2221,6 +2226,20 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.refund_star_payment(42, "37") + async def test_get_star_transactions(self, bot, monkeypatch): + # we just want to test the offset parameter + st = StarTransactions([StarTransaction("1", 1, dtm.datetime.now())]).to_json() + + async def do_request(url, request_data: RequestData, *args, **kwargs): + offset = request_data.parameters.get("offset") == 3 + if offset: + return 200, f'{{"ok": true, "result": {st}}}'.encode() + return 400, b'{"ok": false, "result": []}' + + monkeypatch.setattr(bot.request, "do_request", do_request) + obj = await bot.get_star_transactions(offset=3) + assert isinstance(obj, StarTransactions) + class TestBotWithRequest: """ @@ -4208,3 +4227,8 @@ async def test_do_api_request_list_return_type(self, bot, chat_id, return_type): @pytest.mark.parametrize("return_type", [Message, None]) async def test_do_api_request_bool_return_type(self, bot, chat_id, return_type): assert await bot.do_api_request("delete_my_commands", return_type=return_type) is True + + async def test_get_star_transactions(self, bot): + transactions = await bot.get_star_transactions(limit=1) + assert isinstance(transactions, StarTransactions) + assert len(transactions.transactions) == 0 diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 66dc6856924..5e41b5993cf 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -68,8 +68,8 @@ class TestCallbackQueryWithoutRequest(TestCallbackQueryBase): @staticmethod def skip_params(callback_query: CallbackQuery): if callback_query.inline_message_id: - return {"message_id", "chat_id"} - return {"inline_message_id"} + return {"message_id", "chat_id", "business_connection_id"} + return {"inline_message_id", "business_connection_id"} @staticmethod def shortcut_kwargs(callback_query: CallbackQuery): @@ -178,7 +178,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( CallbackQuery.edit_message_text, Bot.edit_message_text, - ["inline_message_id", "message_id", "chat_id"], + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -210,7 +210,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( CallbackQuery.edit_message_caption, Bot.edit_message_caption, - ["inline_message_id", "message_id", "chat_id"], + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -242,7 +242,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( CallbackQuery.edit_message_reply_markup, Bot.edit_message_reply_markup, - ["inline_message_id", "message_id", "chat_id"], + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -274,7 +274,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( CallbackQuery.edit_message_media, Bot.edit_message_media, - ["inline_message_id", "message_id", "chat_id"], + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -308,7 +308,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( CallbackQuery.edit_message_live_location, Bot.edit_message_live_location, - ["inline_message_id", "message_id", "chat_id"], + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -340,7 +340,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( CallbackQuery.stop_message_live_location, Bot.stop_message_live_location, - ["inline_message_id", "message_id", "chat_id"], + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], [], ) assert await check_shortcut_call( diff --git a/tests/test_message.py b/tests/test_message.py index 075d7089d3a..8bfc632769d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -2347,7 +2347,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.edit_text, Bot.edit_message_text, - ["chat_id", "message_id", "inline_message_id"], + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -2355,7 +2355,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "edit_message_text", skip_params=["inline_message_id"], - shortcut_kwargs=["message_id", "chat_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], ) assert await check_defaults_handling(message.edit_text, message.get_bot()) @@ -2372,7 +2372,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.edit_caption, Bot.edit_message_caption, - ["chat_id", "message_id", "inline_message_id"], + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -2380,7 +2380,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "edit_message_caption", skip_params=["inline_message_id"], - shortcut_kwargs=["message_id", "chat_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], ) assert await check_defaults_handling(message.edit_caption, message.get_bot()) @@ -2397,7 +2397,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.edit_media, Bot.edit_message_media, - ["chat_id", "message_id", "inline_message_id"], + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -2405,7 +2405,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "edit_message_media", skip_params=["inline_message_id"], - shortcut_kwargs=["message_id", "chat_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], ) assert await check_defaults_handling(message.edit_media, message.get_bot()) @@ -2422,7 +2422,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.edit_reply_markup, Bot.edit_message_reply_markup, - ["chat_id", "message_id", "inline_message_id"], + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -2430,7 +2430,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "edit_message_reply_markup", skip_params=["inline_message_id"], - shortcut_kwargs=["message_id", "chat_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], ) assert await check_defaults_handling(message.edit_reply_markup, message.get_bot()) @@ -2449,7 +2449,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.edit_live_location, Bot.edit_message_live_location, - ["chat_id", "message_id", "inline_message_id"], + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -2457,7 +2457,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "edit_message_live_location", skip_params=["inline_message_id"], - shortcut_kwargs=["message_id", "chat_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], ) assert await check_defaults_handling(message.edit_live_location, message.get_bot()) @@ -2473,7 +2473,7 @@ async def make_assertion(*_, **kwargs): assert check_shortcut_signature( Message.stop_live_location, Bot.stop_message_live_location, - ["chat_id", "message_id", "inline_message_id"], + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], [], ) assert await check_shortcut_call( @@ -2481,7 +2481,7 @@ async def make_assertion(*_, **kwargs): message.get_bot(), "stop_message_live_location", skip_params=["inline_message_id"], - shortcut_kwargs=["message_id", "chat_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], ) assert await check_defaults_handling(message.stop_live_location, message.get_bot()) @@ -2561,9 +2561,17 @@ async def make_assertion(*_, **kwargs): return chat_id and message_id assert check_shortcut_signature( - Message.stop_poll, Bot.stop_poll, ["chat_id", "message_id"], [] + Message.stop_poll, + Bot.stop_poll, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.stop_poll, + message.get_bot(), + "stop_poll", + shortcut_kwargs=["business_connection_id"], ) - assert await check_shortcut_call(message.stop_poll, message.get_bot(), "stop_poll") assert await check_defaults_handling(message.stop_poll, message.get_bot()) monkeypatch.setattr(message.get_bot(), "stop_poll", make_assertion) diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 91f186ff738..99df02b82e7 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -125,6 +125,8 @@ class ParamTypeCheckingExceptions: "BackgroundType": {"type"}, # attributes common to all subclasses "BackgroundFill": {"type"}, # attributes common to all subclasses "InputTextMessageContent": {"disable_web_page_preview"}, # convenience arg, here for bw compat + "RevenueWithdrawalState": {"type"}, # attributes common to all subclasses + "TransactionPartner": {"type"}, # attributes common to all subclasses } @@ -149,6 +151,8 @@ def ptb_extra_params(object_name: str) -> set[str]: r"ReactionType\w+": {"type"}, r"BackgroundType\w+": {"type"}, r"BackgroundFill\w+": {"type"}, + r"RevenueWithdrawalState\w+": {"type"}, + r"TransactionPartner\w+": {"type"}, } diff --git a/tests/test_stars.py b/tests/test_stars.py new file mode 100644 index 00000000000..74f367cb0d2 --- /dev/null +++ b/tests/test_stars.py @@ -0,0 +1,580 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import datetime +from copy import deepcopy + +import pytest + +from telegram import ( + Dice, + RevenueWithdrawalState, + RevenueWithdrawalStateFailed, + RevenueWithdrawalStatePending, + RevenueWithdrawalStateSucceeded, + StarTransaction, + StarTransactions, + TransactionPartner, + TransactionPartnerFragment, + TransactionPartnerOther, + TransactionPartnerUser, + User, +) +from telegram._utils.datetime import UTC, from_timestamp, to_timestamp +from telegram.constants import RevenueWithdrawalStateType, TransactionPartnerType +from tests.auxil.slots import mro_slots + + +def withdrawal_state_succeeded(): + return RevenueWithdrawalStateSucceeded( + date=datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC), + url="url", + ) + + +@pytest.fixture() +def withdrawal_state_failed(): + return RevenueWithdrawalStateFailed() + + +@pytest.fixture() +def withdrawal_state_pending(): + return RevenueWithdrawalStatePending() + + +def transaction_partner_user(): + return TransactionPartnerUser( + user=User(id=1, is_bot=False, first_name="first_name", username="username"), + ) + + +@pytest.fixture() +def transaction_partner_other(): + return TransactionPartnerOther() + + +def transaction_partner_fragment(): + return TransactionPartnerFragment( + withdrawal_state=withdrawal_state_succeeded(), + ) + + +def star_transaction(): + return StarTransaction( + id="1", + amount=1, + date=to_timestamp(datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)), + source=transaction_partner_user(), + receiver=transaction_partner_fragment(), + ) + + +@pytest.fixture() +def star_transactions(): + return StarTransactions( + transactions=[ + star_transaction(), + star_transaction(), + ] + ) + + +@pytest.fixture( + scope="module", + params=[ + TransactionPartner.FRAGMENT, + TransactionPartner.OTHER, + TransactionPartner.USER, + ], +) +def tp_scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + TransactionPartnerFragment, + TransactionPartnerOther, + TransactionPartnerUser, + ], + ids=[ + TransactionPartner.FRAGMENT, + TransactionPartner.OTHER, + TransactionPartner.USER, + ], +) +def tp_scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + (TransactionPartnerFragment, TransactionPartner.FRAGMENT), + (TransactionPartnerOther, TransactionPartner.OTHER), + (TransactionPartnerUser, TransactionPartner.USER), + ], + ids=[ + TransactionPartner.FRAGMENT, + TransactionPartner.OTHER, + TransactionPartner.USER, + ], +) +def tp_scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def transaction_partner(tp_scope_class_and_type): + # We use de_json here so that we don't have to worry about which class gets which arguments + return tp_scope_class_and_type[0].de_json( + { + "type": tp_scope_class_and_type[1], + "withdrawal_state": TestTransactionPartnerBase.withdrawal_state.to_dict(), + "user": TestTransactionPartnerBase.user.to_dict(), + }, + bot=None, + ) + + +@pytest.fixture( + scope="module", + params=[ + RevenueWithdrawalState.FAILED, + RevenueWithdrawalState.SUCCEEDED, + RevenueWithdrawalState.PENDING, + ], +) +def rws_scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + RevenueWithdrawalStateFailed, + RevenueWithdrawalStateSucceeded, + RevenueWithdrawalStatePending, + ], + ids=[ + RevenueWithdrawalState.FAILED, + RevenueWithdrawalState.SUCCEEDED, + RevenueWithdrawalState.PENDING, + ], +) +def rws_scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + (RevenueWithdrawalStateFailed, RevenueWithdrawalState.FAILED), + (RevenueWithdrawalStateSucceeded, RevenueWithdrawalState.SUCCEEDED), + (RevenueWithdrawalStatePending, RevenueWithdrawalState.PENDING), + ], + ids=[ + RevenueWithdrawalState.FAILED, + RevenueWithdrawalState.SUCCEEDED, + RevenueWithdrawalState.PENDING, + ], +) +def rws_scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def revenue_withdrawal_state(rws_scope_class_and_type): + # We use de_json here so that we don't have to worry about which class gets which arguments + return rws_scope_class_and_type[0].de_json( + { + "type": rws_scope_class_and_type[1], + "date": to_timestamp(TestRevenueWithdrawalStateBase.date), + "url": TestRevenueWithdrawalStateBase.url, + }, + bot=None, + ) + + +class TestStarTransactionBase: + id = "2" + amount = 2 + date = to_timestamp(datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)) + source = TransactionPartnerUser( + user=User( + id=2, + is_bot=False, + first_name="first_name", + ), + ) + receiver = TransactionPartnerOther() + + +class TestStarTransactionWithoutRequest(TestStarTransactionBase): + def test_slot_behaviour(self): + inst = star_transaction() + 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, bot): + json_dict = { + "id": self.id, + "amount": self.amount, + "date": self.date, + "source": self.source.to_dict(), + "receiver": self.receiver.to_dict(), + } + st = StarTransaction.de_json(json_dict, bot) + st_none = StarTransaction.de_json(None, bot) + assert st.id == self.id + assert st.amount == self.amount + assert st.date == from_timestamp(self.date) + assert st.source == self.source + assert st.receiver == self.receiver + assert st_none is None + + def test_de_json_star_transaction_localization(self, tz_bot, bot, raw_bot): + json_dict = star_transaction().to_dict() + st_raw = StarTransaction.de_json(json_dict, raw_bot) + st_bot = StarTransaction.de_json(json_dict, bot) + st_tz = StarTransaction.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + st_offset = st_tz.date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(st_tz.date.replace(tzinfo=None)) + + assert st_raw.date.tzinfo == UTC + assert st_bot.date.tzinfo == UTC + assert st_offset == tz_bot_offset + + def test_to_dict(self): + st = star_transaction() + expected_dict = { + "id": "1", + "amount": 1, + "date": st.date, + "source": st.source.to_dict(), + "receiver": st.receiver.to_dict(), + } + assert st.to_dict() == expected_dict + + def test_equality(self): + a = StarTransaction( + id=self.id, + amount=self.amount, + date=self.date, + source=self.source, + receiver=self.receiver, + ) + b = StarTransaction( + id=self.id, + amount=self.amount, + date=None, + source=self.source, + receiver=self.receiver, + ) + c = StarTransaction( + id="3", + amount=3, + date=to_timestamp(datetime.datetime.utcnow()), + source=TransactionPartnerUser( + user=User( + id=3, + is_bot=False, + first_name="first_name", + ), + ), + receiver=TransactionPartnerOther(), + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + +class TestStarTransactionsBase: + transactions = [star_transaction(), star_transaction()] + + +class TestStarTransactionsWithoutRequest(TestStarTransactionsBase): + def test_slot_behaviour(self, star_transactions): + inst = star_transactions + 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, bot): + json_dict = { + "transactions": [t.to_dict() for t in self.transactions], + } + st = StarTransactions.de_json(json_dict, bot) + st_none = StarTransactions.de_json(None, bot) + assert st.transactions == tuple(self.transactions) + assert st_none is None + + def test_to_dict(self, star_transactions): + expected_dict = { + "transactions": [t.to_dict() for t in self.transactions], + } + assert star_transactions.to_dict() == expected_dict + + def test_equality(self): + a = StarTransactions( + transactions=self.transactions, + ) + b = StarTransactions( + transactions=self.transactions, + ) + c = StarTransactions( + transactions=[star_transaction()], + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + +class TestTransactionPartnerBase: + withdrawal_state = withdrawal_state_succeeded() + user = transaction_partner_user().user + + +class TestTransactionPartner(TestTransactionPartnerBase): + def test_slot_behaviour(self, transaction_partner): + inst = transaction_partner + 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, bot, tp_scope_class_and_type): + cls = tp_scope_class_and_type[0] + type_ = tp_scope_class_and_type[1] + + json_dict = { + "type": type_, + "withdrawal_state": self.withdrawal_state.to_dict(), + "user": self.user.to_dict(), + } + tp = TransactionPartner.de_json(json_dict, bot) + assert set(tp.api_kwargs.keys()) == {"user", "withdrawal_state"} - set(cls.__slots__) + + assert isinstance(tp, TransactionPartner) + assert type(tp) is cls + assert tp.type == type_ + if "withdrawal_state" in cls.__slots__: + assert tp.withdrawal_state == self.withdrawal_state + if "user" in cls.__slots__: + assert tp.user == self.user + + assert cls.de_json(None, bot) is None + assert TransactionPartner.de_json({}, bot) is None + + def test_de_json_invalid_type(self, bot): + json_dict = { + "type": "invalid", + "withdrawal_state": self.withdrawal_state.to_dict(), + "user": self.user.to_dict(), + } + tp = TransactionPartner.de_json(json_dict, bot) + assert tp.api_kwargs == { + "withdrawal_state": self.withdrawal_state.to_dict(), + "user": self.user.to_dict(), + } + + assert type(tp) is TransactionPartner + assert tp.type == "invalid" + + def test_de_json_subclass(self, tp_scope_class, bot): + """This makes sure that e.g. TransactionPartnerUser(data) never returns a + TransactionPartnerFragment instance.""" + json_dict = { + "type": "invalid", + "withdrawal_state": self.withdrawal_state.to_dict(), + "user": self.user.to_dict(), + } + assert type(tp_scope_class.de_json(json_dict, bot)) is tp_scope_class + + def test_to_dict(self, transaction_partner): + tp_dict = transaction_partner.to_dict() + + assert isinstance(tp_dict, dict) + assert tp_dict["type"] == transaction_partner.type + if hasattr(transaction_partner, "web_app"): + assert tp_dict["user"] == transaction_partner.user.to_dict() + if hasattr(transaction_partner, "withdrawal_state"): + assert tp_dict["withdrawal_state"] == transaction_partner.withdrawal_state.to_dict() + + def test_type_enum_conversion(self): + assert type(TransactionPartner("other").type) is TransactionPartnerType + assert TransactionPartner("unknown").type == "unknown" + + def test_equality(self, transaction_partner, bot): + a = TransactionPartner("base_type") + b = TransactionPartner("base_type") + c = transaction_partner + d = deepcopy(transaction_partner) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "user"): + json_dict = c.to_dict() + json_dict["user"] = User(2, "something", True).to_dict() + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + +class TestRevenueWithdrawalStateBase: + date = datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC) + url = "url" + + +class TestRevenueWithdrawalState(TestRevenueWithdrawalStateBase): + def test_slot_behaviour(self, revenue_withdrawal_state): + inst = revenue_withdrawal_state + 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, bot, rws_scope_class_and_type): + cls = rws_scope_class_and_type[0] + type_ = rws_scope_class_and_type[1] + + json_dict = { + "type": type_, + "date": to_timestamp(self.date), + "url": self.url, + } + rws = RevenueWithdrawalState.de_json(json_dict, bot) + assert set(rws.api_kwargs.keys()) == {"date", "url"} - set(cls.__slots__) + + assert isinstance(rws, RevenueWithdrawalState) + assert type(rws) is cls + assert rws.type == type_ + if "date" in cls.__slots__: + assert rws.date == self.date + if "url" in cls.__slots__: + assert rws.url == self.url + + assert cls.de_json(None, bot) is None + assert RevenueWithdrawalState.de_json({}, bot) is None + + def test_de_json_invalid_type(self, bot): + json_dict = { + "type": "invalid", + "date": to_timestamp(self.date), + "url": self.url, + } + rws = RevenueWithdrawalState.de_json(json_dict, bot) + assert rws.api_kwargs == { + "date": to_timestamp(self.date), + "url": self.url, + } + + assert type(rws) is RevenueWithdrawalState + assert rws.type == "invalid" + + def test_de_json_subclass(self, rws_scope_class, bot): + """This makes sure that e.g. RevenueWithdrawalState(data) never returns a + RevenueWithdrawalStateFailed instance.""" + json_dict = { + "type": "invalid", + "date": to_timestamp(self.date), + "url": self.url, + } + assert type(rws_scope_class.de_json(json_dict, bot)) is rws_scope_class + + def test_to_dict(self, revenue_withdrawal_state): + rws_dict = revenue_withdrawal_state.to_dict() + + assert isinstance(rws_dict, dict) + assert rws_dict["type"] == revenue_withdrawal_state.type + if hasattr(revenue_withdrawal_state, "date"): + assert rws_dict["date"] == to_timestamp(revenue_withdrawal_state.date) + if hasattr(revenue_withdrawal_state, "url"): + assert rws_dict["url"] == revenue_withdrawal_state.url + + def test_type_enum_conversion(self): + assert type(RevenueWithdrawalState("failed").type) is RevenueWithdrawalStateType + assert RevenueWithdrawalState("unknown").type == "unknown" + + def test_equality(self, revenue_withdrawal_state, bot): + a = RevenueWithdrawalState("base_type") + b = RevenueWithdrawalState("base_type") + c = revenue_withdrawal_state + d = deepcopy(revenue_withdrawal_state) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "url"): + json_dict = c.to_dict() + json_dict["url"] = "something" + f = c.__class__.de_json(json_dict, bot) + + assert c == f + assert hash(c) == hash(f) + + if hasattr(c, "date"): + json_dict = c.to_dict() + json_dict["date"] = to_timestamp(datetime.datetime.utcnow()) + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) From 97226b1ae33e53b6b88ce9148828ebb598592dce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:29:09 +0200 Subject: [PATCH 10/26] Bump `pre-commit` Hooks to Latest Versions (#4337) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- .pre-commit-config.yaml | 8 ++++---- telegram/ext/_applicationbuilder.py | 4 +--- telegram/ext/_callbackdatacache.py | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a15b1f1bf4a..e0d933ea11c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,11 +25,11 @@ repos: - --diff - --check - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + rev: 7.1.0 hooks: - id: flake8 - repo: https://github.com/PyCQA/pylint - rev: v3.1.0 + rev: v3.2.4 hooks: - id: pylint files: ^(?!(tests|docs)).*\.py$ @@ -41,7 +41,7 @@ repos: - aiolimiter~=1.1.0 - . # this basically does `pip install -e .` - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.10.1 hooks: - id: mypy name: mypy-ptb @@ -68,7 +68,7 @@ repos: - cachetools~=5.3.3 - . # this basically does `pip install -e .` - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade args: diff --git a/telegram/ext/_applicationbuilder.py b/telegram/ext/_applicationbuilder.py index 2da56279941..a54d491614b 100644 --- a/telegram/ext/_applicationbuilder.py +++ b/telegram/ext/_applicationbuilder.py @@ -322,9 +322,7 @@ def build( bot = self._updater.bot update_queue = self._updater.update_queue - application: Application[ - BT, CCT, UD, CD, BD, JQ - ] = DefaultValue.get_value( # pylint: disable=not-callable + application: Application[BT, CCT, UD, CD, BD, JQ] = DefaultValue.get_value( self._application_class )( bot=bot, diff --git a/telegram/ext/_callbackdatacache.py b/telegram/ext/_callbackdatacache.py index 893150b480d..150f3f055b0 100644 --- a/telegram/ext/_callbackdatacache.py +++ b/telegram/ext/_callbackdatacache.py @@ -30,6 +30,8 @@ CACHE_TOOLS_AVAILABLE = False +import contextlib + from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, User from telegram._utils.datetime import to_float_timestamp from telegram.error import TelegramError @@ -425,10 +427,8 @@ def drop_data(self, callback_query: CallbackQuery) -> None: raise KeyError("CallbackQuery was not found in cache.") from exc def __drop_keyboard(self, keyboard_uuid: str) -> None: - try: + with contextlib.suppress(KeyError): self._keyboard_data.pop(keyboard_uuid) - except KeyError: - return def clear_callback_data(self, time_cutoff: Optional[Union[float, datetime]] = None) -> None: """Clears the stored callback data. From 4213c12c5b2aa22cabe327bfd615ec847d5cae7d Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:04:45 -0400 Subject: [PATCH 11/26] Use Python 3.13 Beta 3 in Test Suite (#4336) Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/unit_tests.yml | 3 ++- tests/ext/test_application.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 214eca12b30..6eac67758da 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -4,6 +4,7 @@ on: paths: - telegram/** - tests/** + - .github/workflows/unit_tests.yml - pyproject.toml - requirements-unit-tests.txt push: @@ -19,7 +20,7 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-beta.2'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-beta.3'] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: False steps: diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index acfce013acc..a74f3c739bf 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -2460,7 +2460,7 @@ async def callback(*args, **kwargs): monkeypatch.setattr(Application, "start", functools.partial(callback, name="start")) monkeypatch.setattr( - Updater, "start_polling", functools.partial(callback, name="start_polling") + Updater, "start_polling", functools.partialmethod(callback, name="start_polling") ) app = ( From c39839b026febe5198e69373d1b663eb4ec64815 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:44:41 -0400 Subject: [PATCH 12/26] Small Fixes for `test_stars.py` (#4347) --- tests/test_stars.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_stars.py b/tests/test_stars.py index 74f367cb0d2..25567b30cc0 100644 --- a/tests/test_stars.py +++ b/tests/test_stars.py @@ -361,7 +361,7 @@ class TestTransactionPartnerBase: user = transaction_partner_user().user -class TestTransactionPartner(TestTransactionPartnerBase): +class TestTransactionPartnerWithoutRequest(TestTransactionPartnerBase): def test_slot_behaviour(self, transaction_partner): inst = transaction_partner for attr in inst.__slots__: @@ -421,7 +421,7 @@ def test_to_dict(self, transaction_partner): assert isinstance(tp_dict, dict) assert tp_dict["type"] == transaction_partner.type - if hasattr(transaction_partner, "web_app"): + if hasattr(transaction_partner, "user"): assert tp_dict["user"] == transaction_partner.user.to_dict() if hasattr(transaction_partner, "withdrawal_state"): assert tp_dict["withdrawal_state"] == transaction_partner.withdrawal_state.to_dict() @@ -469,7 +469,7 @@ class TestRevenueWithdrawalStateBase: url = "url" -class TestRevenueWithdrawalState(TestRevenueWithdrawalStateBase): +class TestRevenueWithdrawalStateWithoutRequest(TestRevenueWithdrawalStateBase): def test_slot_behaviour(self, revenue_withdrawal_state): inst = revenue_withdrawal_state for attr in inst.__slots__: From 8018e5ff3ff8e5791618d4c21bc8a4d82f341e83 Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Fri, 5 Jul 2024 23:03:54 +0200 Subject: [PATCH 13/26] Extend `SuccessfulPayment` Test (#4349) --- tests/_payment/test_successfulpayment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/_payment/test_successfulpayment.py b/tests/_payment/test_successfulpayment.py index a0de91198c7..a93238c537a 100644 --- a/tests/_payment/test_successfulpayment.py +++ b/tests/_payment/test_successfulpayment.py @@ -68,6 +68,7 @@ def test_de_json(self, bot): assert successful_payment.invoice_payload == self.invoice_payload assert successful_payment.shipping_option_id == self.shipping_option_id assert successful_payment.currency == self.currency + assert successful_payment.total_amount == self.total_amount assert successful_payment.order_info == self.order_info assert successful_payment.telegram_payment_charge_id == self.telegram_payment_charge_id assert successful_payment.provider_payment_charge_id == self.provider_payment_charge_id @@ -81,6 +82,7 @@ def test_to_dict(self, successful_payment): successful_payment_dict["shipping_option_id"] == successful_payment.shipping_option_id ) assert successful_payment_dict["currency"] == successful_payment.currency + assert successful_payment_dict["total_amount"] == successful_payment.total_amount assert successful_payment_dict["order_info"] == successful_payment.order_info.to_dict() assert ( successful_payment_dict["telegram_payment_charge_id"] From 42d7c8c4770dd36ccb0b5893c410a380e03236ac Mon Sep 17 00:00:00 2001 From: Antares Date: Sat, 6 Jul 2024 22:08:29 +0800 Subject: [PATCH 14/26] Add `MessageEntity.adjust_message_entities_to_utf_16` Utility Function (#4323) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- telegram/_messageentity.py | 79 ++++++++++++++++++++++++++++++++++++- tests/test_messageentity.py | 22 +++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/telegram/_messageentity.py b/telegram/_messageentity.py index bbea88d10ae..bc80c2dcb3e 100644 --- a/telegram/_messageentity.py +++ b/telegram/_messageentity.py @@ -18,7 +18,9 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram MessageEntity.""" -from typing import TYPE_CHECKING, Final, List, Optional +import copy +import itertools +from typing import TYPE_CHECKING, Dict, Final, List, Optional, Sequence from telegram import constants from telegram._telegramobject import TelegramObject @@ -142,6 +144,81 @@ def de_json( return super().de_json(data=data, bot=bot) + @staticmethod + def adjust_message_entities_to_utf_16( + text: str, entities: Sequence["MessageEntity"] + ) -> Sequence["MessageEntity"]: + """Utility functionality for converting the offset and length of entities from + Unicode (:obj:`str`) to UTF-16 (``utf-16-le`` encoded :obj:`bytes`). + + Tip: + Only the offsets and lengths calulated in UTF-16 is acceptable by the Telegram Bot API. + If they are calculated using the Unicode string (:obj:`str` object), errors may occur + when the text contains characters that are not in the Basic Multilingual Plane (BMP). + For more information, see `Unicode `_ and + `Plane (Unicode) `_. + + .. versionadded:: NEXT.VERSION + + Examples: + Below is a snippet of code that demonstrates how to use this function to convert + entities from Unicode to UTF-16 space. The ``unicode_entities`` are calculated in + Unicode and the `utf_16_entities` are calculated in UTF-16. + + .. code-block:: python + + text = "𠌕 bold 𝄢 italic underlined: 𝛙𝌢𑁍" + unicode_entities = [ + MessageEntity(offset=2, length=4, type=MessageEntity.BOLD), + MessageEntity(offset=9, length=6, type=MessageEntity.ITALIC), + MessageEntity(offset=28, length=3, type=MessageEntity.UNDERLINE), + ] + utf_16_entities = MessageEntity.adjust_message_entities_to_utf_16( + text, unicode_entities + ) + await bot.send_message( + chat_id=123, + text=text, + entities=utf_16_entities, + ) + # utf_16_entities[0]: offset=3, length=4 + # utf_16_entities[1]: offset=11, length=6 + # utf_16_entities[2]: offset=30, length=6 + + Args: + text (:obj:`str`): The text that the entities belong to + entities (Sequence[:class:`telegram.MessageEntity`]): Sequence of entities + with offset and length calculated in Unicode + + Returns: + Sequence[:class:`telegram.MessageEntity`]: Sequence of entities + with offset and length calculated in UTF-16 encoding + """ + # get sorted positions + positions = sorted(itertools.chain(*((x.offset, x.offset + x.length) for x in entities))) + accumulated_length = 0 + # calculate the length of each slice text[:position] in utf-16 accordingly, + # store the position translations + position_translation: Dict[int, int] = {} + for i, position in enumerate(positions): + last_position = positions[i - 1] if i > 0 else 0 + text_slice = text[last_position:position] + accumulated_length += len(text_slice.encode("utf-16-le")) // 2 + position_translation[position] = accumulated_length + # get the final output entites + out = [] + for entity in entities: + translated_positions = position_translation[entity.offset] + translated_length = ( + position_translation[entity.offset + entity.length] - translated_positions + ) + new_entity = copy.copy(entity) + with new_entity._unfrozen(): + new_entity.offset = translated_positions + new_entity.length = translated_length + out.append(new_entity) + return out + ALL_TYPES: Final[List[str]] = list(constants.MessageEntityType) """List[:obj:`str`]: A list of all available message entity types.""" BLOCKQUOTE: Final[str] = constants.MessageEntityType.BLOCKQUOTE diff --git a/tests/test_messageentity.py b/tests/test_messageentity.py index b14ec79c321..8bab9fec7b9 100644 --- a/tests/test_messageentity.py +++ b/tests/test_messageentity.py @@ -16,6 +16,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/]. +import random +from typing import List, Tuple + import pytest from telegram import MessageEntity, User @@ -81,6 +84,25 @@ def test_enum_init(self): entity = MessageEntity(type="url", offset=0, length=1) assert entity.type is MessageEntityType.URL + def test_fix_utf16(self): + text = "𠌕 bold 𝄢 italic underlined: 𝛙𝌢𑁍" + inputs_outputs: List[Tuple[Tuple[int, int, str], Tuple[int, int]]] = [ + ((2, 4, MessageEntity.BOLD), (3, 4)), + ((9, 6, MessageEntity.ITALIC), (11, 6)), + ((28, 3, MessageEntity.UNDERLINE), (30, 6)), + ] + random.shuffle(inputs_outputs) + unicode_entities = [ + MessageEntity(offset=_input[0], length=_input[1], type=_input[2]) + for _input, _ in inputs_outputs + ] + utf_16_entities = MessageEntity.adjust_message_entities_to_utf_16(text, unicode_entities) + for out_entity, input_output in zip(utf_16_entities, inputs_outputs): + _, output = input_output + offset, length = output + assert out_entity.offset == offset + assert out_entity.length == length + def test_equality(self): a = MessageEntity(MessageEntity.BOLD, 2, 3) b = MessageEntity(MessageEntity.BOLD, 2, 3) From 98bed6f01ad922c2782ded17871bfbb50ecbcd18 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:09:04 +0200 Subject: [PATCH 15/26] Log Received Data on Deserialization Errors (#4304) --- telegram/_bot.py | 11 ++++++++++- telegram/ext/_utils/webhookhandler.py | 3 ++- tests/ext/test_updater.py | 1 + tests/test_bot.py | 23 +++++++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 85cf417911d..81b75419b89 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -4374,7 +4374,16 @@ async def get_updates( else: self._LOGGER.debug("No new updates found.") - return Update.de_list(result, self) + try: + return Update.de_list(result, self) + except Exception as exc: + # This logging is in place mostly b/c we can't access the raw json data in Updater, + # where the exception is caught and logged again. Still, it might also be beneficial + # for custom usages of `get_updates`. + self._LOGGER.critical( + "Error while parsing updates! Received data was %r", result, exc_info=exc + ) + raise exc async def set_webhook( self, diff --git a/telegram/ext/_utils/webhookhandler.py b/telegram/ext/_utils/webhookhandler.py index 828dbca4715..a174fbaa476 100644 --- a/telegram/ext/_utils/webhookhandler.py +++ b/telegram/ext/_utils/webhookhandler.py @@ -168,7 +168,8 @@ async def post(self) -> None: except Exception as exc: _LOGGER.critical( "Something went wrong processing the data received from Telegram. " - "Received data was *not* processed!", + "Received data was *not* processed! Received data was: %r", + data, exc_info=exc, ) raise tornado.web.HTTPError( diff --git a/tests/ext/test_updater.py b/tests/ext/test_updater.py index feed189c662..1fd8985dea1 100644 --- a/tests/ext/test_updater.py +++ b/tests/ext/test_updater.py @@ -1179,6 +1179,7 @@ def de_json_fails(*args, **kwargs): assert len(caplog.records) == 1 assert caplog.records[-1].getMessage().startswith("Something went wrong processing") + assert "Received data was: {" in caplog.records[-1].getMessage() assert caplog.records[-1].name == "telegram.ext.Updater" assert response.status_code == 400 assert response.text == self.response_text.format( diff --git a/tests/test_bot.py b/tests/test_bot.py index 05d655450f3..df142e3fb61 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -535,6 +535,29 @@ async def post(url, request_data: RequestData, *args, **kwargs): 123, "text", api_kwargs={"unknown_kwarg_1": 7, "unknown_kwarg_2": 5} ) + async def test_get_updates_deserialization_error(self, bot, monkeypatch, caplog): + async def faulty_do_request(*args, **kwargs): + return ( + HTTPStatus.OK, + b'{"ok": true, "result": [{"update_id": "1", "message": "unknown_format"}]}', + ) + + monkeypatch.setattr(HTTPXRequest, "do_request", faulty_do_request) + + bot = PytestExtBot(get_updates_request=HTTPXRequest(), token=bot.token) + + with caplog.at_level(logging.CRITICAL), pytest.raises(AttributeError): + await bot.get_updates() + + assert len(caplog.records) == 1 + assert caplog.records[0].name == "telegram.ext.ExtBot" + assert caplog.records[0].levelno == logging.CRITICAL + assert caplog.records[0].getMessage() == ( + "Error while parsing updates! Received data was " + "[{'update_id': '1', 'message': 'unknown_format'}]" + ) + assert caplog.records[0].exc_info[0] is AttributeError + async def test_answer_web_app_query(self, bot, raw_bot, monkeypatch): params = False From dba7866aab48401244660f7ad42c044d9da23814 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 7 Jul 2024 07:08:52 -0400 Subject: [PATCH 16/26] API 7.6 (#4333, #4341, #4342, #4334, #4335, #4344, #4348, #4351) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- README.rst | 4 +- docs/source/inclusions/bot_methods.rst | 2 + docs/source/telegram.at-tree.rst | 18 +- docs/source/telegram.inputpaidmedia.rst | 6 + docs/source/telegram.inputpaidmediaphoto.rst | 6 + docs/source/telegram.inputpaidmediavideo.rst | 6 + docs/source/telegram.paidmedia.rst | 6 + docs/source/telegram.paidmediainfo.rst | 6 + docs/source/telegram.paidmediaphoto.rst | 6 + docs/source/telegram.paidmediapreview.rst | 6 + docs/source/telegram.paidmediavideo.rst | 6 + docs/source/telegram.payments-tree.rst | 11 + ...telegram.transactionpartnertelegramads.rst | 7 + telegram/__init__.py | 34 +- telegram/_bot.py | 115 ++++++- telegram/_chat.py | 55 +++ telegram/_chatfullinfo.py | 11 + telegram/_files/_basethumbedmedium.py | 4 +- telegram/_files/animation.py | 20 +- telegram/_files/audio.py | 24 +- telegram/_files/document.py | 14 +- telegram/_files/inputmedia.py | 169 ++++++++- telegram/_files/location.py | 8 +- telegram/_files/video.py | 20 +- telegram/_files/videonote.py | 4 +- telegram/_files/voice.py | 8 +- telegram/_menubutton.py | 9 +- telegram/_message.py | 24 +- telegram/_paidmedia.py | 290 ++++++++++++++++ telegram/_payment/precheckoutquery.py | 4 +- telegram/_payment/shippingquery.py | 4 +- telegram/{_stars.py => _payment/stars.py} | 36 +- telegram/_payment/successfulpayment.py | 4 +- telegram/_poll.py | 2 +- telegram/_reply.py | 13 + telegram/constants.py | 49 ++- telegram/ext/_extbot.py | 46 +++ telegram/request/_requestparameter.py | 4 +- tests/_files/test_inputmedia.py | 123 +++++++ tests/test_chat.py | 16 + tests/test_chatfullinfo.py | 27 +- tests/test_constants.py | 2 + tests/test_message.py | 5 + tests/test_official/exceptions.py | 4 + tests/test_paidmedia.py | 325 ++++++++++++++++++ tests/test_reply.py | 7 + tests/test_stars.py | 20 +- 47 files changed, 1470 insertions(+), 120 deletions(-) create mode 100644 docs/source/telegram.inputpaidmedia.rst create mode 100644 docs/source/telegram.inputpaidmediaphoto.rst create mode 100644 docs/source/telegram.inputpaidmediavideo.rst create mode 100644 docs/source/telegram.paidmedia.rst create mode 100644 docs/source/telegram.paidmediainfo.rst create mode 100644 docs/source/telegram.paidmediaphoto.rst create mode 100644 docs/source/telegram.paidmediapreview.rst create mode 100644 docs/source/telegram.paidmediavideo.rst create mode 100644 docs/source/telegram.transactionpartnertelegramads.rst create mode 100644 telegram/_paidmedia.py rename telegram/{_stars.py => _payment/stars.py} (93%) create mode 100644 tests/test_paidmedia.py diff --git a/README.rst b/README.rst index 1e3570e95be..e8aecc8df93 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.5-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.6-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -79,7 +79,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -All types and methods of the Telegram Bot API **7.5** are supported. +All types and methods of the Telegram Bot API **7.6** are supported. Installing ========== diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index f79f5bd959c..3189de1c1d3 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -33,6 +33,8 @@ - Used for sending media grouped together * - :meth:`~telegram.Bot.send_message` - Used for sending text messages + * - :meth:`~telegram.Bot.send_paid_media` + - Used for sending paid media to channels * - :meth:`~telegram.Bot.send_photo` - Used for sending photos * - :meth:`~telegram.Bot.send_poll` diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 077b124aba4..8d3238a27e4 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -88,6 +88,9 @@ Available Types telegram.inputmediadocument telegram.inputmediaphoto telegram.inputmediavideo + telegram.inputpaidmedia + telegram.inputpaidmediaphoto + telegram.inputpaidmediavideo telegram.inputpolloption telegram.inputsticker telegram.keyboardbutton @@ -113,6 +116,11 @@ Available Types telegram.messageoriginuser telegram.messagereactioncountupdated telegram.messagereactionupdated + telegram.paidmedia + telegram.paidmediainfo + telegram.paidmediaphoto + telegram.paidmediapreview + telegram.paidmediavideo telegram.photosize telegram.poll telegram.pollanswer @@ -125,22 +133,12 @@ Available Types telegram.replykeyboardmarkup telegram.replykeyboardremove telegram.replyparameters - telegram.revenuewithdrawalstate - telegram.revenuewithdrawalstatefailed - telegram.revenuewithdrawalstatepending - telegram.revenuewithdrawalstatesucceeded telegram.sentwebappmessage telegram.shareduser - telegram.startransaction - telegram.startransactions telegram.story telegram.switchinlinequerychosenchat telegram.telegramobject telegram.textquote - telegram.transactionpartner - telegram.transactionpartnerfragment - telegram.transactionpartnerother - telegram.transactionpartneruser telegram.update telegram.user telegram.userchatboosts diff --git a/docs/source/telegram.inputpaidmedia.rst b/docs/source/telegram.inputpaidmedia.rst new file mode 100644 index 00000000000..ecb45d35f6d --- /dev/null +++ b/docs/source/telegram.inputpaidmedia.rst @@ -0,0 +1,6 @@ +InputPaidMedia +============== + +.. autoclass:: telegram.InputPaidMedia + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inputpaidmediaphoto.rst b/docs/source/telegram.inputpaidmediaphoto.rst new file mode 100644 index 00000000000..f8df55823a2 --- /dev/null +++ b/docs/source/telegram.inputpaidmediaphoto.rst @@ -0,0 +1,6 @@ +InputPaidMediaPhoto +=================== + +.. autoclass:: telegram.InputPaidMediaPhoto + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inputpaidmediavideo.rst b/docs/source/telegram.inputpaidmediavideo.rst new file mode 100644 index 00000000000..8a3789f5028 --- /dev/null +++ b/docs/source/telegram.inputpaidmediavideo.rst @@ -0,0 +1,6 @@ +InputPaidMediaVideo +=================== + +.. autoclass:: telegram.InputPaidMediaVideo + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.paidmedia.rst b/docs/source/telegram.paidmedia.rst new file mode 100644 index 00000000000..0883310f324 --- /dev/null +++ b/docs/source/telegram.paidmedia.rst @@ -0,0 +1,6 @@ +PaidMedia +========= + +.. autoclass:: telegram.PaidMedia + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediainfo.rst b/docs/source/telegram.paidmediainfo.rst new file mode 100644 index 00000000000..3c0d1e75c52 --- /dev/null +++ b/docs/source/telegram.paidmediainfo.rst @@ -0,0 +1,6 @@ +PaidMediaInfo +============= + +.. autoclass:: telegram.PaidMediaInfo + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediaphoto.rst b/docs/source/telegram.paidmediaphoto.rst new file mode 100644 index 00000000000..4092cfcc187 --- /dev/null +++ b/docs/source/telegram.paidmediaphoto.rst @@ -0,0 +1,6 @@ +PaidMediaPhoto +============== + +.. autoclass:: telegram.PaidMediaPhoto + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediapreview.rst b/docs/source/telegram.paidmediapreview.rst new file mode 100644 index 00000000000..32ff4809d69 --- /dev/null +++ b/docs/source/telegram.paidmediapreview.rst @@ -0,0 +1,6 @@ +PaidMediaPreview +================ + +.. autoclass:: telegram.PaidMediaPreview + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmediavideo.rst b/docs/source/telegram.paidmediavideo.rst new file mode 100644 index 00000000000..30f2377ac86 --- /dev/null +++ b/docs/source/telegram.paidmediavideo.rst @@ -0,0 +1,6 @@ +PaidMediaVideo +============== + +.. autoclass:: telegram.PaidMediaVideo + :members: + :show-inheritance: diff --git a/docs/source/telegram.payments-tree.rst b/docs/source/telegram.payments-tree.rst index 67f686ecc4b..2a09c5415ac 100644 --- a/docs/source/telegram.payments-tree.rst +++ b/docs/source/telegram.payments-tree.rst @@ -8,7 +8,18 @@ Payments telegram.labeledprice telegram.orderinfo telegram.precheckoutquery + telegram.revenuewithdrawalstate + telegram.revenuewithdrawalstatefailed + telegram.revenuewithdrawalstatepending + telegram.revenuewithdrawalstatesucceeded telegram.shippingaddress telegram.shippingoption telegram.shippingquery + telegram.startransaction + telegram.startransactions telegram.successfulpayment + telegram.transactionpartner + telegram.transactionpartnerfragment + telegram.transactionpartnerother + telegram.transactionpartnertelegramads + telegram.transactionpartneruser diff --git a/docs/source/telegram.transactionpartnertelegramads.rst b/docs/source/telegram.transactionpartnertelegramads.rst new file mode 100644 index 00000000000..926b25bdcd4 --- /dev/null +++ b/docs/source/telegram.transactionpartnertelegramads.rst @@ -0,0 +1,7 @@ +TransactionPartnerTelegramAds +============================= + +.. autoclass:: telegram.TransactionPartnerTelegramAds + :members: + :show-inheritance: + :inherited-members: TelegramObject diff --git a/telegram/__init__.py b/telegram/__init__.py index 48ad57298c6..af2336a4ac9 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -142,6 +142,9 @@ "InputMediaPhoto", "InputMediaVideo", "InputMessageContent", + "InputPaidMedia", + "InputPaidMediaPhoto", + "InputPaidMediaVideo", "InputPollOption", "InputSticker", "InputTextMessageContent", @@ -173,6 +176,11 @@ "MessageReactionCountUpdated", "MessageReactionUpdated", "OrderInfo", + "PaidMedia", + "PaidMediaInfo", + "PaidMediaPhoto", + "PaidMediaPreview", + "PaidMediaVideo", "PassportData", "PassportElementError", "PassportElementErrorDataField", @@ -223,6 +231,7 @@ "TransactionPartner", "TransactionPartnerFragment", "TransactionPartnerOther", + "TransactionPartnerTelegramAds", "TransactionPartnerUser", "Update", "User", @@ -333,6 +342,9 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputPaidMedia, + InputPaidMediaPhoto, + InputPaidMediaVideo, ) from ._files.inputsticker import InputSticker from ._files.location import Location @@ -405,6 +417,7 @@ MessageOriginUser, ) from ._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated +from ._paidmedia import PaidMedia, PaidMediaInfo, PaidMediaPhoto, PaidMediaPreview, PaidMediaVideo from ._passport.credentials import ( Credentials, DataCredentials, @@ -436,16 +449,7 @@ from ._payment.shippingaddress import ShippingAddress from ._payment.shippingoption import ShippingOption from ._payment.shippingquery import ShippingQuery -from ._payment.successfulpayment import SuccessfulPayment -from ._poll import InputPollOption, Poll, PollAnswer, PollOption -from ._proximityalerttriggered import ProximityAlertTriggered -from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji -from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote -from ._replykeyboardmarkup import ReplyKeyboardMarkup -from ._replykeyboardremove import ReplyKeyboardRemove -from ._sentwebappmessage import SentWebAppMessage -from ._shared import ChatShared, SharedUser, UsersShared -from ._stars import ( +from ._payment.stars import ( RevenueWithdrawalState, RevenueWithdrawalStateFailed, RevenueWithdrawalStatePending, @@ -455,8 +459,18 @@ TransactionPartner, TransactionPartnerFragment, TransactionPartnerOther, + TransactionPartnerTelegramAds, TransactionPartnerUser, ) +from ._payment.successfulpayment import SuccessfulPayment +from ._poll import InputPollOption, Poll, PollAnswer, PollOption +from ._proximityalerttriggered import ProximityAlertTriggered +from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji +from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote +from ._replykeyboardmarkup import ReplyKeyboardMarkup +from ._replykeyboardremove import ReplyKeyboardRemove +from ._sentwebappmessage import SentWebAppMessage +from ._shared import ChatShared, SharedUser, UsersShared from ._story import Story from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject diff --git a/telegram/_bot.py b/telegram/_bot.py index 81b75419b89..6a1cbfd07af 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -70,7 +70,7 @@ from telegram._files.contact import Contact from telegram._files.document import Document from telegram._files.file import File -from telegram._files.inputmedia import InputMedia +from telegram._files.inputmedia import InputMedia, InputPaidMedia from telegram._files.location import Location from telegram._files.photosize import PhotoSize from telegram._files.sticker import MaskPosition, Sticker, StickerSet @@ -84,11 +84,11 @@ from telegram._menubutton import MenuButton from telegram._message import Message from telegram._messageid import MessageId +from telegram._payment.stars import StarTransactions from telegram._poll import InputPollOption, Poll from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji from telegram._reply import ReplyParameters from telegram._sentwebappmessage import SentWebAppMessage -from telegram._stars import StarTransactions from telegram._telegramobject import TelegramObject from telegram._update import Update from telegram._user import User @@ -578,13 +578,16 @@ def _insert_defaults(self, data: Dict[str, object]) -> None: with new._unfrozen(): new.parse_mode = DefaultValue.get_value(new.parse_mode) data[key] = new - elif key == "media" and isinstance(val, Sequence): + elif ( + key == "media" + and isinstance(val, Sequence) + and not isinstance(val[0], InputPaidMedia) + ): # Copy objects as not to edit them in-place copy_list = [copy.copy(media) for media in val] for media in copy_list: with media._unfrozen(): media.parse_mode = DefaultValue.get_value(media.parse_mode) - data[key] = copy_list # 2) else: @@ -7654,7 +7657,8 @@ async def copy_message( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, ) -> MessageId: - """Use this method to copy messages of any kind. Service messages and invoice messages + """Use this method to copy messages of any kind. Service messages, paid media messages, + giveaway messages, giveaway winners messages, and invoice messages can't be copied. The method is analogous to the method :meth:`forward_message`, but the copied message doesn't have a link to the original message. @@ -7780,11 +7784,12 @@ async def copy_messages( ) -> Tuple["MessageId", ...]: """ Use this method to copy messages of any kind. If some of the specified messages can't be - found or copied, they are skipped. Service messages, giveaway messages, giveaway winners - messages, and invoice messages can't be copied. A quiz poll can be copied only if the value - of the field correct_option_id is known to the bot. The method is analogous to the method - :meth:`forward_messages`, but the copied messages don't have a link to the original - message. Album grouping is kept for copied messages. + found or copied, they are skipped. Service messages, paid media messages, giveaway + messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can + be copied only if the value + of the field :attr:`telegram.Poll.correct_option_id` is known to the bot. The method is + analogous to the method :meth:`forward_messages`, but the copied messages don't have a + link to the original message. Album grouping is kept for copied messages. .. versionadded:: 20.8 @@ -9163,6 +9168,94 @@ async def get_star_transactions( bot=self, ) + async def send_paid_media( + self, + chat_id: Union[str, int], + star_count: int, + media: Sequence["InputPaidMedia"], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional[ReplyMarkup] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> Message: + """Use this method to send paid media to channel chats. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access + to the media. + media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be + sent; up to :tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items. + caption (:obj:`str`, optional): Caption of the media to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + Returns: + :class:`telegram.Message`: On success, the sent message is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = { + "chat_id": chat_id, + "star_count": star_count, + "media": media, + "show_caption_above_media": show_caption_above_media, + } + + return await self._send_message( + "sendPaidMedia", + data, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + protect_content=protect_content, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -9417,3 +9510,5 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`refund_star_payment`""" getStarTransactions = get_star_transactions """Alias for :meth:`get_star_transactions`""" + sendPaidMedia = send_paid_media + """Alias for :meth:`send_paid_media`""" diff --git a/telegram/_chat.py b/telegram/_chat.py index b5e2d111f1a..2513e0ff334 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -48,6 +48,7 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputPaidMedia, InputPollOption, LabeledPrice, LinkPreviewOptions, @@ -3257,6 +3258,60 @@ async def set_message_reaction( api_kwargs=api_kwargs, ) + async def send_paid_media( + self, + star_count: int, + media: Sequence["InputPaidMedia"], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional[ReplyMarkup] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_paid_media(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.send_paid_media`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + """ + return await self.get_bot().send_paid_media( + chat_id=self.id, + star_count=star_count, + media=media, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, + disable_notification=disable_notification, + protect_content=protect_content, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + class Chat(_ChatBase): """This object represents a chat. diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index fbdc9d6842f..d4bda7d415a 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -195,6 +195,10 @@ class ChatFullInfo(_ChatBase): chats. location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which the supergroup is connected. + can_send_paid_media (:obj:`bool`, optional): :obj:`True`, if paid media messages can be + sent or forwarded to the channel chat. The field is available only for channel chats. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this chat. @@ -345,6 +349,10 @@ class ChatFullInfo(_ChatBase): chats. location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which the supergroup is connected. + can_send_paid_media (:obj:`bool`): Optional. :obj:`True`, if paid media messages can be + sent or forwarded to the channel chat. The field is available only for channel chats. + + .. versionadded:: NEXT.VERSION .. _accent colors: https://core.telegram.org/bots/api#accent-colors .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups @@ -360,6 +368,7 @@ class ChatFullInfo(_ChatBase): "business_intro", "business_location", "business_opening_hours", + "can_send_paid_media", "can_set_sticker_set", "custom_emoji_sticker_set_name", "description", @@ -434,6 +443,7 @@ def __init__( custom_emoji_sticker_set_name: Optional[str] = None, linked_chat_id: Optional[int] = None, location: Optional[ChatLocation] = None, + can_send_paid_media: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -496,6 +506,7 @@ def __init__( self.business_intro: Optional[BusinessIntro] = business_intro 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 @classmethod def de_json( diff --git a/telegram/_files/_basethumbedmedium.py b/telegram/_files/_basethumbedmedium.py index ba35ea2e53e..20ff82eab5e 100644 --- a/telegram/_files/_basethumbedmedium.py +++ b/telegram/_files/_basethumbedmedium.py @@ -44,7 +44,7 @@ class _BaseThumbedMedium(_BaseMedium): 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`, optional): File size. - thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail as defined by sender. + thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail as defined by the sender. .. versionadded:: 20.2 @@ -54,7 +54,7 @@ class _BaseThumbedMedium(_BaseMedium): 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`): Optional. File size. - thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail as defined by sender. + thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail as defined by the sender. .. versionadded:: 20.2 diff --git a/telegram/_files/animation.py b/telegram/_files/animation.py index 3459e642778..5191ce83d89 100644 --- a/telegram/_files/animation.py +++ b/telegram/_files/animation.py @@ -39,11 +39,11 @@ class Animation(_BaseThumbedMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - file_name (:obj:`str`, optional): Original animation filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. + width (:obj:`int`): Video width as defined by the sender. + height (:obj:`int`): Video height as defined by the sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. + file_name (:obj:`str`, optional): Original animation filename as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Animation thumbnail as defined by sender. @@ -56,11 +56,11 @@ class Animation(_BaseThumbedMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - file_name (:obj:`str`): Optional. Original animation filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + width (:obj:`int`): Video width as defined by the sender. + height (:obj:`int`): Video height as defined by the sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. + file_name (:obj:`str`): Optional. Original animation filename as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined by sender. diff --git a/telegram/_files/audio.py b/telegram/_files/audio.py index bf5eb123d00..fb7bc2ce7d1 100644 --- a/telegram/_files/audio.py +++ b/telegram/_files/audio.py @@ -39,12 +39,12 @@ class Audio(_BaseThumbedMedium): or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. - file_name (:obj:`str`, optional): Original filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. + duration (:obj:`int`): Duration of the audio in seconds as defined by the sender. + performer (:obj:`str`, optional): Performer of the audio as defined by the sender or by + audio tags. + title (:obj:`str`, optional): Title of the audio as defined by the sender or by audio tags. + file_name (:obj:`str`, optional): Original filename as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to which the music file belongs. @@ -56,12 +56,12 @@ class Audio(_BaseThumbedMedium): or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. - file_name (:obj:`str`): Optional. Original filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + duration (:obj:`int`): Duration of the audio in seconds as defined by the sender. + performer (:obj:`str`): Optional. Performer of the audio as defined by the sender or by + audio tags. + title (:obj:`str`): Optional. Title of the audio as defined by the sender or by audio tags. + file_name (:obj:`str`): Optional. Original filename as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to which the music file belongs. diff --git a/telegram/_files/document.py b/telegram/_files/document.py index a281ffefeaf..e278dc43e3b 100644 --- a/telegram/_files/document.py +++ b/telegram/_files/document.py @@ -39,10 +39,11 @@ class Document(_BaseThumbedMedium): or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - file_name (:obj:`str`, optional): Original filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. + file_name (:obj:`str`, optional): Original filename as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. - thumbnail (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by sender. + thumbnail (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by the + sender. .. versionadded:: 20.2 @@ -51,10 +52,11 @@ class Document(_BaseThumbedMedium): or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - file_name (:obj:`str`): Optional. Original filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + file_name (:obj:`str`): Optional. Original filename as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. - thumbnail (:class:`telegram.PhotoSize`): Optional. Document thumbnail as defined by sender. + thumbnail (:class:`telegram.PhotoSize`): Optional. Document thumbnail as defined by the + sender. .. versionadded:: 20.2 diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 0cf5955a4d3..3715682fa3b 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """Base class for Telegram InputMedia Objects.""" -from typing import Optional, Sequence, Tuple, Union +from typing import Final, Optional, Sequence, Tuple, Union from telegram import constants from telegram._files.animation import Animation @@ -115,13 +115,162 @@ def _parse_thumbnail_input(thumbnail: Optional[FileInput]) -> Optional[Union[str ) +class InputPaidMedia(TelegramObject): + """ + Base class for Telegram InputPaidMedia Objects. Currently, it can be one of: + + * :class:`telegram.InputMediaPhoto` + * :class:`telegram.InputMediaVideo` + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of media that the instance represents. + media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.PhotoSize` | :class:`telegram.Video`): File to send. |fileinputnopath| + Lastly you can pass an existing telegram media object of the corresponding type + to send. + + Attributes: + type (:obj:`str`): Type of the input media. + media (:obj:`str` | :class:`telegram.InputFile`): Media to send. + """ + + PHOTO: Final[str] = constants.InputPaidMediaType.PHOTO + """:const:`telegram.constants.InputPaidMediaType.PHOTO`""" + VIDEO: Final[str] = constants.InputPaidMediaType.VIDEO + """:const:`telegram.constants.InputPaidMediaType.VIDEO`""" + + __slots__ = ("media", "type") + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + media: Union[str, InputFile, PhotoSize, Video], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.InputPaidMediaType, type, type) + self.media: Union[str, InputFile, PhotoSize, Video] = media + + self._freeze() + + +class InputPaidMediaPhoto(InputPaidMedia): + """The paid media to send is a photo. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionadded:: NEXT.VERSION + + Args: + media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.PhotoSize`): File to send. |fileinputnopath| + Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. + + Attributes: + type (:obj:`str`): Type of the media, always + :tg-const:`telegram.constants.InputPaidMediaType.PHOTO`. + media (:obj:`str` | :class:`telegram.InputFile`): Photo to send. + """ + + __slots__ = () + + def __init__( + self, + media: Union[FileInput, PhotoSize], + *, + api_kwargs: Optional[JSONDict] = None, + ): + media = parse_file_input(media, PhotoSize, attach=True, local_mode=True) + super().__init__(type=InputPaidMedia.PHOTO, media=media, api_kwargs=api_kwargs) + self._freeze() + + +class InputPaidMediaVideo(InputPaidMedia): + """ + The paid media to send is a video. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionadded:: NEXT.VERSION + + Note: + * When using a :class:`telegram.Video` for the :attr:`media` attribute, it will take the + width, height and duration from that video, unless otherwise specified with the optional + arguments. + * :paramref:`thumbnail` will be ignored for small video files, for which Telegram can + easily generate thumbnails. However, this behaviour is undocumented and might be + changed by Telegram. + + Args: + media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ + :class:`telegram.Video`): File to send. |fileinputnopath| + 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| + width (:obj:`int`, optional): Video width. + height (:obj:`int`, optional): Video height. + duration (:obj:`int`, optional): Video duration in seconds. + supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is + suitable for streaming. + + Attributes: + type (:obj:`str`): Type of the media, always + :tg-const:`telegram.constants.InputPaidMediaType.VIDEO`. + media (:obj:`str` | :class:`telegram.InputFile`): Video to send. + thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| + width (:obj:`int`): Optional. Video width. + height (:obj:`int`): Optional. Video height. + duration (:obj:`int`): Optional. Video duration in seconds. + supports_streaming (:obj:`bool`): Optional. :obj:`True`, if the uploaded video is + suitable for streaming. + """ + + __slots__ = ("duration", "height", "supports_streaming", "thumbnail", "width") + + def __init__( + self, + media: Union[FileInput, Video], + thumbnail: Optional[FileInput] = None, + width: Optional[int] = None, + height: Optional[int] = None, + duration: Optional[int] = None, + supports_streaming: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + if isinstance(media, Video): + width = width if width is not None else media.width + height = height if height is not None else media.height + duration = duration if duration is not None else media.duration + media = media.file_id + else: + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + media = parse_file_input(media, attach=True, local_mode=True) + + super().__init__(type=InputPaidMedia.VIDEO, media=media, api_kwargs=api_kwargs) + with self._unfrozen(): + self.thumbnail: Optional[Union[str, InputFile]] = InputMedia._parse_thumbnail_input( + thumbnail + ) + self.width: Optional[int] = width + self.height: Optional[int] = height + self.duration: Optional[int] = duration + self.supports_streaming: Optional[bool] = supports_streaming + + class InputMediaAnimation(InputMedia): """Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. Note: When using a :class:`telegram.Animation` for the :attr:`media` attribute, it will take the - width, height and duration from that video, unless otherwise specified with the optional - arguments. + width, height and duration from that animation, unless otherwise specified with the + optional arguments. .. seealso:: :wiki:`Working with Files and Media ` @@ -510,10 +659,10 @@ class InputMediaAudio(InputMedia): .. versionchanged:: 20.0 |sequenceclassargs| - duration (:obj:`int`, optional): Duration of the audio in seconds as defined by sender. - performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. + duration (:obj:`int`, optional): Duration of the audio in seconds as defined by the sender. + performer (:obj:`str`, optional): Performer of the audio as defined by the sender or by + audio tags. + title (:obj:`str`, optional): Title of the audio as defined by the sender or by audio tags. thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ optional): |thumbdocstringnopath| @@ -533,9 +682,9 @@ class InputMediaAudio(InputMedia): * |tupleclassattrs| * |alwaystuple| duration (:obj:`int`): Optional. Duration of the audio in seconds. - performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio - tags. - title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. + performer (:obj:`str`): Optional. Performer of the audio as defined by the sender or by + audio tags. + title (:obj:`str`): Optional. Title of the audio as defined by the sender or by audio tags. thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| .. versionadded:: 20.2 diff --git a/telegram/_files/location.py b/telegram/_files/location.py index 45401868720..b2e1458d17f 100644 --- a/telegram/_files/location.py +++ b/telegram/_files/location.py @@ -32,8 +32,8 @@ class Location(TelegramObject): considered equal, if their :attr:`longitude` and :attr:`latitude` are equal. Args: - longitude (:obj:`float`): Longitude as defined by sender. - latitude (:obj:`float`): Latitude as defined by sender. + longitude (:obj:`float`): Longitude as defined by the sender. + latitude (:obj:`float`): Latitude as defined by the sender. horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`. live_period (:obj:`int`, optional): Time relative to the message sending date, during which @@ -45,8 +45,8 @@ class Location(TelegramObject): approaching another chat member, in meters. For sent live locations only. Attributes: - longitude (:obj:`float`): Longitude as defined by sender. - latitude (:obj:`float`): Latitude as defined by sender. + longitude (:obj:`float`): Longitude as defined by the sender. + latitude (:obj:`float`): Latitude as defined by the sender. horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location, measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`. live_period (:obj:`int`): Optional. Time relative to the message sending date, during which diff --git a/telegram/_files/video.py b/telegram/_files/video.py index d28138c75e1..7a1201c431e 100644 --- a/telegram/_files/video.py +++ b/telegram/_files/video.py @@ -39,11 +39,11 @@ class Video(_BaseThumbedMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - file_name (:obj:`str`, optional): Original filename as defined by sender. - mime_type (:obj:`str`, optional): MIME type of a file as defined by sender. + width (:obj:`int`): Video width as defined by the sender. + height (:obj:`int`): Video height as defined by the sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. + file_name (:obj:`str`, optional): Original filename as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of a file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail. @@ -55,11 +55,11 @@ class Video(_BaseThumbedMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - width (:obj:`int`): Video width as defined by sender. - height (:obj:`int`): Video height as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. - file_name (:obj:`str`): Optional. Original filename as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of a file as defined by sender. + width (:obj:`int`): Video width as defined by the sender. + height (:obj:`int`): Video height as defined by the sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. + file_name (:obj:`str`): Optional. Original filename as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of a file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail. diff --git a/telegram/_files/videonote.py b/telegram/_files/videonote.py index 2a1f760c1d5..15b23a69bf2 100644 --- a/telegram/_files/videonote.py +++ b/telegram/_files/videonote.py @@ -42,7 +42,7 @@ class VideoNote(_BaseThumbedMedium): Can't be used to download or reuse the file. length (:obj:`int`): Video width and height (diameter of the video message) as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail. @@ -56,7 +56,7 @@ class VideoNote(_BaseThumbedMedium): Can't be used to download or reuse the file. length (:obj:`int`): Video width and height (diameter of the video message) as defined by sender. - duration (:obj:`int`): Duration of the video in seconds as defined by sender. + duration (:obj:`int`): Duration of the video in seconds as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail. diff --git a/telegram/_files/voice.py b/telegram/_files/voice.py index 6c1f4dfb289..ae4fa1d6195 100644 --- a/telegram/_files/voice.py +++ b/telegram/_files/voice.py @@ -35,8 +35,8 @@ class Voice(_BaseMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. + duration (:obj:`int`): Duration of the audio in seconds as defined by the sender. + mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender. file_size (:obj:`int`, optional): File size in bytes. Attributes: @@ -45,8 +45,8 @@ class Voice(_BaseMedium): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - duration (:obj:`int`): Duration of the audio in seconds as defined by sender. - mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. + duration (:obj:`int`): Duration of the audio in seconds as defined by the sender. + mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender. file_size (:obj:`int`): Optional. File size in bytes. """ diff --git a/telegram/_menubutton.py b/telegram/_menubutton.py index 5856fc8d10e..ef89d8a53eb 100644 --- a/telegram/_menubutton.py +++ b/telegram/_menubutton.py @@ -145,7 +145,10 @@ class MenuButtonWebApp(MenuButton): web_app (:class:`telegram.WebAppInfo`): Description of the Web App that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method :meth:`~telegram.Bot.answerWebAppQuery` - of :class:`~telegram.Bot`. + of :class:`~telegram.Bot`. Alternatively, a ``t.me`` link to a Web App of the bot can + be specified in the object instead of the Web App's URL, in which case the Web App + will be opened as if the user pressed the link. + Attributes: type (:obj:`str`): :tg-const:`telegram.constants.MenuButtonType.WEB_APP`. @@ -153,7 +156,9 @@ class MenuButtonWebApp(MenuButton): web_app (:class:`telegram.WebAppInfo`): Description of the Web App that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method :meth:`~telegram.Bot.answerWebAppQuery` - of :class:`~telegram.Bot`. + of :class:`~telegram.Bot`. Alternatively, a ``t.me`` link to a Web App of the bot can + be specified in the object instead of the Web App's URL, in which case the Web App + will be opened as if the user pressed the link. """ __slots__ = ("text", "web_app") diff --git a/telegram/_message.py b/telegram/_message.py index f5626279a93..b52b2bc9b48 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -52,6 +52,7 @@ from telegram._linkpreviewoptions import LinkPreviewOptions from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from telegram._messageentity import MessageEntity +from telegram._paidmedia import PaidMediaInfo from telegram._passport.passportdata import PassportData from telegram._payment.invoice import Invoice from telegram._payment.successfulpayment import SuccessfulPayment @@ -380,7 +381,8 @@ class Message(MaybeInaccessibleMessage): .. versionchanged:: 20.0 |sequenceclassargs| - caption (:obj:`str`, optional): Caption for the animation, audio, document, photo, video + caption (:obj:`str`, optional): Caption for the animation, audio, document, paid media, + photo, video or voice, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. contact (:class:`telegram.Contact`, optional): Message is a shared contact, information about the contact. @@ -571,6 +573,10 @@ class Message(MaybeInaccessibleMessage): background set. .. versionadded:: 21.2 + paid_media (:obj:`telegram.PaidMediaInfo`, optional): Message contains paid media; + information about the paid media. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -692,7 +698,8 @@ class Message(MaybeInaccessibleMessage): .. versionchanged:: 20.0 |tupleclassattrs| - caption (:obj:`str`): Optional. Caption for the animation, audio, document, photo, video + caption (:obj:`str`): Optional. Caption for the animation, audio, document, paid media, + photo, video or voice, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. contact (:class:`telegram.Contact`): Optional. Message is a shared contact, information about the contact. @@ -884,6 +891,10 @@ class Message(MaybeInaccessibleMessage): background set .. versionadded:: 21.2 + paid_media (:obj:`telegram.PaidMediaInfo`): Optional. Message contains paid media; + information about the paid media. + + .. versionadded:: NEXT.VERSION .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a @@ -950,6 +961,7 @@ class Message(MaybeInaccessibleMessage): "new_chat_members", "new_chat_photo", "new_chat_title", + "paid_media", "passport_data", "photo", "pinned_message", @@ -1067,6 +1079,7 @@ def __init__( chat_background_set: Optional[ChatBackground] = None, effect_id: Optional[str] = None, show_caption_above_media: Optional[bool] = None, + paid_media: Optional[PaidMediaInfo] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1168,6 +1181,7 @@ def __init__( self.chat_background_set: Optional[ChatBackground] = chat_background_set self.effect_id: Optional[str] = effect_id self.show_caption_above_media: Optional[bool] = show_caption_above_media + self.paid_media: Optional[PaidMediaInfo] = paid_media self._effective_attachment = DEFAULT_NONE @@ -1283,6 +1297,7 @@ def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optio data["users_shared"] = UsersShared.de_json(data.get("users_shared"), bot) data["chat_shared"] = ChatShared.de_json(data.get("chat_shared"), bot) data["chat_background_set"] = ChatBackground.de_json(data.get("chat_background_set"), bot) + data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot) # Unfortunately, this needs to be here due to cyclic imports from telegram._giveaway import ( # pylint: disable=import-outside-toplevel @@ -1346,6 +1361,7 @@ def effective_attachment( Location, PassportData, Sequence[PhotoSize], + PaidMediaInfo, Poll, Sticker, Story, @@ -1369,6 +1385,7 @@ def effective_attachment( * :class:`telegram.Location` * :class:`telegram.PassportData` * List[:class:`telegram.PhotoSize`] + * :class:`telegram.PaidMediaInfo` * :class:`telegram.Poll` * :class:`telegram.Sticker` * :class:`telegram.Story` @@ -1386,6 +1403,9 @@ def effective_attachment( :attr:`dice`, :attr:`passport_data` and :attr:`poll` are now also considered to be an attachment. + .. versionchanged:: NEXT.VERSION + :attr:`paid_media` is now also considered to be an attachment. + """ if not isinstance(self._effective_attachment, DefaultValue): return self._effective_attachment diff --git a/telegram/_paidmedia.py b/telegram/_paidmedia.py new file mode 100644 index 00000000000..82594ada337 --- /dev/null +++ b/telegram/_paidmedia.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# 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 objects that represent paid media in Telegram.""" + +from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type + +from telegram import constants +from telegram._files.photosize import PhotoSize +from telegram._files.video import Video +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class PaidMedia(TelegramObject): + """Describes the paid media added to a message. Currently, it can be one of: + + * :class:`telegram.PaidMediaPreview` + * :class:`telegram.PaidMediaPhoto` + * :class:`telegram.PaidMediaVideo` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the paid media. + + Attributes: + type (:obj:`str`): Type of the paid media. + """ + + __slots__ = ("type",) + + PREVIEW: Final[str] = constants.PaidMediaType.PREVIEW + """:const:`telegram.constants.PaidMediaType.PREVIEW`""" + PHOTO: Final[str] = constants.PaidMediaType.PHOTO + """:const:`telegram.constants.PaidMediaType.PHOTO`""" + VIDEO: Final[str] = constants.PaidMediaType.VIDEO + """:const:`telegram.constants.PaidMediaType.VIDEO`""" + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.PaidMediaType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMedia"]: + """Converts JSON data to the appropriate :class:`PaidMedia` object, i.e. takes + care of selecting the correct subclass. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`, optional): The bot associated with this object. + + Returns: + The Telegram object. + + """ + data = cls._parse_data(data) + + if data is None: + return None + + if not data and cls is PaidMedia: + return None + + _class_mapping: Dict[str, Type[PaidMedia]] = { + cls.PREVIEW: PaidMediaPreview, + cls.PHOTO: PaidMediaPhoto, + cls.VIDEO: PaidMediaVideo, + } + + if cls is PaidMedia and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) + + return super().de_json(data=data, bot=bot) + + +class PaidMediaPreview(PaidMedia): + """The paid media isn't available before the payment. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`width`, :attr:`height`, and :attr:`duration` + are equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`. + width (:obj:`int`, optional): Media width as defined by the sender. + height (:obj:`int`, optional): Media height as defined by the sender. + duration (:obj:`int`, optional): Duration of the media in seconds as defined by the sender. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`. + width (:obj:`int`): Optional. Media width as defined by the sender. + height (:obj:`int`): Optional. Media height as defined by the sender. + duration (:obj:`int`): Optional. Duration of the media in seconds as defined by the sender. + """ + + __slots__ = ("duration", "height", "width") + + def __init__( + self, + width: Optional[int] = None, + height: Optional[int] = None, + duration: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.PREVIEW, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.width: Optional[int] = width + self.height: Optional[int] = height + self.duration: Optional[int] = duration + + self._id_attrs = (self.type, self.width, self.height, self.duration) + + +class PaidMediaPhoto(PaidMedia): + """ + The paid media is a photo. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`photo` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PHOTO`. + photo (Sequence[:class:`telegram.PhotoSize`]): The photo. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PHOTO`. + photo (Tuple[:class:`telegram.PhotoSize`]): The photo. + """ + + __slots__ = ("photo",) + + def __init__( + self, + photo: Sequence["PhotoSize"], + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.PHOTO, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.photo: Tuple[PhotoSize, ...] = parse_sequence_arg(photo) + + self._id_attrs = (self.type, self.photo) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaPhoto"]: + data = cls._parse_data(data) + + if not data: + return None + + data["photo"] = PhotoSize.de_list(data.get("photo"), bot=bot) + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class PaidMediaVideo(PaidMedia): + """ + The paid media is a video. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`video` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.VIDEO`. + video (:class:`telegram.Video`): The video. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.VIDEO`. + video (:class:`telegram.Video`): The video. + """ + + __slots__ = ("video",) + + def __init__( + self, + video: Video, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.VIDEO, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.video: Video = video + + self._id_attrs = (self.type, self.video) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaVideo"]: + data = cls._parse_data(data) + + if not data: + return None + + data["video"] = Video.de_json(data.get("video"), bot=bot) + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class PaidMediaInfo(TelegramObject): + """ + Describes the paid media added to a message. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`star_count` and :attr:`paid_media` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to + the media. + paid_media (Sequence[:class:`telegram.PaidMedia`]): Information about the paid media. + + Attributes: + star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to + the media. + paid_media (Tuple[:class:`telegram.PaidMedia`]): Information about the paid media. + """ + + __slots__ = ("paid_media", "star_count") + + def __init__( + self, + star_count: int, + paid_media: Sequence[PaidMedia], + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.star_count: int = star_count + self.paid_media: Tuple[PaidMedia, ...] = parse_sequence_arg(paid_media) + + self._id_attrs = (self.star_count, self.paid_media) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaInfo"]: + data = cls._parse_data(data) + + if not data: + return None + + data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot) + return super().de_json(data=data, bot=bot) diff --git a/telegram/_payment/precheckoutquery.py b/telegram/_payment/precheckoutquery.py index 1e7dca7bf33..30ae30be797 100644 --- a/telegram/_payment/precheckoutquery.py +++ b/telegram/_payment/precheckoutquery.py @@ -50,7 +50,7 @@ class PreCheckoutQuery(TelegramObject): `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user. @@ -66,7 +66,7 @@ class PreCheckoutQuery(TelegramObject): `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. diff --git a/telegram/_payment/shippingquery.py b/telegram/_payment/shippingquery.py index 47a62192489..cf81b4ecfa6 100644 --- a/telegram/_payment/shippingquery.py +++ b/telegram/_payment/shippingquery.py @@ -43,13 +43,13 @@ class ShippingQuery(TelegramObject): Args: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address. Attributes: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address. diff --git a/telegram/_stars.py b/telegram/_payment/stars.py similarity index 93% rename from telegram/_stars.py rename to telegram/_payment/stars.py index 8cb6ac1311f..6c537532c37 100644 --- a/telegram/_stars.py +++ b/telegram/_payment/stars.py @@ -183,8 +183,9 @@ class TransactionPartner(TelegramObject): """This object describes the source of a transaction, or its recipient for outgoing transactions. Currently, it can be one of: - * :class:`TransactionPartnerFragment` * :class:`TransactionPartnerUser` + * :class:`TransactionPartnerFragment` + * :class:`TransactionPartnerTelegramAds` * :class:`TransactionPartnerOther` Objects of this class are comparable in terms of equality. Two objects of this class are @@ -207,6 +208,8 @@ class TransactionPartner(TelegramObject): """:const:`telegram.constants.TransactionPartnerType.USER`""" OTHER: Final[str] = constants.TransactionPartnerType.OTHER """:const:`telegram.constants.TransactionPartnerType.OTHER`""" + TELEGRAM_ADS: Final[str] = constants.TransactionPartnerType.TELEGRAM_ADS + """:const:`telegram.constants.TransactionPartnerType.TELEGRAM_ADS`""" def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None: super().__init__(api_kwargs=api_kwargs) @@ -242,6 +245,7 @@ def de_json( cls.FRAGMENT: TransactionPartnerFragment, cls.USER: TransactionPartnerUser, cls.OTHER: TransactionPartnerOther, + cls.TELEGRAM_ADS: TransactionPartnerTelegramAds, } if cls is TransactionPartner and data.get("type") in _class_mapping: @@ -305,20 +309,29 @@ class TransactionPartnerUser(TransactionPartner): Args: user (:class:`telegram.User`): Information about the user. + invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. Attributes: type (:obj:`str`): The type of the transaction partner, always :tg-const:`telegram.TransactionPartner.USER`. user (:class:`telegram.User`): Information about the user. + invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. """ - __slots__ = ("user",) + __slots__ = ("invoice_payload", "user") - def __init__(self, user: "User", *, api_kwargs: Optional[JSONDict] = None) -> None: + def __init__( + self, + user: "User", + invoice_payload: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) with self._unfrozen(): self.user: User = user + self.invoice_payload: Optional[str] = invoice_payload self._id_attrs = ( self.type, self.user, @@ -355,6 +368,23 @@ def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: self._freeze() +class TransactionPartnerTelegramAds(TransactionPartner): + """Describes a withdrawal transaction to the Telegram Ads platform. + + .. versionadded:: NEXT.VERSION + + Attributes: + type (:obj:`str`): The type of the transaction partner, + always :tg-const:`telegram.TransactionPartner.TELEGRAM_ADS`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(type=TransactionPartner.TELEGRAM_ADS, api_kwargs=api_kwargs) + self._freeze() + + class StarTransaction(TelegramObject): """Describes a Telegram Star transaction. diff --git a/telegram/_payment/successfulpayment.py b/telegram/_payment/successfulpayment.py index 5298f66801e..90b1545b2d5 100644 --- a/telegram/_payment/successfulpayment.py +++ b/telegram/_payment/successfulpayment.py @@ -44,7 +44,7 @@ class SuccessfulPayment(TelegramObject): `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user. @@ -60,7 +60,7 @@ class SuccessfulPayment(TelegramObject): `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). - invoice_payload (:obj:`str`): Bot specified invoice payload. + invoice_payload (:obj:`str`): Bot-specified invoice payload. shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. diff --git a/telegram/_poll.py b/telegram/_poll.py index 01ec75ca5fa..8ea387a0950 100644 --- a/telegram/_poll.py +++ b/telegram/_poll.py @@ -38,7 +38,7 @@ class InputPollOption(TelegramObject): """ - This object contains information about one answer option in a poll to send. + This object contains information about one answer option in a poll to be sent. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`text` is equal. diff --git a/telegram/_reply.py b/telegram/_reply.py index 3ca342d067b..6c04847de64 100644 --- a/telegram/_reply.py +++ b/telegram/_reply.py @@ -37,6 +37,7 @@ from telegram._linkpreviewoptions import LinkPreviewOptions from telegram._messageentity import MessageEntity from telegram._messageorigin import MessageOrigin +from telegram._paidmedia import PaidMediaInfo from telegram._payment.invoice import Invoice from telegram._poll import Poll from telegram._story import Story @@ -101,6 +102,10 @@ class ExternalReplyInfo(TelegramObject): poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the poll. venue (:class:`telegram.Venue`, optional): Message is a venue, information about the venue. + paid_media (:class:`telegram.PaidMedia`, optional): Message contains paid media; + information about the paid media. + + .. versionadded:: NEXT.VERSION Attributes: origin (:class:`telegram.MessageOrigin`): Origin of the message replied to by the given @@ -144,6 +149,10 @@ class ExternalReplyInfo(TelegramObject): poll (:class:`telegram.Poll`): Optional. Message is a native poll, information about the poll. venue (:class:`telegram.Venue`): Optional. Message is a venue, information about the venue. + paid_media (:class:`telegram.PaidMedia`): Optional. Message contains paid media; + information about the paid media. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -162,6 +171,7 @@ class ExternalReplyInfo(TelegramObject): "location", "message_id", "origin", + "paid_media", "photo", "poll", "sticker", @@ -197,6 +207,7 @@ def __init__( location: Optional[Location] = None, poll: Optional[Poll] = None, venue: Optional[Venue] = None, + paid_media: Optional[PaidMediaInfo] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -225,6 +236,7 @@ def __init__( self.location: Optional[Location] = location self.poll: Optional[Poll] = poll self.venue: Optional[Venue] = venue + self.paid_media: Optional[PaidMediaInfo] = paid_media self._id_attrs = (self.origin,) @@ -263,6 +275,7 @@ def de_json( data["location"] = Location.de_json(data.get("location"), bot) data["poll"] = Poll.de_json(data.get("poll"), bot) data["venue"] = Venue.de_json(data.get("venue"), bot) + data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot) return super().de_json(data=data, bot=bot) diff --git a/telegram/constants.py b/telegram/constants.py index 5cd6e7ffc2c..d9b26b417c6 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -71,6 +71,7 @@ "InlineQueryResultType", "InlineQueryResultsButtonLimit", "InputMediaType", + "InputPaidMediaType", "InvoiceLimit", "KeyboardButtonRequestUsersLimit", "LocationLimit", @@ -82,6 +83,7 @@ "MessageLimit", "MessageOriginType", "MessageType", + "PaidMediaType", "ParseMode", "PollLimit", "PollType", @@ -149,7 +151,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=5) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=6) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -1259,6 +1261,21 @@ class InputMediaType(StringEnum): """:obj:`str`: Type of :class:`telegram.InputMediaVideo`.""" +class InputPaidMediaType(StringEnum): + """This enum contains the available types of :class:`telegram.InputPaidMedia`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + PHOTO = "photo" + """:obj:`str`: Type of :class:`telegram.InputMediaPhoto`.""" + VIDEO = "video" + """:obj:`str`: Type of :class:`telegram.InputMediaVideo`.""" + + class InlineQueryLimit(IntEnum): """This enum contains limitations for :class:`telegram.InlineQuery`/ :meth:`telegram.Bot.answer_inline_query`. The enum members of this enumeration are instances @@ -1602,6 +1619,11 @@ class MessageAttachmentType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.invoice`.""" LOCATION = "location" """:obj:`str`: Messages with :attr:`telegram.Message.location`.""" + PAID_MEDIA = "paid_media" + """:obj:`str`: Messages with :attr:`telegram.Message.paid_media`. + + .. versionadded:: NEXT.VERSION + """ PASSPORT_DATA = "passport_data" """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" PHOTO = "photo" @@ -1883,6 +1905,11 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.new_chat_title`.""" NEW_CHAT_PHOTO = "new_chat_photo" """:obj:`str`: Messages with :attr:`telegram.Message.new_chat_photo`.""" + PAID_MEDIA = "paid_media" + """:obj:`str`: Messages with :attr:`telegram.Message.paid_media`. + + .. versionadded:: NEXT.VERSION + """ PASSPORT_DATA = "passport_data" """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" PHOTO = "photo" @@ -1951,6 +1978,24 @@ class MessageType(StringEnum): """ +class PaidMediaType(StringEnum): + """ + This enum contains the available types of :class:`telegram.PaidMedia`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + PREVIEW = "preview" + """:obj:`str`: The type of :class:`telegram.PaidMediaPreview`.""" + VIDEO = "video" + """:obj:`str`: The type of :class:`telegram.PaidMediaVideo`.""" + PHOTO = "photo" + """:obj:`str`: The type of :class:`telegram.PaidMediaPhoto`.""" + + class PollingLimit(IntEnum): """This enum contains limitations for :paramref:`telegram.Bot.get_updates.limit`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. @@ -2490,6 +2535,8 @@ class TransactionPartnerType(StringEnum): """:obj:`str`: Transaction with a user.""" OTHER = "other" """:obj:`str`: Transaction with unknown source or recipient.""" + TELEGRAM_ADS = "telegram_ads" + """:obj:`str`: Transaction with Telegram Ads.""" class ParseMode(StringEnum): diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 3cd4ab389e7..7d8d10e4902 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -107,6 +107,7 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputPaidMedia, InputSticker, LabeledPrice, MessageEntity, @@ -4216,6 +4217,50 @@ async def get_star_transactions( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def send_paid_media( + self, + chat_id: Union[str, int], + star_count: int, + media: Sequence["InputPaidMedia"], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional[ReplyMarkup] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> Message: + return await super().send_paid_media( + chat_id=chat_id, + star_count=star_count, + media=media, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, + disable_notification=disable_notification, + protect_content=protect_content, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message @@ -4339,3 +4384,4 @@ async def get_star_transactions( replaceStickerInSet = replace_sticker_in_set refundStarPayment = refund_star_payment getStarTransactions = get_star_transactions + sendPaidMedia = send_paid_media diff --git a/telegram/request/_requestparameter.py b/telegram/request/_requestparameter.py index 6b16a5cae66..2e14a8be6ba 100644 --- a/telegram/request/_requestparameter.py +++ b/telegram/request/_requestparameter.py @@ -23,7 +23,7 @@ from typing import List, Optional, Sequence, Tuple, final from telegram._files.inputfile import InputFile -from telegram._files.inputmedia import InputMedia +from telegram._files.inputmedia import InputMedia, InputPaidMedia from telegram._files.inputsticker import InputSticker from telegram._telegramobject import TelegramObject from telegram._utils.datetime import to_timestamp @@ -117,7 +117,7 @@ def _value_and_input_files_from_input( # pylint: disable=too-many-return-statem return value.attach_uri, [value] return None, [value] - if isinstance(value, InputMedia) and isinstance(value.media, InputFile): + if isinstance(value, (InputMedia, InputPaidMedia)) and isinstance(value.media, InputFile): # We call to_dict and change the returned dict instead of overriding # value.media in case the same value is reused for another request data = value.to_dict() diff --git a/tests/_files/test_inputmedia.py b/tests/_files/test_inputmedia.py index cce7fdc07a5..d25a679ffc4 100644 --- a/tests/_files/test_inputmedia.py +++ b/tests/_files/test_inputmedia.py @@ -31,6 +31,8 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputPaidMediaPhoto, + InputPaidMediaVideo, Message, MessageEntity, ReplyParameters, @@ -134,6 +136,25 @@ def input_media_document(class_thumb_file): ) +@pytest.fixture(scope="module") +def input_paid_media_photo(): + return InputPaidMediaPhoto( + media=TestInputMediaPhotoBase.media, + ) + + +@pytest.fixture(scope="module") +def input_paid_media_video(class_thumb_file): + return InputPaidMediaVideo( + media=TestInputMediaVideoBase.media, + thumbnail=class_thumb_file, + width=TestInputMediaVideoBase.width, + height=TestInputMediaVideoBase.height, + duration=TestInputMediaVideoBase.duration, + supports_streaming=TestInputMediaVideoBase.supports_streaming, + ) + + class TestInputMediaVideoBase: type_ = "video" media = "NOTAREALFILEID" @@ -514,6 +535,91 @@ def test_with_local_files(self): assert input_media_document.thumbnail == data_file("telegram.jpg").as_uri() +class TestInputPaidMediaPhotoWithoutRequest(TestInputMediaPhotoBase): + def test_slot_behaviour(self, input_paid_media_photo): + inst = input_paid_media_photo + 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_expected_values(self, input_paid_media_photo): + assert input_paid_media_photo.type == self.type_ + assert input_paid_media_photo.media == self.media + + def test_to_dict(self, input_paid_media_photo): + input_paid_media_photo_dict = input_paid_media_photo.to_dict() + assert input_paid_media_photo_dict["type"] == input_paid_media_photo.type + assert input_paid_media_photo_dict["media"] == input_paid_media_photo.media + + def test_with_photo(self, photo): # noqa: F811 + # fixture found in test_photo + input_paid_media_photo = InputPaidMediaPhoto(photo) + assert input_paid_media_photo.type == self.type_ + assert input_paid_media_photo.media == photo.file_id + + def test_with_photo_file(self, photo_file): # noqa: F811 + # fixture found in test_photo + input_paid_media_photo = InputPaidMediaPhoto(photo_file) + assert input_paid_media_photo.type == self.type_ + assert isinstance(input_paid_media_photo.media, InputFile) + + def test_with_local_files(self): + input_paid_media_photo = InputPaidMediaPhoto(data_file("telegram.jpg")) + assert input_paid_media_photo.media == data_file("telegram.jpg").as_uri() + + +class TestInputPaidMediaVideoWithoutRequest(TestInputMediaVideoBase): + def test_slot_behaviour(self, input_paid_media_video): + inst = input_paid_media_video + 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_expected_values(self, input_paid_media_video): + assert input_paid_media_video.type == self.type_ + assert input_paid_media_video.media == self.media + assert input_paid_media_video.width == self.width + assert input_paid_media_video.height == self.height + 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) + + def test_to_dict(self, input_paid_media_video): + input_paid_media_video_dict = input_paid_media_video.to_dict() + assert input_paid_media_video_dict["type"] == input_paid_media_video.type + assert input_paid_media_video_dict["media"] == input_paid_media_video.media + assert input_paid_media_video_dict["width"] == input_paid_media_video.width + assert input_paid_media_video_dict["height"] == input_paid_media_video.height + assert input_paid_media_video_dict["duration"] == input_paid_media_video.duration + assert ( + input_paid_media_video_dict["supports_streaming"] + == input_paid_media_video.supports_streaming + ) + assert input_paid_media_video_dict["thumbnail"] == input_paid_media_video.thumbnail + + def test_with_video(self, video): # noqa: F811 + # fixture found in test_video + input_paid_media_video = InputPaidMediaVideo(video) + assert input_paid_media_video.type == self.type_ + assert input_paid_media_video.media == video.file_id + assert input_paid_media_video.width == video.width + assert input_paid_media_video.height == video.height + assert input_paid_media_video.duration == video.duration + + def test_with_video_file(self, video_file): # noqa: F811 + # fixture found in test_video + input_paid_media_video = InputPaidMediaVideo(video_file) + assert input_paid_media_video.type == self.type_ + assert isinstance(input_paid_media_video.media, InputFile) + + def test_with_local_files(self): + input_paid_media_video = InputPaidMediaVideo( + 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() + + @pytest.fixture(scope="module") def media_group(photo, thumb): # noqa: F811 return [ @@ -1044,3 +1150,20 @@ def build_media(parse_mode, med_type): assert message.caption_entities == () # make sure that the media was not modified assert media.parse_mode == copied_media.parse_mode + + async def test_send_paid_media(self, bot, channel_id, photo_file, video_file): # noqa: F811 + msg = await bot.send_paid_media( + chat_id=channel_id, + star_count=20, + media=[ + InputPaidMediaPhoto(media=photo_file), + InputPaidMediaVideo(media=video_file), + ], + caption="bye onlyfans", + show_caption_above_media=True, + ) + + assert isinstance(msg, Message) + assert msg.caption == "bye onlyfans" + assert msg.show_caption_above_media + assert msg.paid_media.star_count == 20 diff --git a/tests/test_chat.py b/tests/test_chat.py index a11b40c647b..682bdbe514a 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1244,6 +1244,22 @@ async def make_assertion(*_, **kwargs): 123, [ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN)], True ) + async def test_instance_method_send_paid_media(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["media"] == "media" + and kwargs["star_count"] == 42 + and kwargs["caption"] == "stars" + ) + + assert check_shortcut_signature(Chat.send_paid_media, Bot.send_paid_media, ["chat_id"], []) + assert await check_shortcut_call(chat.send_paid_media, chat.get_bot(), "send_paid_media") + assert await check_defaults_handling(chat.send_paid_media, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_paid_media", make_assertion) + assert await chat.send_paid_media(media="media", star_count=42, caption="stars") + def test_mention_html(self): chat = Chat(id=1, type="foo") with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): diff --git a/tests/test_chatfullinfo.py b/tests/test_chatfullinfo.py index b547e4de913..ee9d697ca71 100644 --- a/tests/test_chatfullinfo.py +++ b/tests/test_chatfullinfo.py @@ -55,13 +55,15 @@ def chat_full_info(bot): bio=TestChatFullInfoBase.bio, linked_chat_id=TestChatFullInfoBase.linked_chat_id, location=TestChatFullInfoBase.location, - has_private_forwards=True, - has_protected_content=True, - has_visible_history=True, - join_to_send_messages=True, - join_by_request=True, - has_restricted_voice_and_video_messages=True, - is_forum=True, + has_private_forwards=TestChatFullInfoBase.has_private_forwards, + has_protected_content=TestChatFullInfoBase.has_protected_content, + has_visible_history=TestChatFullInfoBase.has_visible_history, + join_to_send_messages=TestChatFullInfoBase.join_to_send_messages, + join_by_request=TestChatFullInfoBase.join_by_request, + has_restricted_voice_and_video_messages=( + TestChatFullInfoBase.has_restricted_voice_and_video_messages + ), + is_forum=TestChatFullInfoBase.is_forum, active_usernames=TestChatFullInfoBase.active_usernames, emoji_status_custom_emoji_id=TestChatFullInfoBase.emoji_status_custom_emoji_id, emoji_status_expiration_date=TestChatFullInfoBase.emoji_status_expiration_date, @@ -76,10 +78,11 @@ def chat_full_info(bot): business_intro=TestChatFullInfoBase.business_intro, business_location=TestChatFullInfoBase.business_location, business_opening_hours=TestChatFullInfoBase.business_opening_hours, - birthdate=Birthdate(1, 1), + birthdate=TestChatFullInfoBase.birthdate, personal_chat=TestChatFullInfoBase.personal_chat, - first_name="first_name", - last_name="last_name", + first_name=TestChatFullInfoBase.first_name, + last_name=TestChatFullInfoBase.last_name, + can_send_paid_media=TestChatFullInfoBase.can_send_paid_media, ) chat.set_bot(bot) chat._unfreeze() @@ -136,6 +139,7 @@ class TestChatFullInfoBase: personal_chat = Chat(3, "private", "private") first_name = "first_name" last_name = "last_name" + can_send_paid_media = True class TestChatFullInfoWithoutRequest(TestChatFullInfoBase): @@ -188,6 +192,7 @@ def test_de_json(self, bot): "personal_chat": self.personal_chat.to_dict(), "first_name": self.first_name, "last_name": self.last_name, + "can_send_paid_media": self.can_send_paid_media, } cfi = ChatFullInfo.de_json(json_dict, bot) assert cfi.id == self.id_ @@ -232,6 +237,7 @@ def test_de_json(self, bot): assert cfi.first_name == self.first_name assert cfi.last_name == self.last_name assert cfi.max_reaction_count == self.max_reaction_count + assert cfi.can_send_paid_media == self.can_send_paid_media def test_de_json_localization(self, bot, raw_bot, tz_bot): json_dict = { @@ -305,6 +311,7 @@ def test_to_dict(self, chat_full_info): assert cfi_dict["personal_chat"] == cfi.personal_chat.to_dict() assert cfi_dict["first_name"] == cfi.first_name assert cfi_dict["last_name"] == cfi.last_name + assert cfi_dict["can_send_paid_media"] == cfi.can_send_paid_media assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count diff --git a/tests/test_constants.py b/tests/test_constants.py index 75368857325..b750f7fba3a 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -216,6 +216,8 @@ def test_message_attachment_type_completeness_reverse(self): name = to_snake_case(match.group(1)) if name == "photo_size": name = "photo" + if name == "paid_media_info": + name = "paid_media" try: constants.MessageAttachmentType(name) except ValueError: diff --git a/tests/test_message.py b/tests/test_message.py index 8bfc632769d..5596710396d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -46,6 +46,8 @@ MessageAutoDeleteTimerChanged, MessageEntity, MessageOriginChat, + PaidMediaInfo, + PaidMediaPreview, PassportData, PhotoSize, Poll, @@ -275,6 +277,7 @@ def message(bot): {"chat_background_set": ChatBackground(type=BackgroundTypeChatTheme("ice"))}, {"effect_id": "123456789"}, {"show_caption_above_media": True}, + {"paid_media": PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)])}, ], ids=[ "reply", @@ -346,6 +349,7 @@ def message(bot): "chat_background_set", "effect_id", "show_caption_above_media", + "paid_media", ], ) def message_params(bot, request): @@ -1221,6 +1225,7 @@ def test_effective_attachment(self, message_params): "game", "invoice", "location", + "paid_media", "passport_data", "photo", "poll", diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 99df02b82e7..c9e3b4e4650 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -127,6 +127,8 @@ class ParamTypeCheckingExceptions: "InputTextMessageContent": {"disable_web_page_preview"}, # convenience arg, here for bw compat "RevenueWithdrawalState": {"type"}, # attributes common to all subclasses "TransactionPartner": {"type"}, # attributes common to all subclasses + "PaidMedia": {"type"}, # attributes common to all subclasses + "InputPaidMedia": {"type", "media"}, # attributes common to all subclasses } @@ -153,6 +155,8 @@ def ptb_extra_params(object_name: str) -> set[str]: r"BackgroundFill\w+": {"type"}, r"RevenueWithdrawalState\w+": {"type"}, r"TransactionPartner\w+": {"type"}, + r"PaidMedia\w+": {"type"}, + r"InputPaidMedia\w+": {"type"}, } diff --git a/tests/test_paidmedia.py b/tests/test_paidmedia.py new file mode 100644 index 00000000000..f76bcf6310f --- /dev/null +++ b/tests/test_paidmedia.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# 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/]. + +from copy import deepcopy + +import pytest + +from telegram import ( + Dice, + PaidMedia, + PaidMediaInfo, + PaidMediaPhoto, + PaidMediaPreview, + PaidMediaVideo, + PhotoSize, + Video, +) +from telegram.constants import PaidMediaType +from tests.auxil.slots import mro_slots + + +@pytest.fixture( + scope="module", + params=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + PaidMediaPreview, + PaidMediaPhoto, + PaidMediaVideo, + ], + ids=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + ( + PaidMediaPreview, + PaidMedia.PREVIEW, + ), + ( + PaidMediaPhoto, + PaidMedia.PHOTO, + ), + ( + PaidMediaVideo, + PaidMedia.VIDEO, + ), + ], + ids=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def paid_media(pm_scope_class_and_type): + # We use de_json here so that we don't have to worry about which class gets which arguments + return pm_scope_class_and_type[0].de_json( + { + "type": pm_scope_class_and_type[1], + "width": TestPaidMediaBase.width, + "height": TestPaidMediaBase.height, + "duration": TestPaidMediaBase.duration, + "video": TestPaidMediaBase.video.to_dict(), + "photo": [p.to_dict() for p in TestPaidMediaBase.photo], + }, + bot=None, + ) + + +def paid_media_video(): + return PaidMediaVideo(video=TestPaidMediaBase.video) + + +def paid_media_photo(): + return PaidMediaPhoto(photo=TestPaidMediaBase.photo) + + +@pytest.fixture(scope="module") +def paid_media_info(): + return PaidMediaInfo( + star_count=TestPaidMediaInfoBase.star_count, + paid_media=[paid_media_video(), paid_media_photo()], + ) + + +class TestPaidMediaBase: + width = 640 + height = 480 + duration = 60 + video = Video( + file_id="video_file_id", + width=640, + height=480, + file_unique_id="file_unique_id", + duration=60, + ) + photo = ( + PhotoSize( + file_id="photo_file_id", + width=640, + height=480, + file_unique_id="file_unique_id", + ), + ) + + +class TestPaidMediaWithoutRequest(TestPaidMediaBase): + def test_slot_behaviour(self, paid_media): + inst = paid_media + 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, bot, pm_scope_class_and_type): + cls = pm_scope_class_and_type[0] + type_ = pm_scope_class_and_type[1] + + json_dict = { + "type": type_, + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + pm = PaidMedia.de_json(json_dict, bot) + assert set(pm.api_kwargs.keys()) == { + "width", + "height", + "duration", + "video", + "photo", + } - set(cls.__slots__) + + assert isinstance(pm, PaidMedia) + assert type(pm) is cls + assert pm.type == type_ + if "width" in cls.__slots__: + assert pm.width == self.width + assert pm.height == self.height + assert pm.duration == self.duration + if "video" in cls.__slots__: + assert pm.video == self.video + if "photo" in cls.__slots__: + assert pm.photo == self.photo + + assert cls.de_json(None, bot) is None + assert PaidMedia.de_json({}, bot) is None + + def test_de_json_invalid_type(self, bot): + json_dict = { + "type": "invalid", + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + pm = PaidMedia.de_json(json_dict, bot) + assert pm.api_kwargs == { + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + + assert type(pm) is PaidMedia + assert pm.type == "invalid" + + def test_de_json_subclass(self, pm_scope_class, bot): + """This makes sure that e.g. PaidMediaPreivew(data) never returns a + TransactionPartnerPhoto instance.""" + json_dict = { + "type": "invalid", + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + assert type(pm_scope_class.de_json(json_dict, bot)) is pm_scope_class + + def test_to_dict(self, paid_media): + pm_dict = paid_media.to_dict() + + assert isinstance(pm_dict, dict) + assert pm_dict["type"] == paid_media.type + if hasattr(paid_media_info, "width"): + assert pm_dict["width"] == paid_media.width + assert pm_dict["height"] == paid_media.height + assert pm_dict["duration"] == paid_media.duration + if hasattr(paid_media_info, "video"): + assert pm_dict["video"] == paid_media.video.to_dict() + if hasattr(paid_media_info, "photo"): + assert pm_dict["photo"] == [p.to_dict() for p in paid_media.photo] + + def test_type_enum_conversion(self): + assert type(PaidMedia("video").type) is PaidMediaType + assert PaidMedia("unknown").type == "unknown" + + def test_equality(self, paid_media, bot): + a = PaidMedia("base_type") + b = PaidMedia("base_type") + c = paid_media + d = deepcopy(paid_media) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "video"): + json_dict = c.to_dict() + json_dict["video"] = Video("different", "d2", 1, 1, 1).to_dict() + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + if hasattr(c, "photo"): + json_dict = c.to_dict() + json_dict["photo"] = [PhotoSize("different", "d2", 1, 1, 1).to_dict()] + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + +class TestPaidMediaInfoBase: + star_count = 200 + paid_media = [paid_media_video(), paid_media_photo()] + + +class TestPaidMediaInfoWithoutRequest(TestPaidMediaInfoBase): + def test_slot_behaviour(self, paid_media_info): + inst = paid_media_info + 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, bot): + json_dict = { + "star_count": self.star_count, + "paid_media": [t.to_dict() for t in self.paid_media], + } + pmi = PaidMediaInfo.de_json(json_dict, bot) + pmi_none = PaidMediaInfo.de_json(None, bot) + assert pmi.paid_media == tuple(self.paid_media) + assert pmi.star_count == self.star_count + assert pmi_none is None + + def test_to_dict(self, paid_media_info): + assert paid_media_info.to_dict() == { + "star_count": self.star_count, + "paid_media": [t.to_dict() for t in self.paid_media], + } + + def test_equality(self): + pmi1 = PaidMediaInfo( + star_count=self.star_count, paid_media=[paid_media_video(), paid_media_photo()] + ) + pmi2 = PaidMediaInfo( + star_count=self.star_count, paid_media=[paid_media_video(), paid_media_photo()] + ) + pmi3 = PaidMediaInfo(star_count=100, paid_media=[paid_media_photo()]) + + assert pmi1 == pmi2 + assert hash(pmi1) == hash(pmi2) + + assert pmi1 != pmi3 + assert hash(pmi1) != hash(pmi3) diff --git a/tests/test_reply.py b/tests/test_reply.py index 4d2c35e8d31..f41ed01eb59 100644 --- a/tests/test_reply.py +++ b/tests/test_reply.py @@ -29,6 +29,8 @@ LinkPreviewOptions, MessageEntity, MessageOriginUser, + PaidMediaInfo, + PaidMediaPreview, ReplyParameters, TextQuote, User, @@ -44,6 +46,7 @@ def external_reply_info(): message_id=TestExternalReplyInfoBase.message_id, link_preview_options=TestExternalReplyInfoBase.link_preview_options, giveaway=TestExternalReplyInfoBase.giveaway, + paid_media=TestExternalReplyInfoBase.paid_media, ) @@ -59,6 +62,7 @@ class TestExternalReplyInfoBase: dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0), 1, ) + paid_media = PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)]) class TestExternalReplyInfoWithoutRequest(TestExternalReplyInfoBase): @@ -76,6 +80,7 @@ def test_de_json(self, bot): "message_id": self.message_id, "link_preview_options": self.link_preview_options.to_dict(), "giveaway": self.giveaway.to_dict(), + "paid_media": self.paid_media.to_dict(), } external_reply_info = ExternalReplyInfo.de_json(json_dict, bot) @@ -86,6 +91,7 @@ def test_de_json(self, bot): assert external_reply_info.message_id == self.message_id assert external_reply_info.link_preview_options == self.link_preview_options assert external_reply_info.giveaway == self.giveaway + assert external_reply_info.paid_media == self.paid_media assert ExternalReplyInfo.de_json(None, bot) is None @@ -98,6 +104,7 @@ def test_to_dict(self, external_reply_info): assert ext_reply_info_dict["message_id"] == self.message_id assert ext_reply_info_dict["link_preview_options"] == self.link_preview_options.to_dict() assert ext_reply_info_dict["giveaway"] == self.giveaway.to_dict() + assert ext_reply_info_dict["paid_media"] == self.paid_media.to_dict() def test_equality(self, external_reply_info): a = external_reply_info diff --git a/tests/test_stars.py b/tests/test_stars.py index 25567b30cc0..fb1339a7217 100644 --- a/tests/test_stars.py +++ b/tests/test_stars.py @@ -33,6 +33,7 @@ TransactionPartner, TransactionPartnerFragment, TransactionPartnerOther, + TransactionPartnerTelegramAds, TransactionPartnerUser, User, ) @@ -101,6 +102,7 @@ def star_transactions(): TransactionPartner.FRAGMENT, TransactionPartner.OTHER, TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, ], ) def tp_scope_type(request): @@ -113,11 +115,13 @@ def tp_scope_type(request): TransactionPartnerFragment, TransactionPartnerOther, TransactionPartnerUser, + TransactionPartnerTelegramAds, ], ids=[ TransactionPartner.FRAGMENT, TransactionPartner.OTHER, TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, ], ) def tp_scope_class(request): @@ -130,11 +134,13 @@ def tp_scope_class(request): (TransactionPartnerFragment, TransactionPartner.FRAGMENT), (TransactionPartnerOther, TransactionPartner.OTHER), (TransactionPartnerUser, TransactionPartner.USER), + (TransactionPartnerTelegramAds, TransactionPartner.TELEGRAM_ADS), ], ids=[ TransactionPartner.FRAGMENT, TransactionPartner.OTHER, TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, ], ) def tp_scope_class_and_type(request): @@ -147,6 +153,7 @@ def transaction_partner(tp_scope_class_and_type): return tp_scope_class_and_type[0].de_json( { "type": tp_scope_class_and_type[1], + "invoice_payload": TestTransactionPartnerBase.invoice_payload, "withdrawal_state": TestTransactionPartnerBase.withdrawal_state.to_dict(), "user": TestTransactionPartnerBase.user.to_dict(), }, @@ -244,6 +251,7 @@ def test_de_json(self, bot): } st = StarTransaction.de_json(json_dict, bot) st_none = StarTransaction.de_json(None, bot) + assert st.api_kwargs == {} assert st.id == self.id assert st.amount == self.amount assert st.date == from_timestamp(self.date) @@ -329,6 +337,7 @@ def test_de_json(self, bot): } st = StarTransactions.de_json(json_dict, bot) st_none = StarTransactions.de_json(None, bot) + assert st.api_kwargs == {} assert st.transactions == tuple(self.transactions) assert st_none is None @@ -359,6 +368,7 @@ def test_equality(self): class TestTransactionPartnerBase: withdrawal_state = withdrawal_state_succeeded() user = transaction_partner_user().user + invoice_payload = "payload" class TestTransactionPartnerWithoutRequest(TestTransactionPartnerBase): @@ -374,11 +384,14 @@ def test_de_json(self, bot, tp_scope_class_and_type): json_dict = { "type": type_, + "invoice_payload": self.invoice_payload, "withdrawal_state": self.withdrawal_state.to_dict(), "user": self.user.to_dict(), } tp = TransactionPartner.de_json(json_dict, bot) - assert set(tp.api_kwargs.keys()) == {"user", "withdrawal_state"} - set(cls.__slots__) + assert set(tp.api_kwargs.keys()) == {"user", "withdrawal_state", "invoice_payload"} - set( + cls.__slots__ + ) assert isinstance(tp, TransactionPartner) assert type(tp) is cls @@ -387,6 +400,7 @@ def test_de_json(self, bot, tp_scope_class_and_type): assert tp.withdrawal_state == self.withdrawal_state if "user" in cls.__slots__: assert tp.user == self.user + assert tp.invoice_payload == self.invoice_payload assert cls.de_json(None, bot) is None assert TransactionPartner.de_json({}, bot) is None @@ -394,6 +408,7 @@ def test_de_json(self, bot, tp_scope_class_and_type): def test_de_json_invalid_type(self, bot): json_dict = { "type": "invalid", + "invoice_payload": self.invoice_payload, "withdrawal_state": self.withdrawal_state.to_dict(), "user": self.user.to_dict(), } @@ -401,6 +416,7 @@ def test_de_json_invalid_type(self, bot): assert tp.api_kwargs == { "withdrawal_state": self.withdrawal_state.to_dict(), "user": self.user.to_dict(), + "invoice_payload": self.invoice_payload, } assert type(tp) is TransactionPartner @@ -411,6 +427,7 @@ def test_de_json_subclass(self, tp_scope_class, bot): TransactionPartnerFragment instance.""" json_dict = { "type": "invalid", + "invoice_payload": self.invoice_payload, "withdrawal_state": self.withdrawal_state.to_dict(), "user": self.user.to_dict(), } @@ -423,6 +440,7 @@ def test_to_dict(self, transaction_partner): assert tp_dict["type"] == transaction_partner.type if hasattr(transaction_partner, "user"): assert tp_dict["user"] == transaction_partner.user.to_dict() + assert tp_dict["invoice_payload"] == transaction_partner.invoice_payload if hasattr(transaction_partner, "withdrawal_state"): assert tp_dict["withdrawal_state"] == transaction_partner.withdrawal_state.to_dict() From 52237cf00c4831ffac1f962e70ca6955112fe3ad Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 7 Jul 2024 16:23:31 -0400 Subject: [PATCH 17/26] Add `filters.PAID_MEDIA` (#4357) --- telegram/ext/filters.py | 15 +++++++++++++++ tests/ext/test_filters.py | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 5147574e07a..8182ac64996 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -58,6 +58,7 @@ "IS_FROM_OFFLINE", "IS_TOPIC_MESSAGE", "LOCATION", + "PAID_MEDIA", "PASSPORT_DATA", "PHOTO", "POLL", @@ -1706,6 +1707,20 @@ def filter(self, message: Message) -> bool: return any(self._check_mention(message, mention) for mention in self._mentions) +class _PaidMedia(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.paid_media) + + +PAID_MEDIA = _PaidMedia(name="filters.PAID_MEDIA") +"""Messages that contain :attr:`telegram.Message.paid_media`. + +.. versionadded:: NEXT.VERSION +""" + + class _PassportData(MessageFilter): __slots__ = () diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index 97d17e2ebaf..cc237f41001 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -902,6 +902,11 @@ def test_filters_story(self, update): update.message.story = "test" assert filters.STORY.check_update(update) + def test_filters_paid_media(self, update): + assert not filters.PAID_MEDIA.check_update(update) + update.message.paid_media = "test" + assert filters.PAID_MEDIA.check_update(update) + def test_filters_video(self, update): assert not filters.VIDEO.check_update(update) update.message.video = "test" From 71e4015e2271964ca9d2353adfa85928f4241d11 Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Tue, 9 Jul 2024 10:35:18 +0200 Subject: [PATCH 18/26] API 7.7 (#4356) Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- README.rst | 4 +- docs/source/telegram.payments-tree.rst | 1 + docs/source/telegram.refundedpayment.rst | 6 ++ telegram/__init__.py | 2 + telegram/_message.py | 13 +++ telegram/_payment/refundedpayment.py | 91 ++++++++++++++++++ telegram/constants.py | 7 +- telegram/ext/filters.py | 12 +++ tests/_payment/test_refundedpayment.py | 115 +++++++++++++++++++++++ tests/ext/test_filters.py | 5 + tests/test_message.py | 3 + 11 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 docs/source/telegram.refundedpayment.rst create mode 100644 telegram/_payment/refundedpayment.py create mode 100644 tests/_payment/test_refundedpayment.py diff --git a/README.rst b/README.rst index e8aecc8df93..d58e814c391 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.6-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.7-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -79,7 +79,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -All types and methods of the Telegram Bot API **7.6** are supported. +All types and methods of the Telegram Bot API **7.7** are supported. Installing ========== diff --git a/docs/source/telegram.payments-tree.rst b/docs/source/telegram.payments-tree.rst index 2a09c5415ac..0db0ba21959 100644 --- a/docs/source/telegram.payments-tree.rst +++ b/docs/source/telegram.payments-tree.rst @@ -8,6 +8,7 @@ Payments telegram.labeledprice telegram.orderinfo telegram.precheckoutquery + telegram.refundedpayment telegram.revenuewithdrawalstate telegram.revenuewithdrawalstatefailed telegram.revenuewithdrawalstatepending diff --git a/docs/source/telegram.refundedpayment.rst b/docs/source/telegram.refundedpayment.rst new file mode 100644 index 00000000000..f99349c859c --- /dev/null +++ b/docs/source/telegram.refundedpayment.rst @@ -0,0 +1,6 @@ +RefundedPayment +=============== + +.. autoclass:: telegram.RefundedPayment + :members: + :show-inheritance: diff --git a/telegram/__init__.py b/telegram/__init__.py index af2336a4ac9..5b52bf85c40 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -204,6 +204,7 @@ "ReactionType", "ReactionTypeCustomEmoji", "ReactionTypeEmoji", + "RefundedPayment", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "ReplyParameters", @@ -446,6 +447,7 @@ from ._payment.labeledprice import LabeledPrice from ._payment.orderinfo import OrderInfo from ._payment.precheckoutquery import PreCheckoutQuery +from ._payment.refundedpayment import RefundedPayment from ._payment.shippingaddress import ShippingAddress from ._payment.shippingoption import ShippingOption from ._payment.shippingquery import ShippingQuery diff --git a/telegram/_message.py b/telegram/_message.py index b52b2bc9b48..5c45b9582a4 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -55,6 +55,7 @@ from telegram._paidmedia import PaidMediaInfo from telegram._passport.passportdata import PassportData from telegram._payment.invoice import Invoice +from telegram._payment.refundedpayment import RefundedPayment from telegram._payment.successfulpayment import SuccessfulPayment from telegram._poll import Poll from telegram._proximityalerttriggered import ProximityAlertTriggered @@ -576,6 +577,10 @@ class Message(MaybeInaccessibleMessage): paid_media (:obj:`telegram.PaidMediaInfo`, optional): Message contains paid media; information about the paid media. + .. versionadded:: NEXT.VERSION + refunded_payment (:obj:`telegram.RefundedPayment`, optional): Message is a service message + about a refunded payment, information about the payment. + .. versionadded:: NEXT.VERSION Attributes: @@ -894,6 +899,10 @@ class Message(MaybeInaccessibleMessage): paid_media (:obj:`telegram.PaidMediaInfo`): Optional. Message contains paid media; information about the paid media. + .. versionadded:: NEXT.VERSION + refunded_payment (:obj:`telegram.RefundedPayment`): Optional. Message is a service message + about a refunded payment, information about the payment. + .. versionadded:: NEXT.VERSION .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by @@ -968,6 +977,7 @@ class Message(MaybeInaccessibleMessage): "poll", "proximity_alert_triggered", "quote", + "refunded_payment", "reply_markup", "reply_to_message", "reply_to_story", @@ -1080,6 +1090,7 @@ def __init__( effect_id: Optional[str] = None, show_caption_above_media: Optional[bool] = None, paid_media: Optional[PaidMediaInfo] = None, + refunded_payment: Optional[RefundedPayment] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1182,6 +1193,7 @@ def __init__( self.effect_id: Optional[str] = effect_id self.show_caption_above_media: Optional[bool] = show_caption_above_media self.paid_media: Optional[PaidMediaInfo] = paid_media + self.refunded_payment: Optional[RefundedPayment] = refunded_payment self._effective_attachment = DEFAULT_NONE @@ -1298,6 +1310,7 @@ def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optio data["chat_shared"] = ChatShared.de_json(data.get("chat_shared"), bot) data["chat_background_set"] = ChatBackground.de_json(data.get("chat_background_set"), bot) data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot) + data["refunded_payment"] = RefundedPayment.de_json(data.get("refunded_payment"), bot) # Unfortunately, this needs to be here due to cyclic imports from telegram._giveaway import ( # pylint: disable=import-outside-toplevel diff --git a/telegram/_payment/refundedpayment.py b/telegram/_payment/refundedpayment.py new file mode 100644 index 00000000000..19bdfe84649 --- /dev/null +++ b/telegram/_payment/refundedpayment.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram RefundedPayment.""" + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class RefundedPayment(TelegramObject): + """This object contains basic information about a refunded payment. + + 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` is equal. + + Args: + currency (:obj:`str`): Three-letter ISO 4217 `currency + `_ code, or ``XTR`` for + payments in |tg_stars|. Currently, always ``XTR``. + total_amount (:obj:`int`): Total refunded price in the *smallest units* of the currency + (integer, **not** float/double). For example, for a price of ``US$ 1.45``, + ``total_amount = 145``. See the *exp* parameter in + `currencies.json `_, + it shows the number of digits past the decimal point for each currency + (2 for the majority of currencies). + invoice_payload (:obj:`str`): Bot-specified invoice payload. + telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. + provider_payment_charge_id (:obj:`str`, optional): Provider payment identifier. + + Attributes: + currency (:obj:`str`): Three-letter ISO 4217 `currency + `_ code, or ``XTR`` for + payments in |tg_stars|. Currently, always ``XTR``. + total_amount (:obj:`int`): Total refunded price in the *smallest units* of the currency + (integer, **not** float/double). For example, for a price of ``US$ 1.45``, + ``total_amount = 145``. See the *exp* parameter in + `currencies.json `_, + it shows the number of digits past the decimal point for each currency + (2 for the majority of currencies). + invoice_payload (:obj:`str`): Bot-specified invoice payload. + telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. + provider_payment_charge_id (:obj:`str`): Optional. Provider payment identifier. + + """ + + __slots__ = ( + "currency", + "invoice_payload", + "provider_payment_charge_id", + "telegram_payment_charge_id", + "total_amount", + ) + + def __init__( + self, + currency: str, + total_amount: int, + invoice_payload: str, + telegram_payment_charge_id: str, + provider_payment_charge_id: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.currency: str = currency + self.total_amount: int = total_amount + self.invoice_payload: str = invoice_payload + self.telegram_payment_charge_id: str = telegram_payment_charge_id + # Optional + self.provider_payment_charge_id: Optional[str] = provider_payment_charge_id + + self._id_attrs = (self.telegram_payment_charge_id,) + + self._freeze() diff --git a/telegram/constants.py b/telegram/constants.py index d9b26b417c6..9cd41b5f3a2 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -151,7 +151,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=6) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=7) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -1920,6 +1920,11 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.poll`.""" PROXIMITY_ALERT_TRIGGERED = "proximity_alert_triggered" """:obj:`str`: Messages with :attr:`telegram.Message.proximity_alert_triggered`.""" + REFUNDED_PAYMENT = "refunded_payment" + """:obj:`str`: Messages with :attr:`telegram.Message.refunded_payment`. + + .. versionadded:: NEXT.VERSION + """ REPLY_TO_STORY = "reply_to_story" """:obj:`str`: Messages with :attr:`telegram.Message.reply_to_story`. diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 8182ac64996..bc6b06fda28 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1959,6 +1959,7 @@ def filter(self, update: Update) -> bool: or StatusUpdate.NEW_CHAT_TITLE.check_update(update) or StatusUpdate.PINNED_MESSAGE.check_update(update) 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) @@ -2205,6 +2206,17 @@ def filter(self, message: Message) -> bool: ) """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" + class _RefundedPayment(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.refunded_payment) + + REFUNDED_PAYMENT = _RefundedPayment("filters.StatusUpdate.REFUNDED_PAYMENT") + """Messages that contain :attr:`telegram.Message.refunded_payment`. + .. versionadded:: NEXT.VERSION + """ + class _UserShared(MessageFilter): __slots__ = () diff --git a/tests/_payment/test_refundedpayment.py b/tests/_payment/test_refundedpayment.py new file mode 100644 index 00000000000..75e252660da --- /dev/null +++ b/tests/_payment/test_refundedpayment.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import pytest + +from telegram import RefundedPayment +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def refunded_payment(): + return RefundedPayment( + TestRefundedPaymentBase.currency, + TestRefundedPaymentBase.total_amount, + TestRefundedPaymentBase.invoice_payload, + TestRefundedPaymentBase.telegram_payment_charge_id, + TestRefundedPaymentBase.provider_payment_charge_id, + ) + + +class TestRefundedPaymentBase: + invoice_payload = "invoice_payload" + currency = "EUR" + total_amount = 100 + telegram_payment_charge_id = "telegram_payment_charge_id" + provider_payment_charge_id = "provider_payment_charge_id" + + +class TestRefundedPaymentWithoutRequest(TestRefundedPaymentBase): + def test_slot_behaviour(self, refunded_payment): + inst = refunded_payment + 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, bot): + json_dict = { + "invoice_payload": self.invoice_payload, + "currency": self.currency, + "total_amount": self.total_amount, + "telegram_payment_charge_id": self.telegram_payment_charge_id, + "provider_payment_charge_id": self.provider_payment_charge_id, + } + refunded_payment = RefundedPayment.de_json(json_dict, bot) + assert refunded_payment.api_kwargs == {} + + assert refunded_payment.invoice_payload == self.invoice_payload + assert refunded_payment.currency == self.currency + assert refunded_payment.total_amount == self.total_amount + assert refunded_payment.telegram_payment_charge_id == self.telegram_payment_charge_id + assert refunded_payment.provider_payment_charge_id == self.provider_payment_charge_id + + def test_to_dict(self, refunded_payment): + refunded_payment_dict = refunded_payment.to_dict() + + assert isinstance(refunded_payment_dict, dict) + assert refunded_payment_dict["invoice_payload"] == refunded_payment.invoice_payload + assert refunded_payment_dict["currency"] == refunded_payment.currency + assert refunded_payment_dict["total_amount"] == refunded_payment.total_amount + assert ( + refunded_payment_dict["telegram_payment_charge_id"] + == refunded_payment.telegram_payment_charge_id + ) + assert ( + refunded_payment_dict["provider_payment_charge_id"] + == refunded_payment.provider_payment_charge_id + ) + + def test_equality(self): + a = RefundedPayment( + self.currency, + self.total_amount, + self.invoice_payload, + self.telegram_payment_charge_id, + self.provider_payment_charge_id, + ) + b = RefundedPayment( + self.currency, + self.total_amount, + self.invoice_payload, + self.telegram_payment_charge_id, + self.provider_payment_charge_id, + ) + c = RefundedPayment("", 0, "", self.telegram_payment_charge_id) + d = RefundedPayment( + self.currency, + self.total_amount, + self.invoice_payload, + "", + ) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index cc237f41001..9cf47dc47fa 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -1100,6 +1100,11 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.CHAT_BACKGROUND_SET.check_update(update) update.message.chat_background_set = None + update.message.refunded_payment = "refunded_payment" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.REFUNDED_PAYMENT.check_update(update) + update.message.refunded_payment = None + def test_filters_forwarded(self, update, message_origin_user): assert filters.FORWARDED.check_update(update) update.message.forward_origin = MessageOriginHiddenUser(datetime.datetime.utcnow(), 1) diff --git a/tests/test_message.py b/tests/test_message.py index 5596710396d..6352845ffa2 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -53,6 +53,7 @@ Poll, PollOption, ProximityAlertTriggered, + RefundedPayment, ReplyParameters, SharedUser, Sticker, @@ -278,6 +279,7 @@ def message(bot): {"effect_id": "123456789"}, {"show_caption_above_media": True}, {"paid_media": PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)])}, + {"refunded_payment": RefundedPayment("EUR", 243, "payload", "charge_id", "provider_id")}, ], ids=[ "reply", @@ -350,6 +352,7 @@ def message(bot): "effect_id", "show_caption_above_media", "paid_media", + "refunded_payment", ], ) def message_params(bot, request): From 1714bfd8f681bb91be7d3a35858b25d1c57c6c93 Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Tue, 9 Jul 2024 23:33:57 +0200 Subject: [PATCH 19/26] Deprecate Inclusion of `successful_payment` in `Message.effective_attachment` (#4365) --- telegram/_message.py | 16 ++++++++++++++-- tests/test_message.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/telegram/_message.py b/telegram/_message.py index 5c45b9582a4..3cd654abbd9 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -1385,8 +1385,8 @@ def effective_attachment( Voice, None, ]: - """If this message is neither a plain text message nor a status update, this gives the - attachment that this message was sent with. This may be one of + """If the message is a user generated content which is not a plain text message, this + property is set to this content. It may be one of * :class:`telegram.Audio` * :class:`telegram.Dice` @@ -1419,6 +1419,9 @@ def effective_attachment( .. versionchanged:: NEXT.VERSION :attr:`paid_media` is now also considered to be an attachment. + .. deprecated:: NEXT.VERSION + :attr:`successful_payment` will be removed in future major versions. + """ if not isinstance(self._effective_attachment, DefaultValue): return self._effective_attachment @@ -1426,6 +1429,15 @@ def effective_attachment( for attachment_type in MessageAttachmentType: if self[attachment_type]: self._effective_attachment = self[attachment_type] # type: ignore[assignment] + if attachment_type == MessageAttachmentType.SUCCESSFUL_PAYMENT: + warn( + PTBDeprecationWarning( + "NEXT.VERSION", + "successful_payment will no longer be considered an attachment in" + " future major versions", + ), + stacklevel=2, + ) break else: self._effective_attachment = None diff --git a/tests/test_message.py b/tests/test_message.py index 6352845ffa2..9e575a99f45 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -2774,3 +2774,15 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(message.get_bot(), "unpin_all_forum_topic_messages", make_assertion) assert await message.unpin_all_forum_topic_messages() + + def test_attachement_successful_payment_deprecated(self, message, recwarn): + message.successful_payment = "something" + # kinda unnecessary to assert but one needs to call the function ofc so. Here we are. + assert message.effective_attachment == "something" + assert len(recwarn) == 1 + assert ( + "successful_payment will no longer be considered an attachment in future major " + "versions" in str(recwarn[0].message) + ) + assert recwarn[0].category is PTBDeprecationWarning + assert recwarn[0].filename == __file__ From 7a470d57c8caf8df0ef56cdd7c5daf5ba4cb079e Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:10:33 +0200 Subject: [PATCH 20/26] Add a Test Case for `MenuButton` (#4363) --- tests/test_menubutton.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_menubutton.py b/tests/test_menubutton.py index bb859a20609..48c9c30c9f2 100644 --- a/tests/test_menubutton.py +++ b/tests/test_menubutton.py @@ -136,6 +136,13 @@ def test_de_json_subclass(self, scope_class, bot): json_dict = {"type": "invalid", "text": self.text, "web_app": self.web_app.to_dict()} assert type(scope_class.de_json(json_dict, bot)) is scope_class + def test_de_json_empty_data(self, scope_class): + if scope_class in (MenuButtonWebApp,): + pytest.skip( + "This test is not relevant for subclasses that have more attributes than just type" + ) + assert isinstance(scope_class.de_json({}, None), scope_class) + def test_to_dict(self, menu_button): menu_button_dict = menu_button.to_dict() From 06f1da576e80c4f176452e41551096be4dbcd391 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:11:22 +0200 Subject: [PATCH 21/26] Stabilize Some Concurrency Usages in Test Suite (#4360) --- tests/test_bot.py | 14 +++++++++----- tests/test_constants.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index df142e3fb61..85232a8c708 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2368,11 +2368,15 @@ async def test_delete_message_old_message(self, bot, chat_id): # test modules. No need to duplicate here. async def test_delete_messages(self, bot, chat_id): - msg1 = await bot.send_message(chat_id, text="will be deleted") - msg2 = await bot.send_message(chat_id, text="will be deleted") - await asyncio.sleep(2) + msg1, msg2 = await asyncio.gather( + bot.send_message(chat_id, text="will be deleted"), + bot.send_message(chat_id, text="will be deleted"), + ) - assert await bot.delete_messages(chat_id=chat_id, message_ids=(msg1.id, msg2.id)) is True + assert ( + await bot.delete_messages(chat_id=chat_id, message_ids=sorted((msg1.id, msg2.id))) + is True + ) async def test_send_venue(self, bot, chat_id): longitude = -46.788279 @@ -3915,7 +3919,7 @@ async def test_copy_messages(self, bot, chat_id): msg1, msg2 = await tasks copy_messages = await bot.copy_messages( - chat_id, from_chat_id=chat_id, message_ids=(msg1.message_id, msg2.message_id) + chat_id, from_chat_id=chat_id, message_ids=sorted((msg1.message_id, msg2.message_id)) ) assert isinstance(copy_messages, tuple) diff --git a/tests/test_constants.py b/tests/test_constants.py index b750f7fba3a..dc76bea3aef 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -234,6 +234,11 @@ async def test_max_message_length(self, bot, chat_id): return_exceptions=True, ) good_msg, bad_msg = await tasks + + if isinstance(good_msg, BaseException): + # handling xfails + raise good_msg + assert good_msg.text == good_text assert isinstance(bad_msg, BadRequest) assert "Message is too long" in str(bad_msg) @@ -247,6 +252,11 @@ async def test_max_caption_length(self, bot, chat_id): return_exceptions=True, ) good_msg, bad_msg = await tasks + + if isinstance(good_msg, BaseException): + # handling xfails + raise good_msg + assert good_msg.caption == good_caption assert isinstance(bad_msg, BadRequest) assert "Message caption is too long" in str(bad_msg) From f7377025440ba8abc7395ca18df0bbed904e5225 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:33:04 +0200 Subject: [PATCH 22/26] Use a Composite Action for Testing Type Completeness (#4367) --- .github/workflows/type_completeness.yml | 65 ++----------------------- 1 file changed, 4 insertions(+), 61 deletions(-) diff --git a/.github/workflows/type_completeness.yml b/.github/workflows/type_completeness.yml index 4a98c0b30a8..17dc249c81f 100644 --- a/.github/workflows/type_completeness.yml +++ b/.github/workflows/type_completeness.yml @@ -4,6 +4,7 @@ on: paths: - telegram/** - pyproject.toml + - .github/workflows/type_completeness.yml push: branches: - master @@ -13,66 +14,8 @@ jobs: name: test-type-completeness runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: git fetch --depth=1 # https://github.com/actions/checkout/issues/329#issuecomment-674881489 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - uses: Bibo-Joshi/pyright-type-completeness@1.0.0 with: + package-name: telegram python-version: 3.12 - cache: 'pip' - cache-dependency-path: '**/requirements*.txt' - - name: Install Pyright - run: | - python -W ignore -m pip install pyright~=1.1.367 - - name: Get PR Completeness - # Must run before base completeness, as base completeness will checkout the base branch - # And we can't go back to the PR branch after that in case the PR is coming from a fork - run: | - pip install . -U - pyright --verifytypes telegram --ignoreexternal --outputjson > pr.json || true - pyright --verifytypes telegram --ignoreexternal > pr.readable || true - - name: Get Base Completeness - run: | - git checkout ${{ github.base_ref }} - pip install . -U - pyright --verifytypes telegram --ignoreexternal --outputjson > base.json || true - - name: Compare Completeness - uses: jannekem/run-python-script-action@v1 - with: - script: | - import json - import os - from pathlib import Path - - base = float( - json.load(open("base.json", "rb"))["typeCompleteness"]["completenessScore"] - ) - pr = float( - json.load(open("pr.json", "rb"))["typeCompleteness"]["completenessScore"] - ) - base_text = f"This PR changes type completeness from {round(base, 3)} to {round(pr, 3)}." - - if base == 0: - text = f"Something is broken in the workflow. Reported type completeness is 0. 💥" - set_summary(text) - print(Path("pr.readable").read_text(encoding="utf-8")) - error(text) - exit(1) - - if pr < (base - 0.001): - text = f"{base_text} ❌" - set_summary(text) - print(Path("pr.readable").read_text(encoding="utf-8")) - error(text) - exit(1) - elif pr > (base + 0.001): - text = f"{base_text} ✨" - set_summary(text) - if pr < 1: - print(Path("pr.readable").read_text(encoding="utf-8")) - print(text) - else: - text = f"{base_text} This is less than 0.1 percentage points. ✅" - set_summary(text) - print(Path("pr.readable").read_text(encoding="utf-8")) - print(text) + pyright-version: ~=1.1.367 From 86c8cae40d9e47dde478d48caf8f27ab9a56fa7f Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 10 Jul 2024 20:36:47 +0200 Subject: [PATCH 23/26] Restructure Readme (#4362) --- README.rst | 46 ++++++++++++++++++++++++++++++------------- docs/source/index.rst | 12 +++++++++++ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index d58e814c391..4dceafa9bd3 100644 --- a/README.rst +++ b/README.rst @@ -66,23 +66,36 @@ We have a vibrant community of developers helping each other in our `Telegram gr *Stay tuned for library updates and new releases on our* `Telegram Channel `_. Introduction -============ +------------ This library provides a pure Python, asynchronous interface for the `Telegram Bot API `_. It's compatible with Python versions **3.8+**. -In addition to the pure API implementation, this library features a number of high-level classes to +In addition to the pure API implementation, this library features several convenience methods and shortcuts as well as a number of high-level classes to make the development of bots easy and straightforward. These classes are contained in the ``telegram.ext`` submodule. +After installing_ the library, be sure to check out the section on `working with PTB`_. + Telegram API support -==================== +~~~~~~~~~~~~~~~~~~~~ + +All types and methods of the Telegram Bot API **7.7** are natively supported by this library. +In addition, Bot API functionality not yet natively included can still be used as described `in our wiki `_. + +Notable Features +~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **7.7** are supported. +- `Fully asynchronous `_ +- Convenient shortcut methods, e.g. `Message.reply_text `_ +- `Fully annotated with static type hints `_ +- `Customizable and extendable interface `_ +- Seamless integration with `webhooks `_ and `polling `_ +- `Comprehensive documentation and examples <#working-with-ptb>`_ Installing -========== +---------- You can install or upgrade ``python-telegram-bot`` via @@ -102,7 +115,7 @@ You can also install ``python-telegram-bot`` from source, though this is usually $ python -m build Verifying Releases ------------------- +~~~~~~~~~~~~~~~~~~ We sign all the releases with a GPG key. The signatures are uploaded to both the `GitHub releases page `_ and the `PyPI project `_ and end with a suffix ``.asc``. @@ -114,7 +127,7 @@ In addition, the GitHub release page also contains the sha1 hashes of the releas This allows you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team. Dependencies & Their Versions ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``python-telegram-bot`` tries to use as few 3rd party dependencies as possible. However, for some features using a 3rd party library is more sane than implementing the functionality again. @@ -150,14 +163,19 @@ Additionally, two shortcuts are provided: * ``pip install "python-telegram-bot[all]"`` installs all optional dependencies. * ``pip install "python-telegram-bot[ext]"`` installs all optional dependencies that are related to ``telegram.ext``, i.e. ``[rate-limiter, webhooks, callback-data, job-queue]``. +Working with PTB +---------------- + +Once you have installed the library, you can begin working with it - so let's get started! + Quick Start -=========== +~~~~~~~~~~~ Our Wiki contains an `Introduction to the API `_ explaining how the pure Bot API can be accessed via ``python-telegram-bot``. Moreover, the `Tutorial: Your first Bot `_ gives an introduction on how chatbots can be easily programmed with the help of the ``telegram.ext`` module. Resources -========= +~~~~~~~~~ - The `package documentation `_ is the technical reference for ``python-telegram-bot``. It contains descriptions of all available classes, modules, methods and arguments as well as the `changelog `_. @@ -168,7 +186,7 @@ Resources - The `official Telegram Bot API documentation `_ is of course always worth a read. Getting help -============ +~~~~~~~~~~~~ If the resources mentioned above don't answer your questions or simply overwhelm you, there are several ways of getting help. @@ -179,7 +197,7 @@ If the resources mentioned above don't answer your questions or simply overwhelm 3. You can even ask for help on Stack Overflow using the `python-telegram-bot tag `_. Concurrency -=========== +~~~~~~~~~~~ Since v20.0, ``python-telegram-bot`` is built on top of Pythons ``asyncio`` module. Because ``asyncio`` is in general single-threaded, ``python-telegram-bot`` does currently not aim to be thread-safe. @@ -192,20 +210,20 @@ Noteworthy parts of ``python-telegram-bots`` API that are likely to cause issues * all classes in the ``telegram.ext.filters`` module that allow to add/remove allowed users/chats at runtime Contributing -============ +------------ Contributions of all sizes are welcome. Please review our `contribution guidelines `_ to get started. You can also help by `reporting bugs or feature requests `_. Donating -======== +-------- Occasionally we are asked if we accept donations to support the development. While we appreciate the thought, maintaining PTB is our hobby, and we have almost no running costs for it. We therefore have nothing set up to accept donations. If you still want to donate, we kindly ask you to donate to another open source project/initiative of your choice instead. License -======= +------- You may copy, distribute and modify the software provided that modifications are described and licensed for free under `LGPL-3 `_. Derivatives works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be. diff --git a/docs/source/index.rst b/docs/source/index.rst index d7be3ab9edf..f8aa9e7b647 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,6 +3,18 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +.. raw:: html + +
+ +Hidden Headline +=============== +This is just here to get furo to display the right sidebar. + +.. raw:: html + +
+ .. include:: ../../README.rst .. The toctrees are hidden such that they don't render on the start page but still include the contents into the documentation. From 0a673e8f7e32f1e50dab6565323f93182a2d4d0b Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:33:42 +0200 Subject: [PATCH 24/26] Documentation Improvements (#4303) Co-authored-by: poolitzer --- .github/CONTRIBUTING.rst | 2 +- telegram/_bot.py | 29 ++++++++++--------- telegram/_callbackquery.py | 2 +- telegram/_inline/inlinekeyboardbutton.py | 20 ++++++++----- telegram/_inline/inlinekeyboardmarkup.py | 2 +- .../_inline/inputinvoicemessagecontent.py | 15 +++++----- telegram/_keyboardbuttonrequest.py | 14 +++------ telegram/_message.py | 16 ++++++++-- telegram/_payment/invoice.py | 12 ++++---- telegram/_payment/labeledprice.py | 4 +-- telegram/_payment/precheckoutquery.py | 8 ++--- telegram/_payment/successfulpayment.py | 8 ++--- telegram/_reply.py | 18 ++++++++---- telegram/_replykeyboardmarkup.py | 2 +- telegram/ext/_application.py | 15 ++++++---- telegram/ext/_basepersistence.py | 12 ++++++++ telegram/request/_requestparameter.py | 2 +- 17 files changed, 107 insertions(+), 74 deletions(-) diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index 635cdb23ebc..63906fabd6f 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -194,7 +194,7 @@ Feel free to copy (parts of) the checklist to the PR description to remind you o - Added or updated documentation for the changed class(es) and/or method(s) - Added the new method(s) to ``_extbot.py`` - Added or updated ``bot_methods.rst`` - - Updated the Bot API version number in all places: ``README.rst`` and ``README_RAW.rst`` (including the badge), as well as ``telegram.constants.BOT_API_VERSION_INFO`` + - Updated the Bot API version number in all places: ``README.rst`` (including the badge) and ``telegram.constants.BOT_API_VERSION_INFO`` - Added logic for arbitrary callback data in :class:`telegram.ext.ExtBot` for new methods that either accept a ``reply_markup`` in some form or have a return type that is/contains :class:`~telegram.Message` Documenting diff --git a/telegram/_bot.py b/telegram/_bot.py index 6a1cbfd07af..ff1ccbb3766 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -3972,7 +3972,7 @@ async def edit_message_text( Use this method to edit text and game messages. Note: - * |editreplymarkup|. + * |editreplymarkup| * |bcid_edit_time| .. seealso:: :attr:`telegram.Game.text` @@ -5037,15 +5037,16 @@ async def send_invoice( .. versionchanged:: 20.0 |sequenceargs| max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the - *smallest* units of the currency (integer, **not** float/double). For example, for - a maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the exp parameter in - `currencies.json `_, it - shows the number of digits past the decimal point for each currency (2 for the - majority of currencies). Defaults to ``0``. Not supported for payment in |tg_stars| + *smallest units* of the currency (integer, **not** float/double). For example, for + a maximum tip of ``US$ 1.45`` pass ``max_tip_amount = 145``. See the ``exp`` + parameter in `currencies.json + `_, it shows the number of + digits past the decimal point for each currency (2 for the majority of currencies). + Defaults to ``0``. Not supported for payment in |tg_stars|. .. versionadded:: 13.5 suggested_tip_amounts (Sequence[:obj:`int`], optional): An array of - suggested amounts of tips in the *smallest* units of the currency (integer, **not** + suggested amounts of tips in the *smallest units* of the currency (integer, **not** float/double). At most :tg-const:`telegram.Invoice.MAX_TIP_AMOUNTS` suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed :paramref:`max_tip_amount`. @@ -7978,14 +7979,14 @@ async def create_invoice_link( .. versionchanged:: 20.0 |sequenceargs| max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the - *smallest* units of the currency (integer, **not** float/double). For example, for - a maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the exp parameter in - `currencies.json `_, it - shows the number of digits past the decimal point for each currency (2 for the - majority of currencies). Defaults to ``0``. Not supported for payments in - |tg_stars|. + *smallest units* of the currency (integer, **not** float/double). For example, for + a maximum tip of ``US$ 1.45`` pass ``max_tip_amount = 145``. See the ``exp`` + parameter in `currencies.json + `_, it shows the number of + digits past the decimal point for each currency (2 for the majority of currencies). + Defaults to ``0``. Not supported for payments in |tg_stars|. suggested_tip_amounts (Sequence[:obj:`int`], optional): An array of - suggested amounts of tips in the *smallest* units of the currency (integer, **not** + suggested amounts of tips in the *smallest units* of the currency (integer, **not** float/double). At most :tg-const:`telegram.Invoice.MAX_TIP_AMOUNTS` suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed :paramref:`max_tip_amount`. diff --git a/telegram/_callbackquery.py b/telegram/_callbackquery.py index 94661a10fad..bdfa569dbfd 100644 --- a/telegram/_callbackquery.py +++ b/telegram/_callbackquery.py @@ -93,7 +93,7 @@ class CallbackQuery(TelegramObject): with the callback button that originated the query. .. versionchanged:: 20.8 - Objects maybe be of type :class:`telegram.MaybeInaccessibleMessage` since Bot API + Objects may be of type :class:`telegram.MaybeInaccessibleMessage` since Bot API 7.0. data (:obj:`str` | :obj:`object`): Optional. Data associated with the callback button. Be aware that the message, which originated the query, can contain no callback buttons diff --git a/telegram/_inline/inlinekeyboardbutton.py b/telegram/_inline/inlinekeyboardbutton.py index bbd53ec06a9..6ceca1a311b 100644 --- a/telegram/_inline/inlinekeyboardbutton.py +++ b/telegram/_inline/inlinekeyboardbutton.py @@ -46,7 +46,7 @@ class InlineKeyboardButton(TelegramObject): working as expected. Putting a game short name in it might, but is not guaranteed to work. * If your bot allows for arbitrary callback data, in keyboards returned in a response - from telegram, :attr:`callback_data` maybe be an instance of + from telegram, :attr:`callback_data` may be an instance of :class:`telegram.ext.InvalidCallbackData`. This will be the case, if the data associated with the button was already deleted. @@ -119,9 +119,12 @@ class InlineKeyboardButton(TelegramObject): This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`, but gives no control over which chats can be selected. switch_inline_query_current_chat (:obj:`str`, optional): If set, pressing the button will - prompt the user to select one of their chats of the specified type, open that chat and - insert the bot's username and the specified inline query in the input field. Not - supported for messages sent on behalf of a Telegram Business account. + insert the bot's username and the specified inline query in the current chat's input + field. May be empty, in which case only the bot's username will be inserted. + + This offers a quick way for the user to open your bot in inline mode in the same chat + - good for selecting something from multiple options. Not supported in channels and for + messages sent on behalf of a Telegram Business account. callback_game (:class:`telegram.CallbackGame`, optional): Description of the game that will be launched when the user presses the button @@ -187,9 +190,12 @@ class InlineKeyboardButton(TelegramObject): This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`, but gives no control over which chats can be selected. switch_inline_query_current_chat (:obj:`str`): Optional. If set, pressing the button will - prompt the user to select one of their chats of the specified type, open that chat and - insert the bot's username and the specified inline query in the input field. Not - supported for messages sent on behalf of a Telegram Business account. + insert the bot's username and the specified inline query in the current chat's input + field. May be empty, in which case only the bot's username will be inserted. + + This offers a quick way for the user to open your bot in inline mode in the same chat + - good for selecting something from multiple options. Not supported in channels and for + messages sent on behalf of a Telegram Business account. callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will be launched when the user presses the button. diff --git a/telegram/_inline/inlinekeyboardmarkup.py b/telegram/_inline/inlinekeyboardmarkup.py index efb181e8aa5..6857e4d8e3a 100644 --- a/telegram/_inline/inlinekeyboardmarkup.py +++ b/telegram/_inline/inlinekeyboardmarkup.py @@ -42,7 +42,7 @@ class InlineKeyboardMarkup(TelegramObject): An inline keyboard on a message .. seealso:: - An another kind of keyboard would be the :class:`telegram.ReplyKeyboardMarkup`. + Another kind of keyboard would be the :class:`telegram.ReplyKeyboardMarkup`. Examples: * :any:`Inline Keyboard 1 ` diff --git a/telegram/_inline/inputinvoicemessagecontent.py b/telegram/_inline/inputinvoicemessagecontent.py index be539bcb38c..e13642da562 100644 --- a/telegram/_inline/inputinvoicemessagecontent.py +++ b/telegram/_inline/inputinvoicemessagecontent.py @@ -66,14 +66,13 @@ class InputInvoiceMessageContent(InputMessageContent): |sequenceclassargs| max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the - *smallest* units of the currency (integer, **not** float/double). For example, for a - maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the ``exp`` parameter in + *smallest units* of the currency (integer, **not** float/double). For example, for a + maximum tip of ``US$ 1.45`` pass ``max_tip_amount = 145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority - of currencies). Defaults to ``0``. Defaults to ``0``. Not supported for payments in - |tg_stars|. + of currencies). Defaults to ``0``. Not supported for payments in |tg_stars|. suggested_tip_amounts (Sequence[:obj:`int`], optional): An array of suggested - amounts of tip in the *smallest* units of the currency (integer, **not** float/double). + amounts of tip in the *smallest units* of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed :attr:`max_tip_amount`. @@ -131,13 +130,13 @@ class InputInvoiceMessageContent(InputMessageContent): |tupleclassattrs| max_tip_amount (:obj:`int`): Optional. The maximum accepted amount for tips in the - *smallest* units of the currency (integer, **not** float/double). For example, for a - maximum tip of US$ 1.45 ``max_tip_amount`` is ``145``. See the ``exp`` parameter in + *smallest units* of the currency (integer, **not** float/double). For example, for a + maximum tip of ``US$ 1.45`` ``max_tip_amount`` is ``145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to ``0``. Not supported for payments in |tg_stars|. suggested_tip_amounts (Tuple[:obj:`int`]): Optional. An array of suggested - amounts of tip in the *smallest* units of the currency (integer, **not** float/double). + amounts of tip in the *smallest units* of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed :attr:`max_tip_amount`. diff --git a/telegram/_keyboardbuttonrequest.py b/telegram/_keyboardbuttonrequest.py index 07f2c3a99aa..4416952112e 100644 --- a/telegram/_keyboardbuttonrequest.py +++ b/telegram/_keyboardbuttonrequest.py @@ -30,15 +30,12 @@ class KeyboardButtonRequestUsers(TelegramObject): """This object defines the criteria used to request a suitable user. The identifier of the - selected user will be shared with the bot when the corresponding button is pressed. + selected user will be shared with the bot when the corresponding button is pressed. `More + about requesting users » `_. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`request_id` is equal. - .. seealso:: - `Telegram Docs on requesting users \ - `_ - .. versionadded:: 20.8 This class was previously named ``KeyboardButtonRequestUser``. @@ -136,15 +133,12 @@ def __init__( class KeyboardButtonRequestChat(TelegramObject): """This object defines the criteria used to request a suitable chat. The identifier of the - selected user will be shared with the bot when the corresponding button is pressed. + selected user will be shared with the bot when the corresponding button is pressed. `More + about requesting users » `_. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`request_id` is equal. - .. seealso:: - `Telegram Docs on requesting chats \ - `_ - .. versionadded:: 20.1 Args: diff --git a/telegram/_message.py b/telegram/_message.py index 3cd654abbd9..edf9fb40143 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -336,7 +336,7 @@ class Message(MaybeInaccessibleMessage): effect_id (:obj:`str`, optional): Unique identifier of the message effect added to the message. - ..versionadded:: 21.3 + .. versionadded:: 21.3 caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): For messages with a Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the @@ -358,6 +358,7 @@ class Message(MaybeInaccessibleMessage): about the animation. For backward compatibility, when this field is set, the document field will also be set. game (:class:`telegram.Game`, optional): Message is a game, information about the game. + `More about games >> `_. photo (Sequence[:class:`telegram.PhotoSize`], optional): Message is a photo, available sizes of the photo. This list is empty if the message does not contain a photo. @@ -373,7 +374,8 @@ class Message(MaybeInaccessibleMessage): video. voice (:class:`telegram.Voice`, optional): Message is a voice message, information about the file. - video_note (:class:`telegram.VideoNote`, optional): Message is a video note, information + video_note (:class:`telegram.VideoNote`, optional): Message is a + `video note `_, information about the video message. new_chat_members (Sequence[:class:`telegram.User`], optional): New members that were added to the group or supergroup and information about them (the bot itself may be one of @@ -429,10 +431,13 @@ class Message(MaybeInaccessibleMessage): :class:`telegram.InaccessibleMessage`. invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, information about the invoice. + `More about payments >> `_. successful_payment (:class:`telegram.SuccessfulPayment`, optional): Message is a service message about a successful payment, information about the payment. + `More about payments >> `_. connected_website (:obj:`str`, optional): The domain name of the website on which the user has logged in. + `More about Telegram Login >> `_. author_signature (:obj:`str`, optional): Signature of the post author for messages in channels, or the custom title of an anonymous group administrator. passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data. @@ -670,6 +675,7 @@ class Message(MaybeInaccessibleMessage): .. seealso:: :wiki:`Working with Files and Media ` game (:class:`telegram.Game`): Optional. Message is a game, information about the game. + `More about games >> `_. photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Message is a photo, available sizes of the photo. This list is empty if the message does not contain a photo. @@ -693,7 +699,8 @@ class Message(MaybeInaccessibleMessage): the file. .. seealso:: :wiki:`Working with Files and Media ` - video_note (:class:`telegram.VideoNote`): Optional. Message is a video note, information + video_note (:class:`telegram.VideoNote`): Optional. Message is a + `video note `_, information about the video message. .. seealso:: :wiki:`Working with Files and Media ` @@ -750,10 +757,13 @@ class Message(MaybeInaccessibleMessage): :class:`telegram.InaccessibleMessage`. invoice (:class:`telegram.Invoice`): Optional. Message is an invoice for a payment, information about the invoice. + `More about payments >> `_. successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Message is a service message about a successful payment, information about the payment. + `More about payments >> `_. connected_website (:obj:`str`): Optional. The domain name of the website on which the user has logged in. + `More about Telegram Login >> `_. author_signature (:obj:`str`): Optional. Signature of the post author for messages in channels, or the custom title of an anonymous group administrator. passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data. diff --git a/telegram/_payment/invoice.py b/telegram/_payment/invoice.py index 141f0c8fdd4..804ac040304 100644 --- a/telegram/_payment/invoice.py +++ b/telegram/_payment/invoice.py @@ -39,9 +39,9 @@ class Invoice(TelegramObject): generate this invoice. currency (:obj:`str`): Three-letter ISO 4217 currency code, or ``XTR`` for payments in |tg_stars|. - total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. See the - ``exp`` parameter in + total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, + **not** float/double). For example, for a price of ``US$ 1.45`` pass ``amount = 145``. + See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). @@ -53,9 +53,9 @@ class Invoice(TelegramObject): generate this invoice. currency (:obj:`str`): Three-letter ISO 4217 currency code, or ``XTR`` for payments in |tg_stars|. - total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 ``amount`` is ``145``. See the - ``exp`` parameter in + total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, + **not** float/double). For example, for a price of ``US$ 1.45`` pass ``amount = 145``. + See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). diff --git a/telegram/_payment/labeledprice.py b/telegram/_payment/labeledprice.py index b668931b9d4..ec02f1f7029 100644 --- a/telegram/_payment/labeledprice.py +++ b/telegram/_payment/labeledprice.py @@ -36,7 +36,7 @@ class LabeledPrice(TelegramObject): Args: label (:obj:`str`): Portion label. amount (:obj:`int`): Price of the product in the smallest units of the currency (integer, - not float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. + **not** float/double). For example, for a price of ``US$ 1.45`` pass ``amount = 145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency @@ -45,7 +45,7 @@ class LabeledPrice(TelegramObject): Attributes: label (:obj:`str`): Portion label. amount (:obj:`int`): Price of the product in the smallest units of the currency (integer, - not float/double). For example, for a price of US$ 1.45 ``amount`` is ``145``. + **not** float/double). For example, for a price of ``US$ 1.45`` pass ``amount = 145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency diff --git a/telegram/_payment/precheckoutquery.py b/telegram/_payment/precheckoutquery.py index 30ae30be797..60e1d6078a1 100644 --- a/telegram/_payment/precheckoutquery.py +++ b/telegram/_payment/precheckoutquery.py @@ -44,8 +44,8 @@ class PreCheckoutQuery(TelegramObject): from_user (:class:`telegram.User`): User who sent the query. currency (:obj:`str`): Three-letter ISO 4217 currency code, or ``XTR`` for payments in |tg_stars|. - total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. + total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, + **not** float/double). For example, for a price of ``US$ 1.45`` pass ``amount = 145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency @@ -60,8 +60,8 @@ class PreCheckoutQuery(TelegramObject): from_user (:class:`telegram.User`): User who sent the query. currency (:obj:`str`): Three-letter ISO 4217 currency code, or ``XTR`` for payments in |tg_stars|. - total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 ``amount`` is ``145``. + total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, + **not** float/double). For example, for a price of ``US$ 1.45`` pass ``amount = 145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency diff --git a/telegram/_payment/successfulpayment.py b/telegram/_payment/successfulpayment.py index 90b1545b2d5..34bce29142e 100644 --- a/telegram/_payment/successfulpayment.py +++ b/telegram/_payment/successfulpayment.py @@ -38,8 +38,8 @@ class SuccessfulPayment(TelegramObject): Args: currency (:obj:`str`): Three-letter ISO 4217 currency code, or ``XTR`` for payments in |tg_stars|. - total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. + total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, + **not** float/double). For example, for a price of ``US$ 1.45`` pass ``amount = 145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency @@ -54,8 +54,8 @@ class SuccessfulPayment(TelegramObject): Attributes: currency (:obj:`str`): Three-letter ISO 4217 currency code, or ``XTR`` for payments in |tg_stars|. - total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not - float/double). For example, for a price of US$ 1.45 ``amount`` is ``145``. + total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, + **not** float/double). For example, for a price of ``US$ 1.45`` pass ``amount = 145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency diff --git a/telegram/_reply.py b/telegram/_reply.py index 6c04847de64..2aefa15085a 100644 --- a/telegram/_reply.py +++ b/telegram/_reply.py @@ -81,8 +81,9 @@ class ExternalReplyInfo(TelegramObject): sticker. story (:class:`telegram.Story`, optional): Message is a forwarded story. video (:class:`telegram.Video`, optional): Message is a video, information about the video. - video_note (:class:`telegram.VideoNote`, optional): Message is a video note, information - about the video message. + video_note (:class:`telegram.VideoNote`, optional): Message is a `video note + `_, information about the video + message. voice (:class:`telegram.Voice`, optional): Message is a voice message, information about the file. has_media_spoiler (:obj:`bool`, optional): :obj:`True`, if the message media is covered by @@ -91,12 +92,14 @@ class ExternalReplyInfo(TelegramObject): about the contact. dice (:class:`telegram.Dice`, optional): Message is a dice with random value. game (:Class:`telegram.Game`. optional): Message is a game, information about the game. + `More about games >> `_. giveaway (:class:`telegram.Giveaway`, optional): Message is a scheduled giveaway, information about the giveaway. giveaway_winners (:class:`telegram.GiveawayWinners`, optional): A giveaway with public winners was completed. invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, - information about the invoice. + information about the invoice. `More about payments >> + `_. location (:class:`telegram.Location`, optional): Message is a shared location, information about the location. poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the @@ -128,8 +131,9 @@ class ExternalReplyInfo(TelegramObject): sticker. story (:class:`telegram.Story`): Optional. Message is a forwarded story. video (:class:`telegram.Video`): Optional. Message is a video, information about the video. - video_note (:class:`telegram.VideoNote`): Optional. Message is a video note, information - about the video message. + video_note (:class:`telegram.VideoNote`): Optional. Message is a `video note + `_, information about the video + message. voice (:class:`telegram.Voice`): Optional. Message is a voice message, information about the file. has_media_spoiler (:obj:`bool`): Optional. :obj:`True`, if the message media is covered by @@ -138,12 +142,14 @@ class ExternalReplyInfo(TelegramObject): about the contact. dice (:class:`telegram.Dice`): Optional. Message is a dice with random value. game (:Class:`telegram.Game`): Optional. Message is a game, information about the game. + `More about games >> `_. giveaway (:class:`telegram.Giveaway`): Optional. Message is a scheduled giveaway, information about the giveaway. giveaway_winners (:class:`telegram.GiveawayWinners`): Optional. A giveaway with public winners was completed. invoice (:class:`telegram.Invoice`): Optional. Message is an invoice for a payment, - information about the invoice. + information about the invoice. `More about payments >> + `_. location (:class:`telegram.Location`): Optional. Message is a shared location, information about the location. poll (:class:`telegram.Poll`): Optional. Message is a native poll, information about the diff --git a/telegram/_replykeyboardmarkup.py b/telegram/_replykeyboardmarkup.py index cfca12cc350..1b410ebc709 100644 --- a/telegram/_replykeyboardmarkup.py +++ b/telegram/_replykeyboardmarkup.py @@ -41,7 +41,7 @@ class ReplyKeyboardMarkup(TelegramObject): A reply keyboard with reply options. .. seealso:: - An another kind of keyboard would be the :class:`telegram.InlineKeyboardMarkup`. + Another kind of keyboard would be the :class:`telegram.InlineKeyboardMarkup`. Examples: * Example usage: A user requests to change the bot's language, bot replies to the request diff --git a/telegram/ext/_application.py b/telegram/ext/_application.py index 4f623ed3695..670793c9909 100644 --- a/telegram/ext/_application.py +++ b/telegram/ext/_application.py @@ -786,6 +786,11 @@ def run_polling( - :meth:`shutdown` - :meth:`post_shutdown` + A small wrapper is passed to :paramref:`telegram.ext.Updater.start_polling.error_callback` + which forwards errors occurring during polling to + :meth:`registered error handlers `. The update parameter of the callback + will be set to :obj:`None`. + .. include:: inclusions/application_run_tip.rst Args: @@ -1362,11 +1367,11 @@ def add_handler(self, handler: BaseHandler[Any, CCT], group: int = DEFAULT_GROUP The priority/order of handlers is determined as follows: - * Priority of the group (lower group number == higher priority) - * The first handler in a group which can handle an update (see - :attr:`telegram.ext.BaseHandler.check_update`) will be used. Other handlers from the - group will not be used. The order in which handlers were added to the group defines the - priority. + * Priority of the group (lower group number == higher priority) + * The first handler in a group which can handle an update (see + :attr:`telegram.ext.BaseHandler.check_update`) will be used. Other handlers from the + group will not be used. The order in which handlers were added to the group defines the + priority. Warning: Adding persistent :class:`telegram.ext.ConversationHandler` after the application has diff --git a/telegram/ext/_basepersistence.py b/telegram/ext/_basepersistence.py index 5199a165bb6..126437a0a48 100644 --- a/telegram/ext/_basepersistence.py +++ b/telegram/ext/_basepersistence.py @@ -357,6 +357,10 @@ async def refresh_user_data(self, user_id: int, user_data: UD) -> None: :attr:`~telegram.ext.Application.user_data` to a callback. Can be used to update data stored in :attr:`~telegram.ext.Application.user_data` from an external source. + Tip: + This method is expected to edit the object :paramref:`user_data` in-place instead of + returning a new object. + Warning: When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method may be called while a handler callback is still running. This might lead to race @@ -380,6 +384,10 @@ async def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: :attr:`~telegram.ext.Application.chat_data` to a callback. Can be used to update data stored in :attr:`~telegram.ext.Application.chat_data` from an external source. + Tip: + This method is expected to edit the object :paramref:`chat_data` in-place instead of + returning a new object. + Warning: When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method may be called while a handler callback is still running. This might lead to race @@ -403,6 +411,10 @@ async def refresh_bot_data(self, bot_data: BD) -> None: :attr:`~telegram.ext.Application.bot_data` to a callback. Can be used to update data stored in :attr:`~telegram.ext.Application.bot_data` from an external source. + Tip: + This method is expected to edit the object :paramref:`bot_data` in-place instead of + returning a new object. + Warning: When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method may be called while a handler callback is still running. This might lead to race diff --git a/telegram/request/_requestparameter.py b/telegram/request/_requestparameter.py index 2e14a8be6ba..ab11cbce793 100644 --- a/telegram/request/_requestparameter.py +++ b/telegram/request/_requestparameter.py @@ -90,7 +90,7 @@ def _value_and_input_files_from_input( # pylint: disable=too-many-return-statem value: object, ) -> Tuple[object, List[InputFile]]: """Converts `value` into something that we can json-dump. Returns two values: - 1. the JSON-dumpable value. Maybe be `None` in case the value is an InputFile which must + 1. the JSON-dumpable value. May be `None` in case the value is an InputFile which must not be uploaded via an attach:// URI 2. A list of InputFiles that should be uploaded for this value From efe1392e7304eb231ea2aeab4c2e28183b87d536 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:44:41 +0200 Subject: [PATCH 25/26] Automate PyPI Releases (#4364) --- .github/workflows/release_pypi.yml | 204 ++++++++++++++++++ README.rst | 16 +- .../{v20.0-current.gpg => v20.0-v21.3.gpg} | 0 3 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release_pypi.yml rename public_keys/{v20.0-current.gpg => v20.0-v21.3.gpg} (100%) diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml new file mode 100644 index 00000000000..bcd1794c468 --- /dev/null +++ b/.github/workflows/release_pypi.yml @@ -0,0 +1,204 @@ +name: Publish to PyPI + +on: + # Run on any tag + push: + tags: + - '**' + # manually trigger the workflow - for testing only + workflow_dispatch: + +jobs: + build: + name: Build Distribution + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install pypa/build + run: >- + python3 -m pip install build --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: Publish to PyPI + # only publish to PyPI on tag pushes + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + needs: + - build + runs-on: ubuntu-latest + environment: + name: release_pypi + url: https://pypi.org/p/python-telegram-bot + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + publish-to-test-pypi: + name: Publish to Test PyPI + needs: + - build + runs-on: ubuntu-latest + environment: + name: release_test_pypi + url: https://test.pypi.org/p/python-telegram-bot + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + compute-signatures: + name: Compute SHA1 Sums and Sign with Sigstore + runs-on: ubuntu-latest + needs: + - publish-to-pypi + - publish-to-test-pypi + # run if either of the publishing jobs ran successfully + # see also: + # https://github.com/actions/runner/issues/491#issuecomment-850884422 + if: | + always() && ( + (needs.publish-to-pypi.result == 'success') || + (needs.publish-to-test-pypi.result == 'success') + ) + + permissions: + id-token: write # IMPORTANT: mandatory for sigstore + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Compute SHA1 Sums + run: | + # Compute SHA1 sum of the distribution packages and save it to a file with the same name, + # but with .sha1 extension + for file in dist/*; do + sha1sum $file > $file.sha1 + done + - name: Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v2.1.1 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Store the distribution packages and signatures + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions-and-signatures + path: dist/ + + github-release: + name: Upload to GitHub Release + needs: + - publish-to-pypi + - compute-signatures + if: | + always() && ( + (needs.publish-to-pypi.result == 'success') && + (needs.compute-signatures.result == 'success') + ) + + runs-on: ubuntu-latest + + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions-and-signatures + path: dist/ + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Create a GitHub Release for this tag. The description can be changed later, as for now + # we don't define it through this workflow. + run: >- + gh release create + '${{ github.ref_name }}' + --repo '${{ github.repository }}' + --generate-notes + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Upload to GitHub Release using the `gh` CLI. + # `dist/` contains the built packages, and the + # sigstore-produced signatures and certificates. + run: >- + gh release upload + '${{ github.ref_name }}' dist/** + --repo '${{ github.repository }}' + + github-test-release: + name: Upload to GitHub Release Draft + needs: + - publish-to-test-pypi + - compute-signatures + if: | + always() && ( + (needs.publish-to-test-pypi.result == 'success') && + (needs.compute-signatures.result == 'success') + ) + runs-on: ubuntu-latest + + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions-and-signatures + path: dist/ + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Create a GitHub Release *draft*. The description can be changed later, as for now + # we don't define it through this workflow. + run: >- + gh release create + '${{ github.ref_name }}' + --repo '${{ github.repository }}' + --generate-notes + --draft + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Upload to GitHub Release using the `gh` CLI. + # `dist/` contains the built packages, and the + # sigstore-produced signatures and certificates. + run: >- + gh release upload + '${{ github.ref_name }}' dist/** + --repo '${{ github.repository }}' diff --git a/README.rst b/README.rst index 4dceafa9bd3..2dabd378083 100644 --- a/README.rst +++ b/README.rst @@ -117,15 +117,19 @@ You can also install ``python-telegram-bot`` from source, though this is usually Verifying Releases ~~~~~~~~~~~~~~~~~~ -We sign all the releases with a GPG key. -The signatures are uploaded to both the `GitHub releases page `_ and the `PyPI project `_ and end with a suffix ``.asc``. +To enable you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team, we have taken the following measures. + +Starting with NEXT.VERSION, all releases are signed via `sigstore `_. +The corresponding signature files are uploaded to the `GitHub releases page`_. +To verify the signature, please install the `sigstore Python client `_ and follow the instructions for `verifying signatures from GitHub Actions `_. As input for the ``--repository`` parameter, please use the value ``python-telegram-bot/python-telegram-bot``. + +Earlier releases are signed with a GPG key. +The signatures are uploaded to both the `GitHub releases page`_ and the `PyPI project `_ and end with a suffix ``.asc``. Please find the public keys `here `_. -The keys are named in the format ``-.gpg`` or ``-current.gpg`` if the key is currently being used for new releases. +The keys are named in the format ``-.gpg``. In addition, the GitHub release page also contains the sha1 hashes of the release files in the files with the suffix ``.sha1``. -This allows you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team. - Dependencies & Their Versions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -227,3 +231,5 @@ License You may copy, distribute and modify the software provided that modifications are described and licensed for free under `LGPL-3 `_. Derivatives works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be. + +.. _`GitHub releases page`: https://github.com/python-telegram-bot/python-telegram-bot/releases> \ No newline at end of file diff --git a/public_keys/v20.0-current.gpg b/public_keys/v20.0-v21.3.gpg similarity index 100% rename from public_keys/v20.0-current.gpg rename to public_keys/v20.0-v21.3.gpg From 2ac4e009d02e991b46e5c4363446dc3bd6dbb8be Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:40:42 +0200 Subject: [PATCH 26/26] Bump version to v21.4 (#4371) --- CHANGES.rst | 52 +++++++++++++++++++ README.rst | 2 +- telegram/_bot.py | 18 +++---- telegram/_botcommandscope.py | 2 +- telegram/_chat.py | 2 +- telegram/_chatfullinfo.py | 4 +- telegram/_files/inputmedia.py | 6 +-- telegram/_menubutton.py | 2 +- telegram/_message.py | 28 +++++----- telegram/_messageentity.py | 2 +- telegram/_paidmedia.py | 10 ++-- .../_passport/encryptedpassportelement.py | 4 +- telegram/_passport/passportfile.py | 8 +-- telegram/_payment/stars.py | 22 ++++---- telegram/_reply.py | 4 +- telegram/_telegramobject.py | 4 +- telegram/_version.py | 2 +- telegram/constants.py | 16 +++--- telegram/ext/filters.py | 4 +- 19 files changed, 122 insertions(+), 70 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 79a6f124496..f56a61b9b0c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,58 @@ Changelog ========= +Version 21.4 +============ + +*Released 2024-07-12* + +This is the technical changelog for version 21.4. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support for Bot API 7.5 (:pr:`4328`, :pr:`4316`, :pr:`4315`, :pr:`4312` closes :issue:`4310`, :pr:`4311`) +- Full Support for Bot API 7.6 (:pr:`4333` closes :issue:`4331`, :pr:`4344`, :pr:`4341`, :pr:`4334`, :pr:`4335`, :pr:`4351`, :pr:`4342`, :pr:`4348`) +- Full Support for Bot API 7.7 (:pr:`4356` closes :issue:`4355`) +- Drop ``python-telegram-bot-raw`` And Switch to ``pyproject.toml`` Based Packaging (:pr:`4288` closes :issue:`4129` and :issue:`4296`) +- Deprecate Inclusion of ``successful_payment`` in ``Message.effective_attachment`` (:pr:`4365` closes :issue:`4350`) + +New Features +------------ + +- Add Support for Python 3.13 Beta (:pr:`4253`) +- Add ``filters.PAID_MEDIA`` (:pr:`4357`) +- Log Received Data on Deserialization Errors (:pr:`4304`) +- Add ``MessageEntity.adjust_message_entities_to_utf_16`` Utility Function (:pr:`4323` by `Antares0982 `_ closes :issue:`4319`) +- Make Argument ``bot`` of ``TelegramObject.de_json`` Optional (:pr:`4320`) + +Documentation Improvements +-------------------------- + +- Documentation Improvements (:pr:`4303` closes :issue:`4301`) +- Restructure Readme (:pr:`4362`) +- Fix Link-Check Workflow (:pr:`4332`) + +Internal Changes +---------------- + +- Automate PyPI Releases (:pr:`4364` closes :issue:`4318`) +- Add ``mise-en-place`` to ``.gitignore`` (:pr:`4300`) +- Use a Composite Action for Testing Type Completeness (:pr:`4367`) +- Stabilize Some Concurrency Usages in Test Suite (:pr:`4360`) +- Add a Test Case for ``MenuButton`` (:pr:`4363`) +- Extend ``SuccessfulPayment`` Test (:pr:`4349`) +- Small Fixes for ``test_stars.py`` (:pr:`4347`) +- Use Python 3.13 Beta 3 in Test Suite (:pr:`4336`) + +Dependency Updates +------------------ + +- Bump ``ruff`` and Add New Rules (:pr:`4329`) +- Bump ``pre-commit`` Hooks to Latest Versions (:pr:`4337`) +- Add Lower Bound for ``flaky`` Dependency (:pr:`4322` by `Palaptin `_) +- Bump ``pytest`` from 8.2.1 to 8.2.2 (:pr:`4294`) + Version 21.3 ============ *Released 2024-06-07* diff --git a/README.rst b/README.rst index 2dabd378083..09b01d923a8 100644 --- a/README.rst +++ b/README.rst @@ -119,7 +119,7 @@ Verifying Releases To enable you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team, we have taken the following measures. -Starting with NEXT.VERSION, all releases are signed via `sigstore `_. +Starting with v21.4, all releases are signed via `sigstore `_. The corresponding signature files are uploaded to the `GitHub releases page`_. To verify the signature, please install the `sigstore Python client `_ and follow the instructions for `verifying signatures from GitHub Actions `_. As input for the ``--repository`` parameter, please use the value ``python-telegram-bot/python-telegram-bot``. diff --git a/telegram/_bot.py b/telegram/_bot.py index ff1ccbb3766..4d8a778cc63 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -2856,7 +2856,7 @@ async def edit_message_live_location( .. versionadded:: 21.2. business_connection_id (:obj:`str`, optional): |business_id_str_edit| - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Keyword Args: location (:class:`telegram.Location`, optional): The location to send. @@ -2932,7 +2932,7 @@ async def stop_message_live_location( inline keyboard. business_connection_id (:obj:`str`, optional): |business_id_str_edit| - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the @@ -4006,7 +4006,7 @@ async def edit_message_text( inline keyboard. business_connection_id (:obj:`str`, optional): |business_id_str_edit| - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Keyword Args: disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in @@ -4104,7 +4104,7 @@ async def edit_message_caption( .. versionadded:: 21.3 business_connection_id (:obj:`str`, optional): |business_id_str_edit| - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the @@ -4177,7 +4177,7 @@ async def edit_message_media( inline keyboard. business_connection_id (:obj:`str`, optional): |business_id_str_edit| - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the @@ -4238,7 +4238,7 @@ async def edit_message_reply_markup( inline keyboard. business_connection_id (:obj:`str`, optional): |business_id_str_edit| - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the @@ -7185,7 +7185,7 @@ async def stop_poll( message inline keyboard. business_connection_id (:obj:`str`, optional): |business_id_str_edit| - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Returns: :class:`telegram.Poll`: On success, the stopped Poll is returned. @@ -9138,7 +9138,7 @@ async def get_star_transactions( ) -> StarTransactions: """Returns the bot's Telegram Star transactions in chronological order. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: offset (:obj:`int`, optional): Number of transactions to skip in the response. @@ -9193,7 +9193,7 @@ async def send_paid_media( ) -> Message: """Use this method to send paid media to channel chats. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| diff --git a/telegram/_botcommandscope.py b/telegram/_botcommandscope.py index 53e65610c0a..73cafd17599 100644 --- a/telegram/_botcommandscope.py +++ b/telegram/_botcommandscope.py @@ -95,7 +95,7 @@ def de_json( bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to :obj:`None`, in which case shortcut methods will not be available. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 :paramref:`bot` is now optional and defaults to :obj:`None` Returns: diff --git a/telegram/_chat.py b/telegram/_chat.py index 2513e0ff334..200e192c95f 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -3286,7 +3286,7 @@ async def send_paid_media( For the documentation of the arguments, please see :meth:`telegram.Bot.send_paid_media`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Returns: :class:`telegram.Message`: On success, instance representing the message posted. diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index d4bda7d415a..04898659c3c 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -198,7 +198,7 @@ class ChatFullInfo(_ChatBase): can_send_paid_media (:obj:`bool`, optional): :obj:`True`, if paid media messages can be sent or forwarded to the channel chat. The field is available only for channel chats. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Attributes: id (:obj:`int`): Unique identifier for this chat. @@ -352,7 +352,7 @@ class ChatFullInfo(_ChatBase): can_send_paid_media (:obj:`bool`): Optional. :obj:`True`, if paid media messages can be sent or forwarded to the channel chat. The field is available only for channel chats. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 .. _accent colors: https://core.telegram.org/bots/api#accent-colors .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 3715682fa3b..692369130a4 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -124,7 +124,7 @@ class InputPaidMedia(TelegramObject): .. seealso:: :wiki:`Working with Files and Media ` - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: type (:obj:`str`): Type of media that the instance represents. @@ -164,7 +164,7 @@ class InputPaidMediaPhoto(InputPaidMedia): .. seealso:: :wiki:`Working with Files and Media ` - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ @@ -196,7 +196,7 @@ class InputPaidMediaVideo(InputPaidMedia): .. seealso:: :wiki:`Working with Files and Media ` - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Note: * When using a :class:`telegram.Video` for the :attr:`media` attribute, it will take the diff --git a/telegram/_menubutton.py b/telegram/_menubutton.py index ef89d8a53eb..50b6511b08d 100644 --- a/telegram/_menubutton.py +++ b/telegram/_menubutton.py @@ -80,7 +80,7 @@ def de_json( bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to :obj:`None`, in which case shortcut methods will not be available. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 :paramref:`bot` is now optional and defaults to :obj:`None` Returns: diff --git a/telegram/_message.py b/telegram/_message.py index edf9fb40143..fceb8cb8768 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -582,11 +582,11 @@ class Message(MaybeInaccessibleMessage): paid_media (:obj:`telegram.PaidMediaInfo`, optional): Message contains paid media; information about the paid media. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 refunded_payment (:obj:`telegram.RefundedPayment`, optional): Message is a service message about a refunded payment, information about the payment. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -909,11 +909,11 @@ class Message(MaybeInaccessibleMessage): paid_media (:obj:`telegram.PaidMediaInfo`): Optional. Message contains paid media; information about the paid media. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 refunded_payment (:obj:`telegram.RefundedPayment`): Optional. Message is a service message about a refunded payment, information about the payment. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a @@ -1426,10 +1426,10 @@ def effective_attachment( :attr:`dice`, :attr:`passport_data` and :attr:`poll` are now also considered to be an attachment. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 :attr:`paid_media` is now also considered to be an attachment. - .. deprecated:: NEXT.VERSION + .. deprecated:: 21.4 :attr:`successful_payment` will be removed in future major versions. """ @@ -1442,7 +1442,7 @@ def effective_attachment( if attachment_type == MessageAttachmentType.SUCCESSFUL_PAYMENT: warn( PTBDeprecationWarning( - "NEXT.VERSION", + "21.4", "successful_payment will no longer be considered an attachment in" " future major versions", ), @@ -3649,7 +3649,7 @@ async def edit_text( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 Now also passes :attr:`business_connection_id`. Returns: @@ -3706,7 +3706,7 @@ async def edit_caption( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 Now also passes :attr:`business_connection_id`. Returns: @@ -3759,7 +3759,7 @@ async def edit_media( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 Now also passes :attr:`business_connection_id`. Returns: @@ -3808,7 +3808,7 @@ async def edit_reply_markup( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 Now also passes :attr:`business_connection_id`. Returns: @@ -3862,7 +3862,7 @@ async def edit_live_location( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 Now also passes :attr:`business_connection_id`. Returns: @@ -3916,7 +3916,7 @@ async def stop_live_location( of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 Now also passes :attr:`business_connection_id`. Returns: @@ -4072,7 +4072,7 @@ async def stop_poll( For the documentation of the arguments, please see :meth:`telegram.Bot.stop_poll`. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 Now also passes :attr:`business_connection_id`. Returns: diff --git a/telegram/_messageentity.py b/telegram/_messageentity.py index bc80c2dcb3e..302f3a1c080 100644 --- a/telegram/_messageentity.py +++ b/telegram/_messageentity.py @@ -158,7 +158,7 @@ def adjust_message_entities_to_utf_16( For more information, see `Unicode `_ and `Plane (Unicode) `_. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Examples: Below is a snippet of code that demonstrates how to use this function to convert diff --git a/telegram/_paidmedia.py b/telegram/_paidmedia.py index 82594ada337..fe78cca28e0 100644 --- a/telegram/_paidmedia.py +++ b/telegram/_paidmedia.py @@ -42,7 +42,7 @@ class PaidMedia(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`type` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: type (:obj:`str`): Type of the paid media. @@ -114,7 +114,7 @@ class PaidMediaPreview(PaidMedia): considered equal, if their :attr:`width`, :attr:`height`, and :attr:`duration` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`. @@ -156,7 +156,7 @@ class PaidMediaPhoto(PaidMedia): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`photo` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PHOTO`. @@ -202,7 +202,7 @@ class PaidMediaVideo(PaidMedia): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`video` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.VIDEO`. @@ -248,7 +248,7 @@ class PaidMediaInfo(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`star_count` and :attr:`paid_media` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to diff --git a/telegram/_passport/encryptedpassportelement.py b/telegram/_passport/encryptedpassportelement.py index 76eb8e51f54..b05003f2cbd 100644 --- a/telegram/_passport/encryptedpassportelement.py +++ b/telegram/_passport/encryptedpassportelement.py @@ -222,10 +222,10 @@ def de_json_decrypted( bot (:class:`telegram.Bot` | :obj:`None`): The bot associated with these object. May be :obj:`None`, in which case shortcut methods will not be available. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 :paramref:`bot` is now optional and defaults to :obj:`None` - .. deprecated:: NEXT.VERSION + .. deprecated:: 21.4 This argument will be converted to an optional argument in future versions. credentials (:class:`telegram.FileCredentials`): The credentials diff --git a/telegram/_passport/passportfile.py b/telegram/_passport/passportfile.py index 3ae4a42e81f..61b70486279 100644 --- a/telegram/_passport/passportfile.py +++ b/telegram/_passport/passportfile.py @@ -128,10 +128,10 @@ def de_json_decrypted( bot (:class:`telegram.Bot` | :obj:`None`): The bot associated with these object. May be :obj:`None`, in which case shortcut methods will not be available. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 :paramref:`bot` is now optional and defaults to :obj:`None` - .. deprecated:: NEXT.VERSION + .. deprecated:: 21.4 This argument will be converted to an optional argument in future versions. credentials (:class:`telegram.FileCredentials`): The credentials @@ -168,10 +168,10 @@ def de_list_decrypted( bot (:class:`telegram.Bot` | :obj:`None`): The bot associated with these object. May be :obj:`None`, in which case shortcut methods will not be available. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 :paramref:`bot` is now optional and defaults to :obj:`None` - .. deprecated:: NEXT.VERSION + .. deprecated:: 21.4 This argument will be converted to an optional argument in future versions. credentials (:class:`telegram.FileCredentials`): The credentials diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index 6c537532c37..b176f2315fe 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -45,7 +45,7 @@ class RevenueWithdrawalState(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`type` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: type (:obj:`str`): The type of the state. @@ -94,7 +94,7 @@ def de_json( class RevenueWithdrawalStatePending(RevenueWithdrawalState): """The withdrawal is in progress. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Attributes: type (:obj:`str`): The type of the state, always @@ -114,7 +114,7 @@ class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`date` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object. @@ -165,7 +165,7 @@ def de_json( class RevenueWithdrawalStateFailed(RevenueWithdrawalState): """The withdrawal failed and the transaction was refunded. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Attributes: type (:obj:`str`): The type of the state, always @@ -191,7 +191,7 @@ class TransactionPartner(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`type` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: type (:obj:`str`): The type of the transaction partner. @@ -257,7 +257,7 @@ def de_json( class TransactionPartnerFragment(TransactionPartner): """Describes a withdrawal transaction with Fragment. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: withdrawal_state (:obj:`telegram.RevenueWithdrawalState`, optional): State of the @@ -305,7 +305,7 @@ class TransactionPartnerUser(TransactionPartner): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`user` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: user (:class:`telegram.User`): Information about the user. @@ -354,7 +354,7 @@ def de_json( class TransactionPartnerOther(TransactionPartner): """Describes a transaction with an unknown partner. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Attributes: type (:obj:`str`): The type of the transaction partner, @@ -371,7 +371,7 @@ def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: class TransactionPartnerTelegramAds(TransactionPartner): """Describes a withdrawal transaction to the Telegram Ads platform. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Attributes: type (:obj:`str`): The type of the transaction partner, @@ -391,7 +391,7 @@ class StarTransaction(TelegramObject): 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. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer @@ -474,7 +474,7 @@ class StarTransactions(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`transactions` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Args: transactions (Sequence[:class:`telegram.StarTransaction`]): The list of transactions. diff --git a/telegram/_reply.py b/telegram/_reply.py index 2aefa15085a..0c15844c8d5 100644 --- a/telegram/_reply.py +++ b/telegram/_reply.py @@ -108,7 +108,7 @@ class ExternalReplyInfo(TelegramObject): paid_media (:class:`telegram.PaidMedia`, optional): Message contains paid media; information about the paid media. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 Attributes: origin (:class:`telegram.MessageOrigin`): Origin of the message replied to by the given @@ -158,7 +158,7 @@ class ExternalReplyInfo(TelegramObject): paid_media (:class:`telegram.PaidMedia`): Optional. Message contains paid media; information about the paid media. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ __slots__ = ( diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index 04275b9a928..4f7ba92d602 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -442,7 +442,7 @@ def de_json( bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to :obj:`None`, in which case shortcut methods will not be available. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 :paramref:`bot` is now optional and defaults to :obj:`None` Returns: @@ -467,7 +467,7 @@ def de_list( bot (:class:`telegram.Bot`, optional): The bot associated with these object. Defaults to :obj:`None`, in which case shortcut methods will not be available. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.4 :paramref:`bot` is now optional and defaults to :obj:`None` Returns: diff --git a/telegram/_version.py b/telegram/_version.py index 557a1ab9022..ec3f5618c21 100644 --- a/telegram/_version.py +++ b/telegram/_version.py @@ -51,6 +51,6 @@ def __str__(self) -> str: __version_info__: Final[Version] = Version( - major=21, minor=3, micro=0, releaselevel="final", serial=0 + major=21, minor=4, micro=0, releaselevel="final", serial=0 ) __version__: Final[str] = str(__version_info__) diff --git a/telegram/constants.py b/telegram/constants.py index 9cd41b5f3a2..fb4bc9a19a9 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -1265,7 +1265,7 @@ class InputPaidMediaType(StringEnum): """This enum contains the available types of :class:`telegram.InputPaidMedia`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ __slots__ = () @@ -1622,7 +1622,7 @@ class MessageAttachmentType(StringEnum): PAID_MEDIA = "paid_media" """:obj:`str`: Messages with :attr:`telegram.Message.paid_media`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ PASSPORT_DATA = "passport_data" """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" @@ -1908,7 +1908,7 @@ class MessageType(StringEnum): PAID_MEDIA = "paid_media" """:obj:`str`: Messages with :attr:`telegram.Message.paid_media`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ PASSPORT_DATA = "passport_data" """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" @@ -1923,7 +1923,7 @@ class MessageType(StringEnum): REFUNDED_PAYMENT = "refunded_payment" """:obj:`str`: Messages with :attr:`telegram.Message.refunded_payment`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ REPLY_TO_STORY = "reply_to_story" """:obj:`str`: Messages with :attr:`telegram.Message.reply_to_story`. @@ -1988,7 +1988,7 @@ class PaidMediaType(StringEnum): This enum contains the available types of :class:`telegram.PaidMedia`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ __slots__ = () @@ -2360,7 +2360,7 @@ class RevenueWithdrawalStateType(StringEnum): """This enum contains the available types of :class:`telegram.RevenueWithdrawalState`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ __slots__ = () @@ -2377,7 +2377,7 @@ class StarTransactionsLimit(IntEnum): """This enum contains limitations for :class:`telegram.Bot.get_star_transactions`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ __slots__ = () @@ -2529,7 +2529,7 @@ class TransactionPartnerType(StringEnum): """This enum contains the available types of :class:`telegram.TransactionPartner`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ __slots__ = () diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index bc6b06fda28..de105e28b6a 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1717,7 +1717,7 @@ def filter(self, message: Message) -> bool: PAID_MEDIA = _PaidMedia(name="filters.PAID_MEDIA") """Messages that contain :attr:`telegram.Message.paid_media`. -.. versionadded:: NEXT.VERSION +.. versionadded:: 21.4 """ @@ -2214,7 +2214,7 @@ def filter(self, message: Message) -> bool: REFUNDED_PAYMENT = _RefundedPayment("filters.StatusUpdate.REFUNDED_PAYMENT") """Messages that contain :attr:`telegram.Message.refunded_payment`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.4 """ class _UserShared(MessageFilter):