From 76d8eaf0f275bf6690f6623136d138f349a1d4e2 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:35:16 +0100 Subject: [PATCH 01/36] Add `chango` As Changelog Management Tool (#4672) --- .../workflows/assets/release_template.html | 5 ++ .github/workflows/chango_fragment.yml | 29 +++++++ .github/workflows/docs-admonitions.yml | 2 +- .github/workflows/docs-linkcheck.yml | 2 +- .github/workflows/release_pypi.yml | 31 +++++++- .github/workflows/release_test_pypi.yml | 7 +- CHANGES.rst => changes/LEGACY.rst | 6 -- changes/config.py | 78 +++++++++++++++++++ changes/unreleased/.gitkeep | 0 .../4672.G9szuJ7pRafycByfem2DrT.toml | 5 ++ docs/requirements-docs.txt | 1 + docs/source/changelog.rst | 10 ++- docs/source/conf.py | 9 +++ pyproject.toml | 5 ++ 14 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/assets/release_template.html create mode 100644 .github/workflows/chango_fragment.yml rename CHANGES.rst => changes/LEGACY.rst (99%) create mode 100644 changes/config.py create mode 100644 changes/unreleased/.gitkeep create mode 100644 changes/unreleased/4672.G9szuJ7pRafycByfem2DrT.toml diff --git a/.github/workflows/assets/release_template.html b/.github/workflows/assets/release_template.html new file mode 100644 index 00000000000..2e672ca8482 --- /dev/null +++ b/.github/workflows/assets/release_template.html @@ -0,0 +1,5 @@ +We've just released {tag}. +Thank you to everyone who contributed to this release. +As usual, upgrade using pip install -U python-telegram-bot. + +The release notes can be found here. \ No newline at end of file diff --git a/.github/workflows/chango_fragment.yml b/.github/workflows/chango_fragment.yml new file mode 100644 index 00000000000..a08e4efbb66 --- /dev/null +++ b/.github/workflows/chango_fragment.yml @@ -0,0 +1,29 @@ +name: Create Chango Fragment +on: + pull_request: + types: + - opened + - reopened + +permissions: {} + +jobs: + create-chango-fragment: + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push the + # added or changed files to the repository. + contents: write + name: create-chango-fragment + runs-on: ubuntu-latest + steps: + + # Create the new fragment + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: Bibo-Joshi/chango@176c53372d1f8c0f1f6caabde100077bc45d41cc # v0.3.1 + with: + github-token: ${{ secrets.CHANGO_PAT }} + query-issue-types: true + + diff --git a/.github/workflows/docs-admonitions.yml b/.github/workflows/docs-admonitions.yml index 90ab6f8e97c..52dac4cef94 100644 --- a/.github/workflows/docs-admonitions.yml +++ b/.github/workflows/docs-admonitions.yml @@ -20,7 +20,7 @@ jobs: actions: write strategy: matrix: - python-version: ['3.10'] + python-version: ['3.12'] os: [ubuntu-latest] fail-fast: False steps: diff --git a/.github/workflows/docs-linkcheck.yml b/.github/workflows/docs-linkcheck.yml index d4b28122840..1c6f56ab8bc 100644 --- a/.github/workflows/docs-linkcheck.yml +++ b/.github/workflows/docs-linkcheck.yml @@ -15,7 +15,7 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - python-version: ['3.10'] + python-version: ['3.12'] os: [ubuntu-latest] fail-fast: False steps: diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index 73ae5d8c7eb..fccb7d65c0b 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -119,13 +119,14 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} TAG: ${{ needs.build.outputs.TAG }} - # Create a tag and a GitHub Release. The description can be changed later, as for now - # we don't define it through this workflow. + # Create a tag and a GitHub Release. The description is filled by the static template, we + # just insert the correct tag in the template. run: >- + sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html | gh release create "$TAG" --repo '${{ github.repository }}' - --generate-notes + --notes-file - - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} @@ -137,3 +138,27 @@ jobs: gh release upload "$TAG" dist/** --repo '${{ github.repository }}' + + telegram-channel: + name: Publish to Telegram Channel + needs: + - github-release + + runs-on: ubuntu-latest + environment: + name: release_pypi + permissions: {} + + steps: + - name: Publish to Telegram Channel + env: + TAG: ${{ needs.build.outputs.TAG }} + # This secret is configured only for the `pypi-release` branch + BOT_TOKEN: ${{ secrets.CHANNEL_BOT_TOKEN }} + run: >- + sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html | + curl + -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" + -d "chat_id=@pythontelegrambotchannel" + -d "parse_mode=HTML" + --data-urlencode "text@-" diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index b50ee80a4b1..0f773d0e255 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -121,14 +121,15 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} TAG: ${{ needs.build.outputs.TAG }} - # Create a GitHub Release *draft*. The description can be changed later, as for now - # we don't define it through this workflow. + # Create a tag and a GitHub Release *draft*. The description is filled by the static + # template, we just insert the correct tag in the template. run: >- + sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html | gh release create "$TAG" --repo '${{ github.repository }}' - --generate-notes --draft + --notes-file - - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} diff --git a/CHANGES.rst b/changes/LEGACY.rst similarity index 99% rename from CHANGES.rst rename to changes/LEGACY.rst index aea00e464b0..81c4205cc29 100644 --- a/CHANGES.rst +++ b/changes/LEGACY.rst @@ -1,9 +1,3 @@ -.. _ptb-changelog: - -========= -Changelog -========= - Version 21.11.1 =============== diff --git a/changes/config.py b/changes/config.py new file mode 100644 index 00000000000..d7557990bad --- /dev/null +++ b/changes/config.py @@ -0,0 +1,78 @@ +# noqa: INP001 +# pylint: disable=import-error +"""Configuration for the chango changelog tool""" + +from collections.abc import Collection +from typing import Optional + +from chango.concrete import DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory +from chango.concrete.sections import GitHubSectionChangeNote, Section, SectionVersionNote + +version_scanner = DirectoryVersionScanner(base_directory=".", unreleased_directory="unreleased") + + +class ChangoSectionChangeNote( + GitHubSectionChangeNote.with_sections( # type: ignore[misc] + [ + Section(uid="highlights", title="Highlights", sort_order=0), + Section(uid="breaking", title="Breaking Changes", sort_order=1), + Section(uid="security", title="Security Changes", sort_order=2), + Section(uid="deprecations", title="Deprecations", sort_order=3), + Section(uid="features", title="New Features", sort_order=4), + Section(uid="bugfixes", title="Bug Fixes", sort_order=5), + Section(uid="dependencies", title="Dependencies", sort_order=6), + Section(uid="other", title="Other Changes", sort_order=7), + Section(uid="documentation", title="Documentation", sort_order=8), + Section(uid="internal", title="Internal Changes", sort_order=9), + ] + ) +): + """Custom change note type for PTB. Mainly overrides get_sections to map labels to sections""" + + OWNER = "python-telegram-bot" + REPOSITORY = "python-telegram-bot" + + @classmethod + def get_sections( + cls, + labels: Collection[str], + issue_types: Optional[Collection[str]], + ) -> set[str]: + """Override get_sections to have customized auto-detection of relevant sections based on + the pull request and linked issues. Certainly not perfect in all cases, but should be a + good start for most PRs. + """ + combined_labels = set(labels) | (set(issue_types or [])) + + mapping = { + "๐Ÿ› bug": "bugfixes", + "๐Ÿ’ก feature": "features", + "๐Ÿงน chore": "internal", + "โš™๏ธ bot-api": "features", + "โš™๏ธ documentation": "documentation", + "โš™๏ธ tests": "internal", + "โš™๏ธ ci-cd": "internal", + "โš™๏ธ security": "security", + "โš™๏ธ examples": "documentation", + "โš™๏ธ type-hinting": "other", + "๐Ÿ›  refactor": "internal", + "๐Ÿ›  breaking": "breaking", + "โš™๏ธ dependencies": "dependencies", + "๐Ÿ”— github-actions": "internal", + "๐Ÿ›  code-quality": "internal", + } + + # we want to return *all* from the mapping that are in the combined_labels + # removing superfluous sections from the fragment is a tad easier than adding them + found = {section for label, section in mapping.items() if label in combined_labels} + + # if we have not found any sections, we default to "other" + return found or {"other"} + + +chango_instance = DirectoryChanGo( + change_note_type=ChangoSectionChangeNote, + version_note_type=SectionVersionNote, + version_history_type=HeaderVersionHistory, + scanner=version_scanner, +) diff --git a/changes/unreleased/.gitkeep b/changes/unreleased/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/changes/unreleased/4672.G9szuJ7pRafycByfem2DrT.toml b/changes/unreleased/4672.G9szuJ7pRafycByfem2DrT.toml new file mode 100644 index 00000000000..c13a2d145df --- /dev/null +++ b/changes/unreleased/4672.G9szuJ7pRafycByfem2DrT.toml @@ -0,0 +1,5 @@ +documentation = "Add `chango `_ As Changelog Management Tool" +[[pull_requests]] +uid = "4672" +author_uid = "Bibo-Joshi" +closes_threads = ["4321"] diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ab62b887813..2bd871ed123 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,4 @@ +chango~=0.3.2 sphinx==8.1.3 furo==2024.8.6 furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 1cb32f6be91..c5be34d2b04 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1 +1,9 @@ -.. include:: ../../CHANGES.rst \ No newline at end of file +.. _ptb-changelog: + +========= +Changelog +========= + +.. chango:: + +.. include:: ../../changes/LEGACY.rst \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index b00ba68e204..fbb8b43168e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -8,6 +8,11 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. from sphinx.application import Sphinx +if sys.version_info < (3, 12): + # Due to dependency on chango + raise RuntimeError("This documentation needs at least Python 3.12") + + sys.path.insert(0, str(Path("../..").resolve().absolute())) # -- General configuration ------------------------------------------------ @@ -36,6 +41,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "chango.sphinx_ext", "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", @@ -52,6 +58,9 @@ if os.environ.get("READTHEDOCS", "") == "True": extensions.append("sphinx_build_compatibility.extension") +# Configuration for the chango sphinx directive +chango_pyproject_toml_path = Path(__file__).parent.parent.parent + # For shorter links to Wiki in docstrings extlinks = { "wiki": ("https://github.com/python-telegram-bot/python-telegram-bot/wiki/%s", "%s"), diff --git a/pyproject.toml b/pyproject.toml index ef6556ecef8..f036d57a533 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,11 @@ search-paths = ["telegram"] [tool.hatch.build] packages = ["telegram"] +# CHANGO +[tool.chango] +sys_path = "changes" +chango_instance = { name= "chango_instance", module = "config" } + # BLACK: [tool.black] line-length = 99 From a5dacab8a912b01e7b13013e8a9f2ec1849ebfd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 10:54:18 +0100 Subject: [PATCH 02/36] Bump github/codeql-action from 3.28.8 to 3.28.10 (#4697) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/gha_security.yml | 2 +- changes/unreleased/4697.GzyGCEgj74G6bTEDbuFjUU.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4697.GzyGCEgj74G6bTEDbuFjUU.toml diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml index bd7a8fafe1b..b925fff38a7 100644 --- a/.github/workflows/gha_security.yml +++ b/.github/workflows/gha_security.yml @@ -27,7 +27,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 with: sarif_file: results.sarif category: zizmor \ No newline at end of file diff --git a/changes/unreleased/4697.GzyGCEgj74G6bTEDbuFjUU.toml b/changes/unreleased/4697.GzyGCEgj74G6bTEDbuFjUU.toml new file mode 100644 index 00000000000..d1bdf38d738 --- /dev/null +++ b/changes/unreleased/4697.GzyGCEgj74G6bTEDbuFjUU.toml @@ -0,0 +1,5 @@ +internal = "Bump github/codeql-action from 3.28.8 to 3.28.10" +[[pull_requests]] +uid = "4697" +author_uid = "dependabot[bot]" +closes_threads = [] From d5e5874f960afeec01d6533e6c170552b0e09b6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 10:54:56 +0100 Subject: [PATCH 03/36] Bump srvaroa/labeler from 1.12.0 to 1.13.0 (#4698) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/labelling.yml | 2 +- changes/unreleased/4698.Fkzr3oU2qcFmFX28xfoja5.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4698.Fkzr3oU2qcFmFX28xfoja5.toml diff --git a/.github/workflows/labelling.yml b/.github/workflows/labelling.yml index 24b01a71dfc..21a4d6733ba 100644 --- a/.github/workflows/labelling.yml +++ b/.github/workflows/labelling.yml @@ -13,7 +13,7 @@ jobs: pull-requests: write # for srvaroa/labeler to add labels in PR runs-on: ubuntu-latest steps: - - uses: srvaroa/labeler@fe4b1c73bb8abf2f14a44a6912a8b4fee835d631 # v1.12.0 + - uses: srvaroa/labeler@0a20eccb8c94a1ee0bed5f16859aece1c45c3e55 # v1.13.0 # Config file at .github/labeler.yml env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/changes/unreleased/4698.Fkzr3oU2qcFmFX28xfoja5.toml b/changes/unreleased/4698.Fkzr3oU2qcFmFX28xfoja5.toml new file mode 100644 index 00000000000..6ba893a7200 --- /dev/null +++ b/changes/unreleased/4698.Fkzr3oU2qcFmFX28xfoja5.toml @@ -0,0 +1,5 @@ +internal = "Bump srvaroa/labeler from 1.12.0 to 1.13.0" +[[pull_requests]] +uid = "4698" +author_uid = "dependabot[bot]" +closes_threads = [] From 892a66d0e87391cfe24553fa6512b49dc2692a02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 10:56:11 +0100 Subject: [PATCH 04/36] Bump astral-sh/setup-uv from 5.2.2 to 5.3.1 (#4699) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/gha_security.yml | 2 +- changes/unreleased/4699.LYAYfKXX7C7wqT54kRPnVy.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4699.LYAYfKXX7C7wqT54kRPnVy.toml diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml index b925fff38a7..aaa72e61cae 100644 --- a/.github/workflows/gha_security.yml +++ b/.github/workflows/gha_security.yml @@ -21,7 +21,7 @@ jobs: with: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2 + uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5.3.1 - name: Run zizmor run: uvx zizmor --persona=pedantic --format sarif . > results.sarif env: diff --git a/changes/unreleased/4699.LYAYfKXX7C7wqT54kRPnVy.toml b/changes/unreleased/4699.LYAYfKXX7C7wqT54kRPnVy.toml new file mode 100644 index 00000000000..23bfcbfd4c7 --- /dev/null +++ b/changes/unreleased/4699.LYAYfKXX7C7wqT54kRPnVy.toml @@ -0,0 +1,5 @@ +internal = "Bump astral-sh/setup-uv from 5.2.2 to 5.3.1" +[[pull_requests]] +uid = "4699" +author_uid = "dependabot[bot]" +closes_threads = [] From 753f3727aad33d315c0a919c2a164d13e67e6d3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 10:56:42 +0100 Subject: [PATCH 05/36] Bump Bibo-Joshi/chango from 0.3.1 to 0.3.2 (#4700) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/chango_fragment.yml | 2 +- changes/unreleased/4700.nm6R6YTnkmCo5evbykz4kz.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4700.nm6R6YTnkmCo5evbykz4kz.toml diff --git a/.github/workflows/chango_fragment.yml b/.github/workflows/chango_fragment.yml index a08e4efbb66..b1079d03f28 100644 --- a/.github/workflows/chango_fragment.yml +++ b/.github/workflows/chango_fragment.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Bibo-Joshi/chango@176c53372d1f8c0f1f6caabde100077bc45d41cc # v0.3.1 + - uses: Bibo-Joshi/chango@969684469005dd2e03451f15bfd810f2338bf072 # v0.3.2 with: github-token: ${{ secrets.CHANGO_PAT }} query-issue-types: true diff --git a/changes/unreleased/4700.nm6R6YTnkmCo5evbykz4kz.toml b/changes/unreleased/4700.nm6R6YTnkmCo5evbykz4kz.toml new file mode 100644 index 00000000000..6dc7d8a9e07 --- /dev/null +++ b/changes/unreleased/4700.nm6R6YTnkmCo5evbykz4kz.toml @@ -0,0 +1,5 @@ +internal = "Bump Bibo-Joshi/chango from 0.3.1 to 0.3.2" +[[pull_requests]] +uid = "4700" +author_uid = "dependabot[bot]" +closes_threads = [] From 519dee7e0cd6dd8cec602689b4ee8ddf60b8e908 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 10:57:12 +0100 Subject: [PATCH 06/36] Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4 (#4701) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/release_pypi.yml | 2 +- .github/workflows/release_test_pypi.yml | 2 +- changes/unreleased/4701.ah8Wi4SWc22EbgBc4KQeqH.toml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changes/unreleased/4701.ah8Wi4SWc22EbgBc4KQeqH.toml diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index fccb7d65c0b..c59a4579561 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -60,7 +60,7 @@ jobs: name: python-package-distributions path: dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 compute-signatures: name: Compute SHA1 Sums and Sign with Sigstore diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index 0f773d0e255..ac8872ff509 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -60,7 +60,7 @@ jobs: name: python-package-distributions path: dist/ - name: Publish to Test PyPI - uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 with: repository-url: https://test.pypi.org/legacy/ diff --git a/changes/unreleased/4701.ah8Wi4SWc22EbgBc4KQeqH.toml b/changes/unreleased/4701.ah8Wi4SWc22EbgBc4KQeqH.toml new file mode 100644 index 00000000000..a402f0e8990 --- /dev/null +++ b/changes/unreleased/4701.ah8Wi4SWc22EbgBc4KQeqH.toml @@ -0,0 +1,5 @@ +internal = "Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4" +[[pull_requests]] +uid = "4701" +author_uid = "dependabot[bot]" +closes_threads = [] From 8266870ed74182ce60969e54dc31e3c32dcb480a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Mar 2025 19:55:08 +0100 Subject: [PATCH 07/36] Bump pytest from 8.3.4 to 8.3.5 (#4709) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- changes/unreleased/4709.dbwPVaU8vSacVkMLhiMjyJ.toml | 5 +++++ requirements-unit-tests.txt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4709.dbwPVaU8vSacVkMLhiMjyJ.toml diff --git a/changes/unreleased/4709.dbwPVaU8vSacVkMLhiMjyJ.toml b/changes/unreleased/4709.dbwPVaU8vSacVkMLhiMjyJ.toml new file mode 100644 index 00000000000..a697b214f38 --- /dev/null +++ b/changes/unreleased/4709.dbwPVaU8vSacVkMLhiMjyJ.toml @@ -0,0 +1,5 @@ +internal = "Bump pytest from 8.3.4 to 8.3.5" +[[pull_requests]] +uid = "4709" +author_uid = "dependabot[bot]" +closes_threads = [] diff --git a/requirements-unit-tests.txt b/requirements-unit-tests.txt index a9c4ba3c2c9..f90d2950f60 100644 --- a/requirements-unit-tests.txt +++ b/requirements-unit-tests.txt @@ -4,7 +4,7 @@ build # For the test suite -pytest==8.3.4 +pytest==8.3.5 # needed because pytest doesn't come with native support for coroutines as tests pytest-asyncio==0.21.2 From 6d7134608f2f238cc33764d4e6b2e2f5d101c459 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Mar 2025 19:55:21 +0100 Subject: [PATCH 08/36] Bump sphinx from 8.1.3 to 8.2.3 (#4710) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- changes/unreleased/4710.CSNixpvxJdLFaM6xSQ39Zf.toml | 5 +++++ docs/requirements-docs.txt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4710.CSNixpvxJdLFaM6xSQ39Zf.toml diff --git a/changes/unreleased/4710.CSNixpvxJdLFaM6xSQ39Zf.toml b/changes/unreleased/4710.CSNixpvxJdLFaM6xSQ39Zf.toml new file mode 100644 index 00000000000..2d7787a14a9 --- /dev/null +++ b/changes/unreleased/4710.CSNixpvxJdLFaM6xSQ39Zf.toml @@ -0,0 +1,5 @@ +internal = "Bump sphinx from 8.1.3 to 8.2.3" +[[pull_requests]] +uid = "4710" +author_uid = "dependabot[bot]" +closes_threads = [] diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 2bd871ed123..10c93e18f9a 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ chango~=0.3.2 -sphinx==8.1.3 +sphinx==8.2.3 furo==2024.8.6 furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1 sphinx-paramlinks==0.6.0 From 150328799a5f5e766ccdae98180c1236bb204cee Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 9 Mar 2025 16:02:24 +0100 Subject: [PATCH 09/36] Bump Bibo-Joshi/chango from 0.3.2 to 0.4.0 (#4712) --- .github/workflows/chango_fragment.yml | 3 ++- changes/unreleased/4712.8ckkAPAXGzedityWEyv363.toml | 5 +++++ docs/requirements-docs.txt | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changes/unreleased/4712.8ckkAPAXGzedityWEyv363.toml diff --git a/.github/workflows/chango_fragment.yml b/.github/workflows/chango_fragment.yml index b1079d03f28..8a1cc25984c 100644 --- a/.github/workflows/chango_fragment.yml +++ b/.github/workflows/chango_fragment.yml @@ -4,6 +4,7 @@ on: types: - opened - reopened + - synchronize permissions: {} @@ -21,7 +22,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Bibo-Joshi/chango@969684469005dd2e03451f15bfd810f2338bf072 # v0.3.2 + - uses: Bibo-Joshi/chango@9d6bd9d7612eca5fab2c5161687011be59baaf19 # v0.4.0 with: github-token: ${{ secrets.CHANGO_PAT }} query-issue-types: true diff --git a/changes/unreleased/4712.8ckkAPAXGzedityWEyv363.toml b/changes/unreleased/4712.8ckkAPAXGzedityWEyv363.toml new file mode 100644 index 00000000000..63f0ad0743e --- /dev/null +++ b/changes/unreleased/4712.8ckkAPAXGzedityWEyv363.toml @@ -0,0 +1,5 @@ +internal = "Bump Bibo-Joshi/chango from 0.3.2 to 0.4.0" +[[pull_requests]] +uid = "4712" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 10c93e18f9a..e207cc48175 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ -chango~=0.3.2 +chango~=0.4.0 sphinx==8.2.3 furo==2024.8.6 furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1 From 1cf000c806da7cbf49b455bf8a568a1ff7e8b1cd Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:21:37 +0100 Subject: [PATCH 10/36] Remove Functionality Deprecated in v20.x (#4671) --- .../4671.7B3boYRvHdGzsrZgkpKp7B.toml | 19 ++ docs/auxil/kwargs_insertion.py | 18 +- docs/auxil/sphinx_hooks.py | 8 +- docs/source/stability_policy.rst | 2 + telegram/_bot.py | 16 +- telegram/_inline/inlinequery.py | 16 +- telegram/_inline/inlinequeryresultsbutton.py | 15 +- telegram/_message.py | 252 +++++++----------- .../_passport/encryptedpassportelement.py | 4 - telegram/_passport/passportelementerrors.py | 89 ++----- telegram/_passport/passportfile.py | 51 ++-- telegram/constants.py | 26 +- telegram/ext/_application.py | 55 +--- telegram/ext/_applicationbuilder.py | 53 +--- telegram/ext/_defaults.py | 92 +------ telegram/ext/_updater.py | 65 +---- telegram/ext/filters.py | 45 +--- telegram/request/_baserequest.py | 36 +-- telegram/request/_httpxrequest.py | 22 +- .../test_passportelementerrorfiles.py | 20 +- ...st_passportelementerrortranslationfiles.py | 20 +- tests/_passport/test_passportfile.py | 38 ++- tests/auxil/bot_method_checks.py | 2 - tests/auxil/networking.py | 4 + tests/ext/test_application.py | 43 --- tests/ext/test_applicationbuilder.py | 50 +--- tests/ext/test_defaults.py | 34 +-- tests/ext/test_filters.py | 8 - tests/ext/test_ratelimiter.py | 8 + tests/ext/test_updater.py | 17 -- tests/request/test_request.py | 111 +------- tests/test_bot.py | 44 +-- tests/test_message.py | 74 ++--- tests/test_official/arg_type_checker.py | 2 - tests/test_official/exceptions.py | 7 - tests/test_official/test_official.py | 5 + 36 files changed, 357 insertions(+), 1014 deletions(-) create mode 100644 changes/unreleased/4671.7B3boYRvHdGzsrZgkpKp7B.toml diff --git a/changes/unreleased/4671.7B3boYRvHdGzsrZgkpKp7B.toml b/changes/unreleased/4671.7B3boYRvHdGzsrZgkpKp7B.toml new file mode 100644 index 00000000000..002b70d4ad5 --- /dev/null +++ b/changes/unreleased/4671.7B3boYRvHdGzsrZgkpKp7B.toml @@ -0,0 +1,19 @@ +breaking = """This release removes all functionality that was deprecated in v20.x. This is in line with our :ref:`stability policy `. +This includes the following changes: + +- Removed ``filters.CHAT`` (all messages have an associated chat) and ``filters.StatusUpdate.USER_SHARED`` (use ``filters.StatusUpdate.USERS_SHARED`` instead). +- Removed ``Defaults.disable_web_page_preview`` and ``Defaults.quote``. Use ``Defaults.link_preview_options`` and ``Defaults.do_quote`` instead. +- Removed ``ApplicationBuilder.(get_updates_)proxy_url`` and ``HTTPXRequest.proxy_url``. Use ``ApplicationBuilder.(get_updates_)proxy`` and ``HTTPXRequest.proxy`` instead. +- Removed the ``*_timeout`` arguments of ``Application.run_polling`` and ``Updater.start_webhook``. Instead, specify the values via ``ApplicationBuilder.get_updates_*_timeout``. +- Removed ``constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH``. Use ``constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`` instead. +- Removed the argument ``quote`` of ``Message.reply_*``. Use ``do_quote`` instead. +- Removed the superfluous ``EncryptedPassportElement.credentials`` without replacement. +- Changed attribute value of ``PassportFile.file_date`` from :obj:`int` to :class:`datetime.datetime`. Make sure to adjust your code accordingly. +- Changed the attribute value of ``PassportElementErrors.file_hashes`` from :obj:`list` to :obj:`tuple`. Make sure to adjust your code accordingly. +- Make ``BaseRequest.read_timeout`` an abstract property. If you subclass ``BaseRequest``, you need to implement this property. +- The default value for ``write_timeout`` now defaults to ``DEFAULT_NONE`` also for bot methods that send media. Previously, it was ``20``. If you subclass ``BaseRequest``, make sure to use your desired write timeout if ``RequestData.multipart_data`` is set. +""" +[[pull_requests]] +uid = "4671" +author_uid = "Bibo-Joshi" +closes_threads = ["4659"] diff --git a/docs/auxil/kwargs_insertion.py b/docs/auxil/kwargs_insertion.py index 02fc39c040a..997baf91581 100644 --- a/docs/auxil/kwargs_insertion.py +++ b/docs/auxil/kwargs_insertion.py @@ -47,29 +47,29 @@ "", ] -media_write_timeout_deprecation_methods = [ - "send_photo", +media_write_timeout_change_methods = [ + "add_sticker_to_set", + "create_new_sticker_set", + "send_animation", "send_audio", "send_document", + "send_media_group", + "send_photo", "send_sticker", "send_video", "send_video_note", - "send_animation", "send_voice", - "send_media_group", "set_chat_photo", "upload_sticker_file", - "add_sticker_to_set", - "create_new_sticker_set", ] -media_write_timeout_deprecation = [ +media_write_timeout_change = [ " write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to " " :paramref:`telegram.request.BaseRequest.post.write_timeout`. By default, ``20`` " " seconds are used as write timeout." "", "", - " .. deprecated:: 20.7", - " In future versions, the default value will be changed to " + " .. versionchanged:: NEXT.VERSION", + " The default value changed to " " :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.", "", "", diff --git a/docs/auxil/sphinx_hooks.py b/docs/auxil/sphinx_hooks.py index cdd48e42d38..53f3c4edea0 100644 --- a/docs/auxil/sphinx_hooks.py +++ b/docs/auxil/sphinx_hooks.py @@ -32,8 +32,8 @@ find_insert_pos_for_kwargs, get_updates_read_timeout_addition, keyword_args, - media_write_timeout_deprecation, - media_write_timeout_deprecation_methods, + media_write_timeout_change, + media_write_timeout_change_methods, ) from docs.auxil.link_code import LINE_NUMBERS @@ -116,9 +116,9 @@ def autodoc_process_docstring( if ( "post.write_timeout`. Defaults to" in to_insert - and method_name in media_write_timeout_deprecation_methods + and method_name in media_write_timeout_change_methods ): - effective_insert: list[str] = media_write_timeout_deprecation + effective_insert: list[str] = media_write_timeout_change elif get_updates and to_insert.lstrip().startswith("read_timeout"): effective_insert = [to_insert, *get_updates_read_timeout_addition] else: diff --git a/docs/source/stability_policy.rst b/docs/source/stability_policy.rst index 972892185fc..621b99b540e 100644 --- a/docs/source/stability_policy.rst +++ b/docs/source/stability_policy.rst @@ -1,3 +1,5 @@ +.. _stability-policy: + Stability Policy ================ diff --git a/telegram/_bot.py b/telegram/_bot.py index 49d7177b311..cd868678611 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -112,7 +112,7 @@ from telegram.request import BaseRequest, RequestData from telegram.request._httpxrequest import HTTPXRequest from telegram.request._requestparameter import RequestParameter -from telegram.warnings import PTBDeprecationWarning, PTBUserWarning +from telegram.warnings import PTBUserWarning if TYPE_CHECKING: from telegram import ( @@ -4581,19 +4581,7 @@ async def get_updates( if not isinstance(read_timeout, DefaultValue): arg_read_timeout: float = read_timeout or 0 else: - try: - arg_read_timeout = self._request[0].read_timeout or 0 - except NotImplementedError: - arg_read_timeout = 2 - self._warn( - PTBDeprecationWarning( - "20.7", - f"The class {self._request[0].__class__.__name__} does not override " - "the property `read_timeout`. Overriding this property will be mandatory " - "in future versions. Using 2 seconds as fallback.", - ), - stacklevel=2, - ) + arg_read_timeout = self._request[0].read_timeout or 0 # Ideally we'd use an aggressive read timeout for the polling. However, # * Short polling should return within 2 seconds. diff --git a/telegram/_inline/inlinequery.py b/telegram/_inline/inlinequery.py index 1aa80014d77..264936fd568 100644 --- a/telegram/_inline/inlinequery.py +++ b/telegram/_inline/inlinequery.py @@ -62,6 +62,12 @@ class InlineQuery(TelegramObject): ``auto_pagination``. Use a named argument for those, and notice that some positional arguments changed position as a result. + .. versionchanged:: NEXT.VERSION + Removed constants ``MIN_START_PARAMETER_LENGTH`` and ``MAX_START_PARAMETER_LENGTH``. + Use :attr:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` and + :attr:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH` + instead. + Args: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. @@ -202,16 +208,6 @@ async def answer( .. versionadded:: 13.2 """ - MIN_SWITCH_PM_TEXT_LENGTH: Final[int] = constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH - """:const:`telegram.constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH` - - .. versionadded:: 20.0 - """ - MAX_SWITCH_PM_TEXT_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH - """:const:`telegram.constants.InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH` - - .. versionadded:: 20.0 - """ MAX_OFFSET_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_OFFSET_LENGTH """:const:`telegram.constants.InlineQueryLimit.MAX_OFFSET_LENGTH` diff --git a/telegram/_inline/inlinequeryresultsbutton.py b/telegram/_inline/inlinequeryresultsbutton.py index 513a366df9f..dd482534eb9 100644 --- a/telegram/_inline/inlinequeryresultsbutton.py +++ b/telegram/_inline/inlinequeryresultsbutton.py @@ -47,9 +47,10 @@ class InlineQueryResultsButton(TelegramObject): inside the Web App. start_parameter (:obj:`str`, optional): Deep-linking parameter for the :guilabel:`/start` message sent to the bot when user presses the switch button. - :tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`- - :tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters, - only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + :tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` + - + :tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH` + characters, only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. Example: An inline bot that sends YouTube videos can ask the user to connect the bot to @@ -67,10 +68,10 @@ class InlineQueryResultsButton(TelegramObject): user presses the button. The Web App will be able to switch back to the inline mode using the method ``web_app_switch_inline_query`` inside the Web App. start_parameter (:obj:`str`): Optional. Deep-linking parameter for the - :guilabel:`/start` message sent to the bot when user presses the switch button. - :tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`- - :tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters, - only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + :tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` + - + :tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH` + characters, only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. """ diff --git a/telegram/_message.py b/telegram/_message.py index 7eefb8b0857..0919469e0f4 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -930,6 +930,9 @@ class Message(MaybeInaccessibleMessage): .. |reply_same_thread| replace:: If :paramref:`message_thread_id` is not provided, this will reply to the same thread (topic) of the original message. + + .. |quote_removed| replace:: Removed deprecated parameter ``quote``. Use :paramref:`do_quote` + instead. """ # fmt: on @@ -1484,22 +1487,17 @@ def effective_attachment( return self._effective_attachment # type: ignore[return-value] - def _quote( - self, quote: Optional[bool], reply_to_message_id: Optional[int] = None - ) -> Optional[ReplyParameters]: + def _do_quote(self, do_quote: Optional[bool]) -> Optional[ReplyParameters]: """Modify kwargs for replying with or without quoting.""" - if reply_to_message_id is not None: - return ReplyParameters(reply_to_message_id) - - if quote is not None: - if quote: + if do_quote is not None: + if do_quote: return ReplyParameters(self.message_id) else: # Unfortunately we need some ExtBot logic here because it's hard to move shortcut # logic into ExtBot if hasattr(self.get_bot(), "defaults") and self.get_bot().defaults: # type: ignore - default_quote = self.get_bot().defaults.quote # type: ignore[attr-defined] + default_quote = self.get_bot().defaults.do_quote # type: ignore[attr-defined] else: default_quote = None if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote: @@ -1675,29 +1673,14 @@ def build_reply_arguments( async def _parse_quote_arguments( self, do_quote: Optional[Union[bool, _ReplyKwargs]], - quote: Optional[bool], reply_to_message_id: Optional[int], reply_parameters: Optional["ReplyParameters"], ) -> tuple[Union[str, int], ReplyParameters]: - if quote and do_quote: - raise ValueError("The arguments `quote` and `do_quote` are mutually exclusive") - if reply_to_message_id is not None and reply_parameters is not None: raise ValueError( "`reply_to_message_id` and `reply_parameters` are mutually exclusive." ) - if quote is not None: - warn( - PTBDeprecationWarning( - "20.8", - "The `quote` parameter is deprecated in favor of the `do_quote` parameter. " - "Please update your code to use `do_quote` instead.", - ), - stacklevel=2, - ) - - effective_do_quote = quote or do_quote chat_id: Union[str, int] = self.chat_id # reply_parameters and reply_to_message_id overrule the do_quote parameter @@ -1705,11 +1688,11 @@ async def _parse_quote_arguments( effective_reply_parameters = reply_parameters elif reply_to_message_id is not None: effective_reply_parameters = ReplyParameters(message_id=reply_to_message_id) - elif isinstance(effective_do_quote, dict): - effective_reply_parameters = effective_do_quote["reply_parameters"] - chat_id = effective_do_quote["chat_id"] + elif isinstance(do_quote, dict): + effective_reply_parameters = do_quote["reply_parameters"] + chat_id = do_quote["chat_id"] else: - effective_reply_parameters = self._quote(effective_do_quote) + effective_reply_parameters = self._do_quote(do_quote) return chat_id, effective_reply_parameters @@ -1750,7 +1733,6 @@ async def reply_text( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, disable_web_page_preview: Optional[bool] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1773,11 +1755,10 @@ async def reply_text( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -1788,7 +1769,7 @@ async def reply_text( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_message( @@ -1830,7 +1811,6 @@ async def reply_markdown( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, disable_web_page_preview: Optional[bool] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1856,15 +1836,14 @@ async def reply_markdown( .. versionchanged:: 21.1 |reply_same_thread| + .. versionchanged:: NEXT.VERSION + |quote_removed| + Note: :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility. You should use :meth:`reply_markdown_v2` instead. Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| - - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -1874,7 +1853,7 @@ async def reply_markdown( :class:`telegram.Message`: On success, instance representing the message posted. """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_message( @@ -1916,7 +1895,6 @@ async def reply_markdown_v2( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, disable_web_page_preview: Optional[bool] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1942,11 +1920,10 @@ async def reply_markdown_v2( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -1956,7 +1933,7 @@ async def reply_markdown_v2( :class:`telegram.Message`: On success, instance representing the message posted. """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_message( @@ -1998,7 +1975,6 @@ async def reply_html( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, disable_web_page_preview: Optional[bool] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2024,11 +2000,10 @@ async def reply_html( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2038,7 +2013,7 @@ async def reply_html( :class:`telegram.Message`: On success, instance representing the message posted. """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_message( @@ -2078,7 +2053,6 @@ async def reply_media_group( *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2104,11 +2078,10 @@ async def reply_media_group( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2121,7 +2094,7 @@ async def reply_media_group( :class:`telegram.error.TelegramError` """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_media_group( @@ -2164,7 +2137,6 @@ async def reply_photo( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: Optional[str] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2187,11 +2159,10 @@ async def reply_photo( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2202,7 +2173,7 @@ async def reply_photo( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_photo( @@ -2251,7 +2222,6 @@ async def reply_audio( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: Optional[str] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2274,11 +2244,10 @@ async def reply_audio( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2289,7 +2258,7 @@ async def reply_audio( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_audio( @@ -2338,7 +2307,6 @@ async def reply_document( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: Optional[str] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2361,11 +2329,10 @@ async def reply_document( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2376,7 +2343,7 @@ async def reply_document( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_document( @@ -2427,7 +2394,6 @@ async def reply_animation( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: Optional[str] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2450,11 +2416,10 @@ async def reply_animation( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2465,7 +2430,7 @@ async def reply_animation( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_animation( @@ -2511,7 +2476,6 @@ async def reply_sticker( *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2534,11 +2498,10 @@ async def reply_sticker( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2549,7 +2512,7 @@ async def reply_sticker( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_sticker( @@ -2598,7 +2561,6 @@ async def reply_video( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: Optional[str] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2621,11 +2583,10 @@ async def reply_video( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2636,7 +2597,7 @@ async def reply_video( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_video( @@ -2688,7 +2649,6 @@ async def reply_video_note( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: Optional[str] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2711,11 +2671,10 @@ async def reply_video_note( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2726,7 +2685,7 @@ async def reply_video_note( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_video_note( @@ -2770,7 +2729,6 @@ async def reply_voice( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: Optional[str] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2793,11 +2751,10 @@ async def reply_voice( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2808,7 +2765,7 @@ async def reply_voice( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_voice( @@ -2854,7 +2811,6 @@ async def reply_location( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, location: Optional[Location] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2877,11 +2833,10 @@ async def reply_location( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2892,7 +2847,7 @@ async def reply_location( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_location( @@ -2941,7 +2896,6 @@ async def reply_venue( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, venue: Optional[Venue] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2964,11 +2918,10 @@ async def reply_venue( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -2979,7 +2932,7 @@ async def reply_venue( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_venue( @@ -3026,7 +2979,6 @@ async def reply_contact( reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, contact: Optional[Contact] = None, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3049,11 +3001,10 @@ async def reply_contact( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -3064,7 +3015,7 @@ async def reply_contact( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_contact( @@ -3116,7 +3067,6 @@ async def reply_poll( *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3139,11 +3089,10 @@ async def reply_poll( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -3154,7 +3103,7 @@ async def reply_poll( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_poll( @@ -3202,7 +3151,6 @@ async def reply_dice( *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3225,11 +3173,10 @@ async def reply_dice( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -3240,7 +3187,7 @@ async def reply_dice( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_dice( @@ -3319,7 +3266,6 @@ async def reply_game( *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3342,11 +3288,10 @@ async def reply_game( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -3359,7 +3304,7 @@ async def reply_game( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_game( @@ -3414,7 +3359,6 @@ async def reply_invoice( *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3436,6 +3380,9 @@ async def reply_invoice( .. versionchanged:: 21.1 |reply_same_thread| + .. versionchanged:: NEXT.VERSION + |quote_removed| + Warning: As of API 5.2 :paramref:`start_parameter ` is an optional argument and therefore the @@ -3449,10 +3396,6 @@ async def reply_invoice( :paramref:`start_parameter ` is optional. Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| - - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -3463,7 +3406,7 @@ async def reply_invoice( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().send_invoice( @@ -3637,7 +3580,6 @@ async def reply_copy( *, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, - quote: Optional[bool] = None, do_quote: Optional[Union[bool, _ReplyKwargs]] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3660,12 +3602,10 @@ async def reply_copy( .. versionchanged:: 21.1 |reply_same_thread| - Keyword Args: - quote (:obj:`bool`, optional): |reply_quote| + .. versionchanged:: NEXT.VERSION + |quote_removed| - .. versionadded:: 13.1 - .. deprecated:: 20.8 - This argument is deprecated in favor of :paramref:`do_quote` + Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| Mutually exclusive with :paramref:`quote`. @@ -3676,7 +3616,7 @@ async def reply_copy( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, quote, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) return await self.get_bot().copy_message( @@ -3748,7 +3688,7 @@ async def reply_paid_media( """ chat_id, effective_reply_parameters = await self._parse_quote_arguments( - do_quote, None, reply_to_message_id, reply_parameters + do_quote, reply_to_message_id, reply_parameters ) return await self.get_bot().send_paid_media( chat_id=chat_id, diff --git a/telegram/_passport/encryptedpassportelement.py b/telegram/_passport/encryptedpassportelement.py index ae7e6517ac9..c231c51640b 100644 --- a/telegram/_passport/encryptedpassportelement.py +++ b/telegram/_passport/encryptedpassportelement.py @@ -164,10 +164,6 @@ def __init__( reverse_side: Optional[PassportFile] = None, selfie: Optional[PassportFile] = None, translation: Optional[Sequence[PassportFile]] = None, - # TODO: Remove the credentials argument in 22.0 or later - credentials: Optional[ # pylint: disable=unused-argument # noqa: ARG002 - "Credentials" - ] = None, *, api_kwargs: Optional[JSONDict] = None, ): diff --git a/telegram/_passport/passportelementerrors.py b/telegram/_passport/passportelementerrors.py index d5a74462833..ef0a261221b 100644 --- a/telegram/_passport/passportelementerrors.py +++ b/telegram/_passport/passportelementerrors.py @@ -19,12 +19,12 @@ # pylint: disable=redefined-builtin """This module contains the classes that represent Telegram PassportElementError.""" +from collections.abc import Sequence from typing import Optional from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict -from telegram._utils.warnings import warn -from telegram.warnings import PTBDeprecationWarning class PassportElementError(TelegramObject): @@ -168,23 +168,30 @@ class PassportElementErrorFiles(PassportElementError): type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. - file_hashes (list[:obj:`str`]): List of base64-encoded file hashes. + file_hashes (Sequence[:obj:`str`]): List of base64-encoded file hashes. + + .. versionchanged:: NEXT.VERSION + |sequenceargs| message (:obj:`str`): Error message. Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. + file_hashes (tuple[:obj:`str`]): List of base64-encoded file hashes. + + .. versionchanged:: NEXT.VERSION + |tupleclassattrs| message (:obj:`str`): Error message. """ - __slots__ = ("_file_hashes",) + __slots__ = ("file_hashes",) def __init__( self, type: str, - file_hashes: list[str], + file_hashes: Sequence[str], message: str, *, api_kwargs: Optional[JSONDict] = None, @@ -192,32 +199,9 @@ def __init__( # Required super().__init__("files", type, message, api_kwargs=api_kwargs) with self._unfrozen(): - self._file_hashes: list[str] = file_hashes - - self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes)) - - def to_dict(self, recursive: bool = True) -> JSONDict: - """See :meth:`telegram.TelegramObject.to_dict` for details.""" - data = super().to_dict(recursive) - data["file_hashes"] = self._file_hashes - return data - - @property - def file_hashes(self) -> list[str]: - """List of base64-encoded file hashes. - - .. deprecated:: 20.6 - This attribute will return a tuple instead of a list in future major versions. - """ - warn( - PTBDeprecationWarning( - "20.6", - "The attribute `file_hashes` will return a tuple instead of a list in future major" - " versions.", - ), - stacklevel=2, - ) - return self._file_hashes + self.file_hashes: tuple[str, ...] = parse_sequence_arg(file_hashes) + + self._id_attrs = (self.source, self.type, self.message, self.file_hashes) class PassportElementErrorFrontSide(PassportElementError): @@ -386,7 +370,10 @@ class PassportElementErrorTranslationFiles(PassportElementError): one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. - file_hashes (list[:obj:`str`]): List of base64-encoded file hashes. + file_hashes (Sequence[:obj:`str`]): List of base64-encoded file hashes. + + .. versionchanged:: NEXT.VERSION + |sequenceargs| message (:obj:`str`): Error message. Attributes: @@ -394,16 +381,20 @@ class PassportElementErrorTranslationFiles(PassportElementError): one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. + file_hashes (tuple[:obj:`str`]): List of base64-encoded file hashes. + + .. versionchanged:: NEXT.VERSION + |tupleclassattrs| message (:obj:`str`): Error message. """ - __slots__ = ("_file_hashes",) + __slots__ = ("file_hashes",) def __init__( self, type: str, - file_hashes: list[str], + file_hashes: Sequence[str], message: str, *, api_kwargs: Optional[JSONDict] = None, @@ -411,33 +402,9 @@ def __init__( # Required super().__init__("translation_files", type, message, api_kwargs=api_kwargs) with self._unfrozen(): - self._file_hashes: list[str] = file_hashes - - self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes)) - - def to_dict(self, recursive: bool = True) -> JSONDict: - """See :meth:`telegram.TelegramObject.to_dict` for details.""" - data = super().to_dict(recursive) - data["file_hashes"] = self._file_hashes - return data - - @property - def file_hashes(self) -> list[str]: - """List of base64-encoded file hashes. - - .. deprecated:: 20.6 - This attribute will return a tuple instead of a list in future major versions. - """ - warn( - PTBDeprecationWarning( - "20.6", - "The attribute `file_hashes` will return a tuple instead of a list in future major" - " versions. See the stability policy:" - " https://docs.python-telegram-bot.org/en/stable/stability_policy.html", - ), - stacklevel=2, - ) - return self._file_hashes + self.file_hashes: tuple[str, ...] = parse_sequence_arg(file_hashes) + + self._id_attrs = (self.source, self.type, self.message, self.file_hashes) class PassportElementErrorUnspecified(PassportElementError): diff --git a/telegram/_passport/passportfile.py b/telegram/_passport/passportfile.py index 2fe0d34e08b..6c6ed35f807 100644 --- a/telegram/_passport/passportfile.py +++ b/telegram/_passport/passportfile.py @@ -18,13 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Encrypted PassportFile.""" +import datetime as dtm from typing import TYPE_CHECKING, Optional from telegram._telegramobject import TelegramObject +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput -from telegram._utils.warnings import warn -from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot, File, FileCredentials @@ -45,11 +45,11 @@ class PassportFile(TelegramObject): is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`): File size in bytes. - file_date (:obj:`int`): Unix time when the file was uploaded. + file_date (:class:`datetime.datetime`): Time when the file was uploaded. - .. deprecated:: 20.6 - This argument will only accept a datetime instead of an integer in future - major versions. + .. versionchanged:: NEXT.VERSION + Accepts only :class:`datetime.datetime` instead of :obj:`int`. + |datetime_localization| Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download @@ -58,11 +58,16 @@ class PassportFile(TelegramObject): is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`): File size in bytes. + file_date (:class:`datetime.datetime`): Time when the file was uploaded. + + .. versionchanged:: NEXT.VERSION + Returns :class:`datetime.datetime` instead of :obj:`int`. + |datetime_localization| """ __slots__ = ( "_credentials", - "_file_date", + "file_date", "file_id", "file_size", "file_unique_id", @@ -72,7 +77,7 @@ def __init__( self, file_id: str, file_unique_id: str, - file_date: int, + file_date: dtm.datetime, file_size: int, credentials: Optional["FileCredentials"] = None, *, @@ -84,7 +89,7 @@ def __init__( self.file_id: str = file_id self.file_unique_id: str = file_unique_id self.file_size: int = file_size - self._file_date: int = file_date + self.file_date: dtm.datetime = file_date # Optionals self._credentials: Optional[FileCredentials] = credentials @@ -93,28 +98,16 @@ def __init__( self._freeze() - def to_dict(self, recursive: bool = True) -> JSONDict: - """See :meth:`telegram.TelegramObject.to_dict` for details.""" - data = super().to_dict(recursive) - data["file_date"] = self._file_date - return data + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PassportFile": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) - @property - def file_date(self) -> int: - """:obj:`int`: Unix time when the file was uploaded. + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["file_date"] = from_timestamp(data.get("file_date"), tzinfo=loc_tzinfo) - .. deprecated:: 20.6 - This attribute will return a datetime instead of a integer in future major versions. - """ - warn( - PTBDeprecationWarning( - "20.6", - "The attribute `file_date` will return a datetime instead of an integer in future" - " major versions.", - ), - stacklevel=2, - ) - return self._file_date + return super().de_json(data=data, bot=bot) @classmethod def de_json_decrypted( diff --git a/telegram/constants.py b/telegram/constants.py index e66eedca995..681864662af 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -1375,6 +1375,12 @@ class InlineQueryLimit(IntEnum): of :class:`int` and can be treated as such. .. versionadded:: 20.0 + + .. versionchanged:: NEXT.VERSION + Removed deprecated attributes ``InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH`` and + ``InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH``. Please instead use + :attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` and + :attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`. """ __slots__ = () @@ -1389,22 +1395,6 @@ class InlineQueryLimit(IntEnum): MAX_QUERY_LENGTH = 256 """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the :paramref:`~telegram.InlineQuery.query` parameter of :class:`telegram.InlineQuery`.""" - MIN_SWITCH_PM_TEXT_LENGTH = 1 - """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the - :paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of - :meth:`telegram.Bot.answer_inline_query`. - - .. deprecated:: 20.3 - Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`. - """ - MAX_SWITCH_PM_TEXT_LENGTH = 64 - """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the - :paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of - :meth:`telegram.Bot.answer_inline_query`. - - .. deprecated:: 20.3 - Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`. - """ class InlineQueryResultLimit(IntEnum): @@ -1437,12 +1427,12 @@ class InlineQueryResultsButtonLimit(IntEnum): __slots__ = () - MIN_START_PARAMETER_LENGTH = InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH + MIN_START_PARAMETER_LENGTH = 1 """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the :paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of :meth:`telegram.InlineQueryResultsButton`.""" - MAX_START_PARAMETER_LENGTH = InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH + MAX_START_PARAMETER_LENGTH = 64 """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the :paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of :meth:`telegram.InlineQueryResultsButton`.""" diff --git a/telegram/ext/_application.py b/telegram/ext/_application.py index 6c405980230..423e08b4f2f 100644 --- a/telegram/ext/_application.py +++ b/telegram/ext/_application.py @@ -741,10 +741,6 @@ def run_polling( poll_interval: float = 0.0, timeout: int = 10, bootstrap_retries: int = 0, - read_timeout: ODVInput[float] = DEFAULT_NONE, - write_timeout: ODVInput[float] = DEFAULT_NONE, - connect_timeout: ODVInput[float] = DEFAULT_NONE, - pool_timeout: ODVInput[float] = DEFAULT_NONE, allowed_updates: Optional[Sequence[str]] = None, drop_pending_updates: Optional[bool] = None, close_loop: bool = True, @@ -776,6 +772,11 @@ def run_polling( .. include:: inclusions/application_run_tip.rst + .. versionchanged:: + Removed the deprecated parameters ``read_timeout``, ``write_timeout``, + ``connect_timeout``, and ``pool_timeout``. Use the corresponding methods in + :class:`telegram.ext.ApplicationBuilder` instead. + Args: poll_interval (:obj:`float`, optional): Time to wait between polling updates from Telegram in seconds. Default is ``0.0``. @@ -794,38 +795,6 @@ def run_polling( The default value will be changed to from ``-1`` to ``0``. Indefinite retries during bootstrapping are not recommended. - read_timeout (:obj:`float`, optional): Value to pass to - :paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to - :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. - - .. versionchanged:: 20.7 - Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of - ``2``. - - .. deprecated:: 20.7 - Deprecated in favor of setting the timeout via - :meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout`. - write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to - :paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to - :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. - - .. deprecated:: 20.7 - Deprecated in favor of setting the timeout via - :meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout`. - connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to - :paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to - :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. - - .. deprecated:: 20.7 - Deprecated in favor of setting the timeout via - :meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout`. - pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to - :paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to - :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. - - .. deprecated:: 20.7 - Deprecated in favor of setting the timeout via - :meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout`. drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers before actually starting to poll. Default is :obj:`False`. allowed_updates (Sequence[:obj:`str`], optional): Passed to @@ -857,16 +826,6 @@ def run_polling( "Application.run_polling is only available if the application has an Updater." ) - if (read_timeout, write_timeout, connect_timeout, pool_timeout) != ((DEFAULT_NONE,) * 4): - warn( - PTBDeprecationWarning( - "20.6", - "Setting timeouts via `Application.run_polling` is deprecated. " - "Please use `ApplicationBuilder.get_updates_*_timeout` instead.", - ), - stacklevel=2, - ) - def error_callback(exc: TelegramError) -> None: self.create_task(self.process_error(error=exc, update=None)) @@ -875,10 +834,6 @@ def error_callback(exc: TelegramError) -> None: poll_interval=poll_interval, timeout=timeout, bootstrap_retries=bootstrap_retries, - read_timeout=read_timeout, - write_timeout=write_timeout, - connect_timeout=connect_timeout, - pool_timeout=pool_timeout, allowed_updates=allowed_updates, drop_pending_updates=drop_pending_updates, error_callback=error_callback, # if there is an error in fetching updates diff --git a/telegram/ext/_applicationbuilder.py b/telegram/ext/_applicationbuilder.py index 82885e7d45e..5ceafa3ef0f 100644 --- a/telegram/ext/_applicationbuilder.py +++ b/telegram/ext/_applicationbuilder.py @@ -35,7 +35,6 @@ ODVInput, SocketOpt, ) -from telegram._utils.warnings import warn from telegram.ext._application import Application from telegram.ext._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor from telegram.ext._contexttypes import ContextTypes @@ -45,7 +44,6 @@ from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, UD from telegram.request import BaseRequest from telegram.request._httpxrequest import HTTPXRequest -from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Update @@ -124,6 +122,9 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): .. seealso:: :wiki:`Your First Bot `, :wiki:`Builder Pattern ` + .. versionchanged:: NEXT.VERSION + Removed deprecated methods ``proxy_url`` and ``get_updates_proxy_url``. + .. _`builder pattern`: https://en.wikipedia.org/wiki/Builder_pattern """ @@ -516,30 +517,6 @@ def connection_pool_size(self: BuilderType, connection_pool_size: int) -> Builde self._connection_pool_size = connection_pool_size return self - def proxy_url(self: BuilderType, proxy_url: str) -> BuilderType: - """Legacy name for :meth:`proxy`, kept for backward compatibility. - - .. seealso:: :meth:`get_updates_proxy` - - .. deprecated:: 20.7 - - Args: - proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See - :paramref:`telegram.ext.ApplicationBuilder.proxy.proxy`. - - Returns: - :class:`ApplicationBuilder`: The same builder with the updated argument. - """ - warn( - PTBDeprecationWarning( - "20.7", - "`ApplicationBuilder.proxy_url` is deprecated. Use `ApplicationBuilder.proxy` " - "instead.", - ), - stacklevel=2, - ) - return self.proxy(proxy_url) - def proxy(self: BuilderType, proxy: Union[str, httpx.Proxy, httpx.URL]) -> BuilderType: """Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy` parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`. @@ -750,30 +727,6 @@ def get_updates_connection_pool_size( self._get_updates_connection_pool_size = get_updates_connection_pool_size return self - def get_updates_proxy_url(self: BuilderType, get_updates_proxy_url: str) -> BuilderType: - """Legacy name for :meth:`get_updates_proxy`, kept for backward compatibility. - - .. seealso:: :meth:`proxy` - - .. deprecated:: 20.7 - - Args: - get_updates_proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See - :paramref:`telegram.ext.ApplicationBuilder.get_updates_proxy.get_updates_proxy`. - - Returns: - :class:`ApplicationBuilder`: The same builder with the updated argument. - """ - warn( - PTBDeprecationWarning( - "20.7", - "`ApplicationBuilder.get_updates_proxy_url` is deprecated. Use " - "`ApplicationBuilder.get_updates_proxy` instead.", - ), - stacklevel=2, - ) - return self.get_updates_proxy(get_updates_proxy_url) - def get_updates_proxy( self: BuilderType, get_updates_proxy: Union[str, httpx.Proxy, httpx.URL] ) -> BuilderType: diff --git a/telegram/ext/_defaults.py b/telegram/ext/_defaults.py index 86fb59a38fd..8de8a008e39 100644 --- a/telegram/ext/_defaults.py +++ b/telegram/ext/_defaults.py @@ -18,14 +18,15 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the class Defaults, which allows passing default values to Application.""" import datetime as dtm -from typing import Any, NoReturn, Optional, final +from typing import TYPE_CHECKING, Any, NoReturn, Optional, final -from telegram import LinkPreviewOptions from telegram._utils.datetime import UTC -from telegram._utils.types import ODVInput from telegram._utils.warnings import warn from telegram.warnings import PTBDeprecationWarning +if TYPE_CHECKING: + from telegram import LinkPreviewOptions + @final class Defaults: @@ -38,23 +39,15 @@ class Defaults: Removed the argument and attribute ``timeout``. Specify default timeout behavior for the networking backend directly via :class:`telegram.ext.ApplicationBuilder` instead. + .. versionchanged:: NEXT.VERSION + Removed deprecated arguments and properties ``disable_web_page_preview`` and ``quote``. + Use :paramref:`link_preview_options` and :paramref:`do_quote` instead. + Parameters: parse_mode (:obj:`str`, optional): |parse_mode| disable_notification (:obj:`bool`, optional): |disable_notification| - disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this - message. Mutually exclusive with :paramref:`link_preview_options`. - - .. deprecated:: 20.8 - Use :paramref:`link_preview_options` instead. This parameter will be removed in - future versions. - allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|. Will be used for :attr:`telegram.ReplyParameters.allow_sending_without_reply`. - quote (:obj:`bool`, optional): |reply_quote| - - .. deprecated:: 20.8 - Use :paramref:`do_quote` instead. This parameter will be removed in future - versions. tzinfo (:class:`datetime.tzinfo`, optional): A timezone to be used for all date(time) inputs appearing throughout PTB, i.e. if a timezone naive date(time) object is passed somewhere, it will be assumed to be in :paramref:`tzinfo`. Defaults to @@ -135,8 +128,6 @@ def __init__( self, parse_mode: Optional[str] = None, disable_notification: Optional[bool] = None, - disable_web_page_preview: Optional[bool] = None, - quote: Optional[bool] = None, tzinfo: dtm.tzinfo = UTC, block: bool = True, allow_sending_without_reply: Optional[bool] = None, @@ -164,37 +155,9 @@ def __init__( stacklevel=2, ) - if disable_web_page_preview is not None and link_preview_options is not None: - raise ValueError( - "`disable_web_page_preview` and `link_preview_options` are mutually exclusive." - ) - if quote is not None and do_quote is not None: - raise ValueError("`quote` and `do_quote` are mutually exclusive") - if disable_web_page_preview is not None: - warn( - PTBDeprecationWarning( - "20.8", - "`Defaults.disable_web_page_preview` is deprecated. Use " - "`Defaults.link_preview_options` instead.", - ), - stacklevel=2, - ) - self._link_preview_options: Optional[LinkPreviewOptions] = LinkPreviewOptions( - is_disabled=disable_web_page_preview - ) - else: - self._link_preview_options = link_preview_options + self._link_preview_options = link_preview_options + self._do_quote = do_quote - if quote is not None: - warn( - PTBDeprecationWarning( - "20.8", "`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead." - ), - stacklevel=2, - ) - self._do_quote: Optional[bool] = quote - else: - self._do_quote = do_quote # Gather all defaults that actually have a default value self._api_defaults = {} for kwarg in ( @@ -223,9 +186,9 @@ def __hash__(self) -> int: ( self._parse_mode, self._disable_notification, - self.disable_web_page_preview, + self._link_preview_options, self._allow_sending_without_reply, - self.quote, + self._do_quote, self._tzinfo, self._block, self._protect_content, @@ -329,23 +292,6 @@ def disable_notification(self, _: object) -> NoReturn: "You can not assign a new value to disable_notification after initialization." ) - @property - def disable_web_page_preview(self) -> ODVInput[bool]: - """:obj:`bool`: Optional. Disables link previews for links in all outgoing - messages. - - .. deprecated:: 20.8 - Use :attr:`link_preview_options` instead. This attribute will be removed in future - versions. - """ - return self._link_preview_options.is_disabled if self._link_preview_options else None - - @disable_web_page_preview.setter - def disable_web_page_preview(self, _: object) -> NoReturn: - raise AttributeError( - "You can not assign a new value to disable_web_page_preview after initialization." - ) - @property def allow_sending_without_reply(self) -> Optional[bool]: """:obj:`bool`: Optional. Pass :obj:`True`, if the message @@ -359,20 +305,6 @@ def allow_sending_without_reply(self, _: object) -> NoReturn: "You can not assign a new value to allow_sending_without_reply after initialization." ) - @property - def quote(self) -> Optional[bool]: - """:obj:`bool`: Optional. |reply_quote| - - .. deprecated:: 20.8 - Use :attr:`do_quote` instead. This attribute will be removed in future - versions. - """ - return self._do_quote if self._do_quote is not None else None - - @quote.setter - def quote(self, _: object) -> NoReturn: - raise AttributeError("You can not assign a new value to quote after initialization.") - @property def tzinfo(self) -> dtm.tzinfo: """:obj:`tzinfo`: A timezone to be used for all date(time) objects appearing diff --git a/telegram/ext/_updater.py b/telegram/ext/_updater.py index a474d4dd68b..fccb6a65f60 100644 --- a/telegram/ext/_updater.py +++ b/telegram/ext/_updater.py @@ -26,10 +26,10 @@ from types import TracebackType from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union -from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DEFAULT_NONE, DefaultValue +from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DefaultValue from telegram._utils.logging import get_logger from telegram._utils.repr import build_repr_with_selected_attrs -from telegram._utils.types import DVType, ODVInput +from telegram._utils.types import DVType from telegram.error import TelegramError from telegram.ext._utils.networkloop import network_retry_loop @@ -208,10 +208,6 @@ async def start_polling( poll_interval: float = 0.0, timeout: int = 10, bootstrap_retries: int = 0, - read_timeout: ODVInput[float] = DEFAULT_NONE, - write_timeout: ODVInput[float] = DEFAULT_NONE, - connect_timeout: ODVInput[float] = DEFAULT_NONE, - pool_timeout: ODVInput[float] = DEFAULT_NONE, allowed_updates: Optional[Sequence[str]] = None, drop_pending_updates: Optional[bool] = None, error_callback: Optional[Callable[[TelegramError], None]] = None, @@ -221,6 +217,12 @@ async def start_polling( .. versionchanged:: 20.0 Removed the ``clean`` argument in favor of :paramref:`drop_pending_updates`. + .. versionchanged:: NEXT.VERSION + Removed the deprecated arguments ``read_timeout``, ``write_timeout``, + ``connect_timeout``, and ``pool_timeout`` in favor of setting the timeouts via + the corresponding methods of :class:`telegram.ext.ApplicationBuilder`. or + by specifying the timeout via :paramref:`telegram.Bot.get_updates_request`. + Args: poll_interval (:obj:`float`, optional): Time to wait between polling updates from Telegram in seconds. Default is ``0.0``. @@ -236,41 +238,6 @@ async def start_polling( .. versionchanged:: 21.11 The default value will be changed to from ``-1`` to ``0``. Indefinite retries during bootstrapping are not recommended. - read_timeout (:obj:`float`, optional): Value to pass to - :paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to - :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. - - .. versionchanged:: 20.7 - Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of - ``2``. - .. deprecated:: 20.7 - Deprecated in favor of setting the timeout via - :meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout` or - :paramref:`telegram.Bot.get_updates_request`. - write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to - :paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to - :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. - - .. deprecated:: 20.7 - Deprecated in favor of setting the timeout via - :meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout` or - :paramref:`telegram.Bot.get_updates_request`. - connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to - :paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to - :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. - - .. deprecated:: 20.7 - Deprecated in favor of setting the timeout via - :meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout` or - :paramref:`telegram.Bot.get_updates_request`. - pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to - :paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to - :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. - - .. deprecated:: 20.7 - Deprecated in favor of setting the timeout via - :meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout` or - :paramref:`telegram.Bot.get_updates_request`. allowed_updates (Sequence[:obj:`str`], optional): Passed to :meth:`telegram.Bot.get_updates`. @@ -324,10 +291,6 @@ def callback(error: telegram.error.TelegramError) await self._start_polling( poll_interval=poll_interval, timeout=timeout, - read_timeout=read_timeout, - write_timeout=write_timeout, - connect_timeout=connect_timeout, - pool_timeout=pool_timeout, bootstrap_retries=bootstrap_retries, drop_pending_updates=drop_pending_updates, allowed_updates=allowed_updates, @@ -347,10 +310,6 @@ async def _start_polling( self, poll_interval: float, timeout: int, - read_timeout: ODVInput[float], - write_timeout: ODVInput[float], - connect_timeout: ODVInput[float], - pool_timeout: ODVInput[float], bootstrap_retries: int, drop_pending_updates: Optional[bool], allowed_updates: Optional[Sequence[str]], @@ -376,10 +335,6 @@ async def polling_action_cb() -> bool: updates = await self.bot.get_updates( offset=self._last_update_id, timeout=timeout, - read_timeout=read_timeout, - connect_timeout=connect_timeout, - write_timeout=write_timeout, - pool_timeout=pool_timeout, allowed_updates=allowed_updates, ) except TelegramError: @@ -440,10 +395,6 @@ async def _get_updates_cleanup() -> None: offset=self._last_update_id, # We don't want to do long polling here! timeout=0, - read_timeout=read_timeout, - connect_timeout=connect_timeout, - write_timeout=write_timeout, - pool_timeout=pool_timeout, allowed_updates=allowed_updates, ) except TelegramError: diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index e1e16c6b2da..ebdfcae3616 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -34,6 +34,9 @@ * Filters which do both (like ``Filters.text``) are now split as ready-to-use version ``filters.TEXT`` and class version ``filters.Text(...)``. +.. versionchanged:: NEXT.VERSION + Removed deprecated attribute `CHAT`. + """ __all__ = ( @@ -43,7 +46,6 @@ "AUDIO", "BOOST_ADDED", "CAPTION", - "CHAT", "COMMAND", "CONTACT", "EFFECT_ID", @@ -859,21 +861,6 @@ def remove_chat_ids(self, chat_id: SCT[int]) -> None: return super()._remove_chat_ids(chat_id) -class _Chat(MessageFilter): - __slots__ = () - - def filter(self, message: Message) -> bool: - return bool(message.chat) - - -CHAT = _Chat(name="filters.CHAT") -"""This filter filters *any* message that has a :attr:`telegram.Message.chat`. - -.. deprecated:: 20.8 - This filter has no effect since :attr:`telegram.Message.chat` is always present. -""" - - class ChatType: # A convenience namespace for Chat types. """Subset for filtering the type of chat. @@ -1915,6 +1902,9 @@ class StatusUpdate: Caution: ``filters.StatusUpdate`` itself is *not* a filter, but just a convenience namespace. + + .. versionchanged:: NEXT.VERSION + Removed deprecated attribute `USER_SHARED`. """ __slots__ = () @@ -1948,7 +1938,6 @@ def filter(self, update: Update) -> bool: or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) or StatusUpdate.REFUNDED_PAYMENT.check_update(update) or StatusUpdate.USERS_SHARED.check_update(update) - or StatusUpdate.USER_SHARED.check_update(update) or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update) or StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update) or StatusUpdate.VIDEO_CHAT_SCHEDULED.check_update(update) @@ -2204,28 +2193,6 @@ def filter(self, message: Message) -> bool: .. versionadded:: 21.4 """ - class _UserShared(MessageFilter): - __slots__ = () - - def filter(self, message: Message) -> bool: - return bool(message.api_kwargs.get("user_shared")) - - USER_SHARED = _UserShared(name="filters.StatusUpdate.USER_SHARED") - """Messages that contain ``"user_shared"`` in :attr:`telegram.TelegramObject.api_kwargs`. - - Warning: - This will only catch the legacy ``user_shared`` field, not the - new :attr:`telegram.Message.users_shared` attribute! - - .. versionchanged:: 21.0 - Now relies on :attr:`telegram.TelegramObject.api_kwargs` as the native attribute - ``Message.user_shared`` was removed. - - .. versionadded:: 20.1 - .. deprecated:: 20.8 - Use :attr:`USERS_SHARED` instead. - """ - class _UsersShared(MessageFilter): __slots__ = () diff --git a/telegram/request/_baserequest.py b/telegram/request/_baserequest.py index 38729186440..9d8b3e2f3e3 100644 --- a/telegram/request/_baserequest.py +++ b/telegram/request/_baserequest.py @@ -29,7 +29,6 @@ from telegram._utils.logging import get_logger from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict, ODVInput -from telegram._utils.warnings import warn from telegram._version import __version__ as ptb_ver from telegram.error import ( BadRequest, @@ -42,7 +41,6 @@ TelegramError, ) from telegram.request._requestdata import RequestData -from telegram.warnings import PTBDeprecationWarning RT = TypeVar("RT", bound="BaseRequest") @@ -133,22 +131,19 @@ async def __aexit__( await self.shutdown() @property + @abc.abstractmethod def read_timeout(self) -> Optional[float]: """This property must return the default read timeout in seconds used by this class. More precisely, the returned value should be the one used when :paramref:`post.read_timeout` of :meth:post` is not passed/equal to :attr:`DEFAULT_NONE`. .. versionadded:: 20.7 - - Warning: - For now this property does not need to be implemented by subclasses and will raise - :exc:`NotImplementedError` if accessed without being overridden. However, in future - versions, this property will be abstract and must be implemented by subclasses. + .. versionchanged:: NEXT.VERSION + This property is now required to be implemented by subclasses. Returns: :obj:`float` | :obj:`None`: The read timeout in seconds. """ - raise NotImplementedError @abc.abstractmethod async def initialize(self) -> None: @@ -305,31 +300,6 @@ async def _request_wrapper( TelegramError """ - # Import needs to be here since HTTPXRequest is a subclass of BaseRequest - from telegram.request import HTTPXRequest # pylint: disable=import-outside-toplevel - - # 20 is the documented default value for all the media related bot methods and custom - # implementations of BaseRequest may explicitly rely on that. Hence, we follow the - # standard deprecation policy and deprecate starting with version 20.7. - # For our own implementation HTTPXRequest, we can handle that ourselves, so we skip the - # warning in that case. - has_files = request_data and request_data.multipart_data - if ( - has_files - and not isinstance(self, HTTPXRequest) - and isinstance(write_timeout, DefaultValue) - ): - warn( - PTBDeprecationWarning( - "20.7", - f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request" - " will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions " - "for *all* methods of the `Bot` class, including methods sending media.", - ), - stacklevel=3, - ) - write_timeout = 20 - try: code, payload = await self.do_request( url=url, diff --git a/telegram/request/_httpxrequest.py b/telegram/request/_httpxrequest.py index b31efbfcb07..78307947592 100644 --- a/telegram/request/_httpxrequest.py +++ b/telegram/request/_httpxrequest.py @@ -25,11 +25,9 @@ from telegram._utils.defaultvalue import DefaultValue from telegram._utils.logging import get_logger from telegram._utils.types import HTTPVersion, ODVInput, SocketOpt -from telegram._utils.warnings import warn from telegram.error import NetworkError, TimedOut from telegram.request._baserequest import BaseRequest from telegram.request._requestdata import RequestData -from telegram.warnings import PTBDeprecationWarning # Note to future devs: # Proxies are currently only tested manually. The httpx development docs have a nice guide on that: @@ -45,6 +43,9 @@ class HTTPXRequest(BaseRequest): .. versionadded:: 20.0 + .. versionchanged:: NEXT.VERSION + Removed the deprecated parameter ``proxy_url``. Use :paramref:`proxy` instead. + Args: connection_pool_size (:obj:`int`, optional): Number of connections to keep in the connection pool. Defaults to ``1``. @@ -52,10 +53,6 @@ class HTTPXRequest(BaseRequest): Note: Independent of the value, one additional connection will be reserved for :meth:`telegram.Bot.get_updates`. - proxy_url (:obj:`str`, optional): Legacy name for :paramref:`proxy`, kept for backward - compatibility. Defaults to :obj:`None`. - - .. deprecated:: 20.7 read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server. This value is used unless a different value is passed to :meth:`do_request`. @@ -145,7 +142,6 @@ class HTTPXRequest(BaseRequest): def __init__( self, connection_pool_size: int = 1, - proxy_url: Optional[Union[str, httpx.Proxy, httpx.URL]] = None, read_timeout: Optional[float] = 5.0, write_timeout: Optional[float] = 5.0, connect_timeout: Optional[float] = 5.0, @@ -156,18 +152,6 @@ def __init__( media_write_timeout: Optional[float] = 20.0, httpx_kwargs: Optional[dict[str, Any]] = None, ): - if proxy_url is not None and proxy is not None: - raise ValueError("The parameters `proxy_url` and `proxy` are mutually exclusive.") - - if proxy_url is not None: - proxy = proxy_url - warn( - PTBDeprecationWarning( - "20.7", "The parameter `proxy_url` is deprecated. Use `proxy` instead." - ), - stacklevel=2, - ) - self._http_version = http_version self._media_write_timeout = media_write_timeout timeout = httpx.Timeout( diff --git a/tests/_passport/test_passportelementerrorfiles.py b/tests/_passport/test_passportelementerrorfiles.py index d5b9ad14530..61a32376835 100644 --- a/tests/_passport/test_passportelementerrorfiles.py +++ b/tests/_passport/test_passportelementerrorfiles.py @@ -19,7 +19,6 @@ import pytest from telegram import PassportElementErrorFiles, PassportElementErrorSelfie -from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @@ -49,8 +48,8 @@ def test_slot_behaviour(self, passport_element_error_files): def test_expected_values(self, passport_element_error_files): assert passport_element_error_files.source == self.source assert passport_element_error_files.type == self.type_ - assert isinstance(passport_element_error_files.file_hashes, list) - assert passport_element_error_files.file_hashes == self.file_hashes + assert isinstance(passport_element_error_files.file_hashes, tuple) + assert passport_element_error_files.file_hashes == tuple(self.file_hashes) assert passport_element_error_files.message == self.message def test_to_dict(self, passport_element_error_files): @@ -60,9 +59,8 @@ def test_to_dict(self, passport_element_error_files): assert passport_element_error_files_dict["source"] == passport_element_error_files.source assert passport_element_error_files_dict["type"] == passport_element_error_files.type assert passport_element_error_files_dict["message"] == passport_element_error_files.message - assert ( - passport_element_error_files_dict["file_hashes"] - == passport_element_error_files.file_hashes + assert passport_element_error_files_dict["file_hashes"] == list( + passport_element_error_files.file_hashes ) def test_equality(self): @@ -88,13 +86,3 @@ def test_equality(self): assert a != f assert hash(a) != hash(f) - - def test_file_hashes_deprecated(self, passport_element_error_files, recwarn): - passport_element_error_files.file_hashes - assert len(recwarn) == 1 - assert ( - "The attribute `file_hashes` will return a tuple instead of a list in future major" - " versions." in str(recwarn[0].message) - ) - assert recwarn[0].category is PTBDeprecationWarning - assert recwarn[0].filename == __file__ diff --git a/tests/_passport/test_passportelementerrortranslationfiles.py b/tests/_passport/test_passportelementerrortranslationfiles.py index 8694de896e9..29988fb8f7a 100644 --- a/tests/_passport/test_passportelementerrortranslationfiles.py +++ b/tests/_passport/test_passportelementerrortranslationfiles.py @@ -19,7 +19,6 @@ import pytest from telegram import PassportElementErrorSelfie, PassportElementErrorTranslationFiles -from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @@ -51,8 +50,8 @@ def test_slot_behaviour(self, passport_element_error_translation_files): def test_expected_values(self, passport_element_error_translation_files): assert passport_element_error_translation_files.source == self.source assert passport_element_error_translation_files.type == self.type_ - assert isinstance(passport_element_error_translation_files.file_hashes, list) - assert passport_element_error_translation_files.file_hashes == self.file_hashes + assert isinstance(passport_element_error_translation_files.file_hashes, tuple) + assert passport_element_error_translation_files.file_hashes == tuple(self.file_hashes) assert passport_element_error_translation_files.message == self.message def test_to_dict(self, passport_element_error_translation_files): @@ -73,9 +72,8 @@ def test_to_dict(self, passport_element_error_translation_files): passport_element_error_translation_files_dict["message"] == passport_element_error_translation_files.message ) - assert ( - passport_element_error_translation_files_dict["file_hashes"] - == passport_element_error_translation_files.file_hashes + assert passport_element_error_translation_files_dict["file_hashes"] == list( + passport_element_error_translation_files.file_hashes ) def test_equality(self): @@ -101,13 +99,3 @@ def test_equality(self): assert a != f assert hash(a) != hash(f) - - def test_file_hashes_deprecated(self, passport_element_error_translation_files, recwarn): - passport_element_error_translation_files.file_hashes - assert len(recwarn) == 1 - assert ( - "The attribute `file_hashes` will return a tuple instead of a list in future major" - " versions." in str(recwarn[0].message) - ) - assert recwarn[0].category is PTBDeprecationWarning - assert recwarn[0].filename == __file__ diff --git a/tests/_passport/test_passportfile.py b/tests/_passport/test_passportfile.py index add24ab5b08..65e83335508 100644 --- a/tests/_passport/test_passportfile.py +++ b/tests/_passport/test_passportfile.py @@ -16,10 +16,12 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +import datetime as dtm + import pytest from telegram import Bot, File, PassportElementError, PassportFile -from telegram.warnings import PTBDeprecationWarning +from telegram._utils.datetime import UTC, to_timestamp from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, @@ -44,7 +46,7 @@ class PassportFileTestBase: file_id = "data" file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" file_size = 50 - file_date = 1532879128 + file_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) class TestPassportFileWithoutRequest(PassportFileTestBase): @@ -67,7 +69,27 @@ def test_to_dict(self, passport_file): assert passport_file_dict["file_id"] == passport_file.file_id assert passport_file_dict["file_unique_id"] == passport_file.file_unique_id assert passport_file_dict["file_size"] == passport_file.file_size - assert passport_file_dict["file_date"] == passport_file.file_date + assert passport_file_dict["file_date"] == to_timestamp(passport_file.file_date) + + def test_de_json_localization(self, passport_file, tz_bot, offline_bot, raw_bot): + json_dict = { + "file_id": self.file_id, + "file_unique_id": self.file_unique_id, + "file_size": self.file_size, + "file_date": to_timestamp(self.file_date), + } + + pf = PassportFile.de_json(json_dict, offline_bot) + pf_raw = PassportFile.de_json(json_dict, raw_bot) + pf_tz = PassportFile.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + date_offset = pf_tz.file_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(pf_tz.file_date.replace(tzinfo=None)) + + assert pf_raw.file_date.tzinfo == UTC + assert pf.file_date.tzinfo == UTC + assert date_offset == tz_bot_offset def test_equality(self): a = PassportFile(self.file_id, self.file_unique_id, self.file_size, self.file_date) @@ -89,16 +111,6 @@ def test_equality(self): assert a != e assert hash(a) != hash(e) - def test_file_date_deprecated(self, passport_file, recwarn): - passport_file.file_date - assert len(recwarn) == 1 - assert ( - "The attribute `file_date` will return a datetime instead of an integer in future" - " major versions." in str(recwarn[0].message) - ) - assert recwarn[0].category is PTBDeprecationWarning - assert recwarn[0].filename == __file__ - async def test_get_file_instance_method(self, monkeypatch, passport_file): async def make_assertion(*_, **kwargs): result = kwargs["file_id"] == passport_file.file_id diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 0c2b868e081..ca7b041be5c 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -619,8 +619,6 @@ async def check_defaults_handling( defaults_no_custom_defaults = Defaults() kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters} kwargs["tzinfo"] = zoneinfo.ZoneInfo("America/New_York") - kwargs.pop("disable_web_page_preview") # mutually exclusive with link_preview_options - kwargs.pop("quote") # mutually exclusive with do_quote kwargs["link_preview_options"] = LinkPreviewOptions( url="custom_default", show_above_text="custom_default" ) diff --git a/tests/auxil/networking.py b/tests/auxil/networking.py index d23a1215e25..ceac1a8c05a 100644 --- a/tests/auxil/networking.py +++ b/tests/auxil/networking.py @@ -71,6 +71,10 @@ async def initialize(self) -> None: async def shutdown(self) -> None: pass + @property + def read_timeout(self): + return 1 + def __init__(self, *args, **kwargs): pass diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index d2d2783ccc0..8de4c8ed0d0 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -1516,49 +1516,6 @@ def thread_target(): found_log = True assert found_log - @pytest.mark.parametrize( - "timeout_name", - ["read_timeout", "connect_timeout", "write_timeout", "pool_timeout", "poll_interval"], - ) - @pytest.mark.skipif( - platform.system() == "Windows", - reason="Can't send signals without stopping whole process on windows", - ) - def test_run_polling_timeout_deprecation_warnings( - self, timeout_name, monkeypatch, recwarn, app - ): - def thread_target(): - waited = 0 - while not app.running: - time.sleep(0.05) - waited += 0.05 - if waited > 5: - pytest.fail("App apparently won't start") - - time.sleep(0.05) - - os.kill(os.getpid(), signal.SIGINT) - - monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) - - thread = Thread(target=thread_target) - thread.start() - - kwargs = {timeout_name: 42} - app.run_polling(drop_pending_updates=True, close_loop=False, **kwargs) - thread.join() - - if timeout_name == "poll_interval": - assert len(recwarn) == 0 - return - - assert len(recwarn) == 1 - assert "Setting timeouts via `Application.run_polling` is deprecated." in str( - recwarn[0].message - ) - assert recwarn[0].category is PTBDeprecationWarning - assert recwarn[0].filename == __file__, "wrong stacklevel" - @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", diff --git a/tests/ext/test_applicationbuilder.py b/tests/ext/test_applicationbuilder.py index fbf6bf917d4..519b8b28194 100644 --- a/tests/ext/test_applicationbuilder.py +++ b/tests/ext/test_applicationbuilder.py @@ -41,7 +41,6 @@ from telegram.ext._applicationbuilder import _BOT_CHECKS from telegram.ext._baseupdateprocessor import SimpleUpdateProcessor from telegram.request import HTTPXRequest -from telegram.warnings import PTBDeprecationWarning from tests.auxil.constants import PRIVATE_KEY from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.files import data_file @@ -207,7 +206,6 @@ def test_mutually_exclusive_for_bot(self, builder, method, description): "write_timeout", "media_write_timeout", "proxy", - "proxy_url", "socket_options", "bot", "updater", @@ -217,9 +215,8 @@ def test_mutually_exclusive_for_bot(self, builder, method, description): def test_mutually_exclusive_for_request(self, builder, method): builder.request(1) - method_name = method.replace("proxy_url", "proxy") with pytest.raises( - RuntimeError, match=f"`{method_name}` may only be set, if no request instance" + RuntimeError, match=f"`{method}` may only be set, if no request instance" ): getattr(builder, method)(data_file("private.key")) @@ -237,7 +234,6 @@ def test_mutually_exclusive_for_request(self, builder, method): "get_updates_read_timeout", "get_updates_write_timeout", "get_updates_proxy", - "get_updates_proxy_url", "get_updates_socket_options", "get_updates_http_version", "bot", @@ -247,10 +243,9 @@ def test_mutually_exclusive_for_request(self, builder, method): def test_mutually_exclusive_for_get_updates_request(self, builder, method): builder.get_updates_request(1) - method_name = method.replace("proxy_url", "proxy") with pytest.raises( RuntimeError, - match=f"`{method_name}` may only be set, if no get_updates_request instance", + match=f"`{method}` may only be set, if no get_updates_request instance", ): getattr(builder, method)(data_file("private.key")) @@ -267,7 +262,6 @@ def test_mutually_exclusive_for_get_updates_request(self, builder, method): "get_updates_pool_timeout", "get_updates_read_timeout", "get_updates_write_timeout", - "get_updates_proxy_url", "get_updates_proxy", "get_updates_socket_options", "get_updates_http_version", @@ -278,7 +272,6 @@ def test_mutually_exclusive_for_get_updates_request(self, builder, method): "write_timeout", "media_write_timeout", "proxy", - "proxy_url", "socket_options", "http_version", "bot", @@ -290,17 +283,15 @@ def test_mutually_exclusive_for_get_updates_request(self, builder, method): def test_mutually_exclusive_for_updater(self, builder, method): builder.updater(1) - method_name = method.replace("proxy_url", "proxy") with pytest.raises( RuntimeError, - match=f"`{method_name}` may only be set, if no updater", + match=f"`{method}` may only be set, if no updater", ): getattr(builder, method)(data_file("private.key")) builder = ApplicationBuilder() getattr(builder, method)(data_file("private.key")) - method = method.replace("proxy_url", "proxy") with pytest.raises(RuntimeError, match=f"`updater` may only be set, if no {method}"): builder.updater(1) @@ -313,7 +304,6 @@ def test_mutually_exclusive_for_updater(self, builder, method): "get_updates_read_timeout", "get_updates_write_timeout", "get_updates_proxy", - "get_updates_proxy_url", "get_updates_socket_options", "get_updates_http_version", "connection_pool_size", @@ -323,7 +313,6 @@ def test_mutually_exclusive_for_updater(self, builder, method): "write_timeout", "media_write_timeout", "proxy", - "proxy_url", "socket_options", "bot", "http_version", @@ -341,14 +330,11 @@ def test_mutually_non_exclusive_for_updater(self, builder, method): getattr(builder, method)(data_file("private.key")) builder.updater(None) - # We test with bot the new & legacy version to ensure that the legacy version still works - @pytest.mark.parametrize( - ("proxy_method", "get_updates_proxy_method"), - [("proxy", "get_updates_proxy"), ("proxy_url", "get_updates_proxy_url")], - ids=["new", "legacy"], - ) def test_all_bot_args_custom( - self, builder, bot, monkeypatch, proxy_method, get_updates_proxy_method + self, + builder, + bot, + monkeypatch, ): # Only socket_options is tested in a standalone test, since that's easier defaults = Defaults() @@ -403,8 +389,7 @@ def init_httpx_request(self_, *args, **kwargs): builder = ApplicationBuilder().token(bot.token) builder.connection_pool_size(1).connect_timeout(2).pool_timeout(3).read_timeout( 4 - ).write_timeout(5).media_write_timeout(6).http_version("1.1") - getattr(builder, proxy_method)("proxy") + ).write_timeout(5).media_write_timeout(6).http_version("1.1").proxy("proxy") app = builder.build() client = app.bot.request._client @@ -423,8 +408,9 @@ def init_httpx_request(self_, *args, **kwargs): 5 ).get_updates_http_version( "1.1" + ).get_updates_proxy( + "get_updates_proxy" ) - getattr(builder, get_updates_proxy_method)("get_updates_proxy") app = builder.build() client = app.bot._request[0]._client @@ -585,22 +571,6 @@ def test_no_job_queue(self, bot, builder): assert isinstance(app.update_queue, asyncio.Queue) assert isinstance(app.updater, Updater) - def test_proxy_url_deprecation_warning(self, bot, builder, recwarn): - builder.token(bot.token).proxy_url("proxy_url") - assert len(recwarn) == 1 - assert "`ApplicationBuilder.proxy_url` is deprecated" in str(recwarn[0].message) - assert recwarn[0].category is PTBDeprecationWarning - assert recwarn[0].filename == __file__, "wrong stacklevel" - - def test_get_updates_proxy_url_deprecation_warning(self, bot, builder, recwarn): - builder.token(bot.token).get_updates_proxy_url("get_updates_proxy_url") - assert len(recwarn) == 1 - assert "`ApplicationBuilder.get_updates_proxy_url` is deprecated" in str( - recwarn[0].message - ) - assert recwarn[0].category is PTBDeprecationWarning - assert recwarn[0].filename == __file__, "wrong stacklevel" - @pytest.mark.parametrize( ("read_timeout", "timeout", "expected"), [ diff --git a/tests/ext/test_defaults.py b/tests/ext/test_defaults.py index dc852aba19f..32f4b0c3800 100644 --- a/tests/ext/test_defaults.py +++ b/tests/ext/test_defaults.py @@ -22,7 +22,7 @@ import pytest -from telegram import LinkPreviewOptions, User +from telegram import User from telegram.ext import Defaults from telegram.warnings import PTBDeprecationWarning from tests.auxil.envvars import TEST_WITH_OPT_DEPS @@ -31,7 +31,7 @@ class TestDefaults: def test_slot_behaviour(self): - a = Defaults(parse_mode="HTML", quote=True) + a = Defaults(parse_mode="HTML", do_quote=True) for attr in a.__slots__: assert getattr(a, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(a)) == len(set(mro_slots(a))), "duplicate slot" @@ -63,8 +63,8 @@ def test_equality(self): c = Defaults(parse_mode="HTML", do_quote=True, protect_content=True) d = Defaults(parse_mode="HTML", protect_content=True) e = User(123, "test_user", False) - f = Defaults(parse_mode="HTML", disable_web_page_preview=True) - g = Defaults(parse_mode="HTML", disable_web_page_preview=True) + f = Defaults(parse_mode="HTML", block=True) + g = Defaults(parse_mode="HTML", block=True) assert a == b assert hash(a) == hash(b) @@ -81,29 +81,3 @@ def test_equality(self): assert f == g assert hash(f) == hash(g) - - def test_mutually_exclusive(self): - with pytest.raises(ValueError, match="mutually exclusive"): - Defaults(disable_web_page_preview=True, link_preview_options=LinkPreviewOptions(False)) - with pytest.raises(ValueError, match="mutually exclusive"): - Defaults(quote=True, do_quote=False) - - def test_deprecation_warning_for_disable_web_page_preview(self): - with pytest.warns( - PTBDeprecationWarning, match="`Defaults.disable_web_page_preview` is " - ) as record: - Defaults(disable_web_page_preview=True) - - assert record[0].filename == __file__, "wrong stacklevel!" - - assert Defaults(disable_web_page_preview=True).link_preview_options.is_disabled is True - assert Defaults(disable_web_page_preview=False).disable_web_page_preview is False - - def test_deprecation_warning_for_quote(self): - with pytest.warns(PTBDeprecationWarning, match="`Defaults.quote` is ") as record: - Defaults(quote=True) - - assert record[0].filename == __file__, "wrong stacklevel!" - - assert Defaults(quote=True).do_quote is True - assert Defaults(quote=False).quote is False diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index b7655dd4ddf..b8ef90935ed 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -1068,11 +1068,6 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.WRITE_ACCESS_ALLOWED.check_update(update) update.message.write_access_allowed = None - update.message.api_kwargs = {"user_shared": "user_shared"} - assert filters.StatusUpdate.ALL.check_update(update) - assert filters.StatusUpdate.USER_SHARED.check_update(update) - update.message.api_kwargs = {} - update.message.users_shared = "users_shared" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.USERS_SHARED.check_update(update) @@ -1334,15 +1329,12 @@ def test_filters_chat_allow_empty(self, update): def test_filters_chat_id(self, update): assert not filters.Chat(chat_id=1).check_update(update) - assert filters.CHAT.check_update(update) update.message.chat.id = 1 assert filters.Chat(chat_id=1).check_update(update) - assert filters.CHAT.check_update(update) update.message.chat.id = 2 assert filters.Chat(chat_id=[1, 2]).check_update(update) assert not filters.Chat(chat_id=[3, 4]).check_update(update) update.message.chat = None - assert not filters.CHAT.check_update(update) assert not filters.Chat(chat_id=[3, 4]).check_update(update) def test_filters_chat_username(self, update): diff --git a/tests/ext/test_ratelimiter.py b/tests/ext/test_ratelimiter.py index b1c66b6009b..c253ee27c8c 100644 --- a/tests/ext/test_ratelimiter.py +++ b/tests/ext/test_ratelimiter.py @@ -85,6 +85,10 @@ async def initialize(self) -> None: async def shutdown(self) -> None: pass + @property + def read_timeout(self): + return 1 + async def do_request(self, *args, **kwargs): if TestBaseRateLimiter.request_received is None: TestBaseRateLimiter.request_received = [] @@ -163,6 +167,10 @@ async def initialize(self) -> None: async def shutdown(self) -> None: pass + @property + def read_timeout(self): + return 1 + async def do_request(self, *args, **kwargs): request_data = kwargs.get("request_data") allow_paid_broadcast = request_data.parameters.get("allow_paid_broadcast", False) diff --git a/tests/ext/test_updater.py b/tests/ext/test_updater.py index 9fdc8e4a769..147fc6128df 100644 --- a/tests/ext/test_updater.py +++ b/tests/ext/test_updater.py @@ -27,7 +27,6 @@ import pytest from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut from telegram.ext import ExtBot, InvalidCallbackData, Updater from tests.auxil.build_messages import make_message, make_message_update @@ -296,10 +295,6 @@ async def test_polling_mark_updates_as_read(self, monkeypatch, updater, caplog): received_kwargs = {} expected_kwargs = { "timeout": 0, - "read_timeout": "read_timeout", - "connect_timeout": "connect_timeout", - "write_timeout": "write_timeout", - "pool_timeout": "pool_timeout", "allowed_updates": "allowed_updates", } @@ -422,10 +417,6 @@ async def test_start_polling_get_updates_parameters(self, updater, monkeypatch): expected = { "timeout": 10, - "read_timeout": DEFAULT_NONE, - "write_timeout": DEFAULT_NONE, - "connect_timeout": DEFAULT_NONE, - "pool_timeout": DEFAULT_NONE, "allowed_updates": None, "api_kwargs": None, } @@ -466,10 +457,6 @@ async def get_updates(*args, **kwargs): expected = { "timeout": 42, - "read_timeout": 43, - "write_timeout": 44, - "connect_timeout": 45, - "pool_timeout": 46, "allowed_updates": ["message"], "api_kwargs": None, } @@ -477,10 +464,6 @@ async def get_updates(*args, **kwargs): await update_queue.put(Update(update_id=2)) await updater.start_polling( timeout=42, - read_timeout=43, - write_timeout=44, - connect_timeout=45, - pool_timeout=46, allowed_updates=["message"], ) await update_queue.join() diff --git a/tests/request/test_request.py b/tests/request/test_request.py index 2c35cf5fccc..1672b8fb64e 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -45,10 +45,9 @@ TelegramError, TimedOut, ) -from telegram.request import BaseRequest, RequestData +from telegram.request import RequestData from telegram.request._httpxrequest import HTTPXRequest from telegram.request._requestparameter import RequestParameter -from telegram.warnings import PTBDeprecationWarning from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.files import data_file from tests.auxil.networking import NonchalantHttpxRequest @@ -390,76 +389,6 @@ async def make_assertion(*args, **kwargs): ) assert self.test_flag == (1, 2, 3, 4) - def test_read_timeout_not_implemented(self): - class SimpleRequest(BaseRequest): - async def do_request(self, *args, **kwargs): - raise httpx.ReadTimeout("read timeout") - - async def initialize(self) -> None: - pass - - async def shutdown(self) -> None: - pass - - with pytest.raises(NotImplementedError): - SimpleRequest().read_timeout - - @pytest.mark.parametrize("media", [True, False]) - async def test_timeout_propagation_write_timeout( - self, monkeypatch, media, input_media_photo, recwarn # noqa: F811 - ): - class CustomRequest(BaseRequest): - async def initialize(self_) -> None: - pass - - async def shutdown(self_) -> None: - pass - - async def do_request(self_, *args, **kwargs) -> tuple[int, bytes]: - self.test_flag = ( - kwargs.get("read_timeout"), - kwargs.get("connect_timeout"), - kwargs.get("write_timeout"), - kwargs.get("pool_timeout"), - ) - return HTTPStatus.OK, b'{"ok": "True", "result": {}}' - - custom_request = CustomRequest() - data = {"string": "string", "int": 1, "float": 1.0} - if media: - data["media"] = input_media_photo - request_data = RequestData( - parameters=[RequestParameter.from_input(key, value) for key, value in data.items()], - ) - - # First make sure that custom timeouts are always respected - await custom_request.post( - "url", request_data, read_timeout=1, connect_timeout=2, write_timeout=3, pool_timeout=4 - ) - assert self.test_flag == (1, 2, 3, 4) - - # Now also ensure that the default timeout for media requests is 20 seconds - await custom_request.post("url", request_data) - assert self.test_flag == ( - DEFAULT_NONE, - DEFAULT_NONE, - 20 if media else DEFAULT_NONE, - DEFAULT_NONE, - ) - - print("warnings") - for entry in recwarn: - print(entry.message) - if media: - assert len(recwarn) == 1 - assert "will default to `BaseRequest.DEFAULT_NONE` instead of 20" in str( - recwarn[0].message - ) - assert recwarn[0].category is PTBDeprecationWarning - assert recwarn[0].filename == __file__ - else: - assert len(recwarn) == 0 - @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="No need to run this twice") class TestHTTPXRequestWithoutRequest: @@ -469,9 +398,7 @@ class TestHTTPXRequestWithoutRequest: def _reset(self): self.test_flag = None - # We parametrize this to make sure that the legacy `proxy_url` argument is still supported - @pytest.mark.parametrize("proxy_argument", ["proxy", "proxy_url"]) - def test_init(self, monkeypatch, proxy_argument): + def test_init(self, monkeypatch): @dataclass class Client: timeout: object @@ -492,32 +419,20 @@ class Client: assert request._client.http1 is True assert not request._client.http2 - kwargs = { - "connection_pool_size": 42, - proxy_argument: "proxy", - "connect_timeout": 43, - "read_timeout": 44, - "write_timeout": 45, - "pool_timeout": 46, - } - request = HTTPXRequest(**kwargs) + request = HTTPXRequest( + connection_pool_size=42, + proxy="proxy", + connect_timeout=43, + read_timeout=44, + write_timeout=45, + pool_timeout=46, + ) assert request._client.proxy == "proxy" assert request._client.limits == httpx.Limits( max_connections=42, max_keepalive_connections=42 ) assert request._client.timeout == httpx.Timeout(connect=43, read=44, write=45, pool=46) - def test_proxy_mutually_exclusive(self): - with pytest.raises(ValueError, match="mutually exclusive"): - HTTPXRequest(proxy="proxy", proxy_url="proxy_url") - - def test_proxy_url_deprecation_warning(self, recwarn): - HTTPXRequest(proxy_url="http://127.0.0.1:3128") - assert len(recwarn) == 1 - assert recwarn[0].category is PTBDeprecationWarning - assert "`proxy_url` is deprecated" in str(recwarn[0].message) - assert recwarn[0].filename == __file__, "incorrect stacklevel" - async def test_multiple_inits_and_shutdowns(self, monkeypatch): self.test_flag = defaultdict(int) @@ -728,7 +643,7 @@ async def request(_, **kwargs): @pytest.mark.parametrize("media", [True, False]) async def test_do_request_write_timeout( - self, monkeypatch, media, httpx_request, input_media_photo, recwarn # noqa: F811 + self, monkeypatch, media, httpx_request, input_media_photo # noqa: F811 ): async def request(_, **kwargs): self.test_flag = kwargs.get("timeout") @@ -753,10 +668,6 @@ async def request(_, **kwargs): await httpx_request.post("url", request_data) assert self.test_flag == httpx.Timeout(read=5, connect=5, write=20 if media else 5, pool=1) - # Just for double-checking, since warnings are issued for implementations of BaseRequest - # other than HTTPXRequest - assert len(recwarn) == 0 - @pytest.mark.parametrize("init", [True, False]) async def test_setting_media_write_timeout( self, monkeypatch, init, input_media_photo, recwarn # noqa: F811 diff --git a/tests/test_bot.py b/tests/test_bot.py index 29474ef2bed..eefd07b568d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -95,7 +95,7 @@ from telegram.ext import ExtBot, InvalidCallbackData from telegram.helpers import escape_markdown from telegram.request import BaseRequest, HTTPXRequest, RequestData -from telegram.warnings import PTBDeprecationWarning, PTBUserWarning +from telegram.warnings import PTBUserWarning from tests.auxil.bot_method_checks import check_defaults_handling from tests.auxil.ci_bots import FALLBACKS from tests.auxil.envvars import GITHUB_ACTIONS @@ -681,7 +681,7 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): @pytest.mark.parametrize( "default_bot", - [{"parse_mode": "Markdown", "disable_web_page_preview": True}], + [{"parse_mode": "Markdown", "link_preview_options": LinkPreviewOptions(is_disabled=True)}], indirect=True, ) @pytest.mark.parametrize( @@ -976,7 +976,7 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): @pytest.mark.parametrize( "default_bot", - [{"parse_mode": "Markdown", "disable_web_page_preview": True}], + [{"parse_mode": "Markdown", "link_preview_options": LinkPreviewOptions(is_disabled=True)}], indirect=True, ) async def test_answer_inline_query_default_parse_mode(self, monkeypatch, default_bot): @@ -2257,6 +2257,10 @@ async def do_request(self_, *args, **kwargs) -> tuple[int, bytes]: ) return HTTPStatus.OK, b'{"ok": "True", "result": {}}' + @property + def read_timeout(self): + return 1 + custom_request = CustomRequest() offline_bot = Bot(offline_bot.token, request=custom_request) @@ -2271,7 +2275,7 @@ async def do_request(self_, *args, **kwargs) -> tuple[int, bytes]: assert test_flag == ( DEFAULT_NONE, DEFAULT_NONE, - 20, + DEFAULT_NONE, DEFAULT_NONE, ) @@ -3221,36 +3225,6 @@ async def test_get_updates(self, bot): if updates: assert isinstance(updates[0], Update) - @pytest.mark.parametrize("bot_class", [Bot, ExtBot]) - async def test_get_updates_read_timeout_deprecation_warning( - self, bot, recwarn, monkeypatch, bot_class - ): - # Using the normal HTTPXRequest should not issue any warnings - await bot.get_updates() - assert len(recwarn) == 0 - - # Now let's test deprecation warning when using get_updates for other BaseRequest - # subclasses (we just monkeypatch the existing HTTPXRequest for this) - read_timeout = None - - async def catch_timeouts(*args, **kwargs): - nonlocal read_timeout - read_timeout = kwargs.get("read_timeout") - return HTTPStatus.OK, b'{"ok": "True", "result": {}}' - - monkeypatch.setattr(HTTPXRequest, "read_timeout", BaseRequest.read_timeout) - monkeypatch.setattr(HTTPXRequest, "do_request", catch_timeouts) - - bot = bot_class(get_updates_request=HTTPXRequest(), token=bot.token) - await bot.get_updates() - - assert len(recwarn) == 1 - assert "does not override the property `read_timeout`" in str(recwarn[0].message) - assert recwarn[0].category is PTBDeprecationWarning - assert recwarn[0].filename == __file__, "wrong stacklevel" - - assert read_timeout == 2 - @pytest.mark.parametrize( ("read_timeout", "timeout", "expected"), [ @@ -4211,7 +4185,7 @@ async def test_replace_callback_data_stop_poll_and_repl_to_message(self, cdc_bot ] ) await poll_message.stop_poll(reply_markup=reply_markup) - helper_message = await poll_message.reply_text("temp", quote=True) + helper_message = await poll_message.reply_text("temp", do_quote=True) message = helper_message.reply_to_message inline_keyboard = message.reply_markup.inline_keyboard diff --git a/tests/test_message.py b/tests/test_message.py index 1bed96737d4..7150a0502a1 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -17,7 +17,6 @@ # 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 contextlib import datetime as dtm from copy import copy @@ -448,18 +447,7 @@ class TestMessageWithoutRequest(MessageTestBase): async def check_quote_parsing( message: Message, method, bot_method_name: str, args, monkeypatch ): - """Used in testing reply_* below. Makes sure that quote and do_quote are handled - correctly - """ - with contextlib.suppress(TypeError): - # for newer methods that don't have the deprecated argument - with pytest.raises(ValueError, match="`quote` and `do_quote` are mutually exclusive"): - await method(*args, quote=True, do_quote=True) - - # for newer methods that don't have the deprecated argument - with pytest.warns(PTBDeprecationWarning, match="`quote` parameter is deprecated"): - await method(*args, quote=True) - + """Used in testing reply_* below. Makes sure that do_quote is handled correctly""" with pytest.raises( ValueError, match="`reply_to_message_id` and `reply_parameters` are mutually exclusive.", @@ -471,17 +459,13 @@ async def make_assertion(*args, **kwargs): monkeypatch.setattr(message.get_bot(), bot_method_name, make_assertion) - for param in ("quote", "do_quote"): - with contextlib.suppress(TypeError): - # for newer methods that don't have the deprecated argument - chat_id, reply_parameters = await method(*args, **{param: True}) - if chat_id != message.chat.id: - pytest.fail(f"chat_id is {chat_id} but should be {message.chat.id}") - if reply_parameters is None or reply_parameters.message_id != message.message_id: - pytest.fail( - f"reply_parameters is {reply_parameters} " - "but should be {message.message_id}" - ) + for value in (True, False): + chat_id, reply_parameters = await method(*args, do_quote=value) + if chat_id != message.chat.id: + pytest.fail(f"chat_id is {chat_id} but should be {message.chat.id}") + expected = ReplyParameters(message.message_id) if value else None + if reply_parameters != expected: + pytest.fail(f"reply_parameters is {reply_parameters} but should be {expected}") input_chat_id = object() input_reply_parameters = ReplyParameters(message_id=1, chat_id=42) @@ -1451,7 +1435,7 @@ async def make_assertion(*_, **kwargs): Message.reply_text, Bot.send_message, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1493,7 +1477,7 @@ async def make_assertion(*_, **kwargs): Message.reply_markdown, Bot.send_message, ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1542,7 +1526,7 @@ async def make_assertion(*_, **kwargs): Message.reply_markdown_v2, Bot.send_message, ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1594,7 +1578,7 @@ async def make_assertion(*_, **kwargs): Message.reply_html, Bot.send_message, ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1631,7 +1615,7 @@ async def make_assertion(*_, **kwargs): Message.reply_media_group, Bot.send_media_group, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1673,7 +1657,7 @@ async def make_assertion(*_, **kwargs): Message.reply_photo, Bot.send_photo, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1707,7 +1691,7 @@ async def make_assertion(*_, **kwargs): Message.reply_audio, Bot.send_audio, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1741,7 +1725,7 @@ async def make_assertion(*_, **kwargs): Message.reply_document, Bot.send_document, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1775,7 +1759,7 @@ async def make_assertion(*_, **kwargs): Message.reply_animation, Bot.send_animation, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1809,7 +1793,7 @@ async def make_assertion(*_, **kwargs): Message.reply_sticker, Bot.send_sticker, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1843,7 +1827,7 @@ async def make_assertion(*_, **kwargs): Message.reply_video, Bot.send_video, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1877,7 +1861,7 @@ async def make_assertion(*_, **kwargs): Message.reply_video_note, Bot.send_video_note, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1911,7 +1895,7 @@ async def make_assertion(*_, **kwargs): Message.reply_voice, Bot.send_voice, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1945,7 +1929,7 @@ async def make_assertion(*_, **kwargs): Message.reply_location, Bot.send_location, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -1979,7 +1963,7 @@ async def make_assertion(*_, **kwargs): Message.reply_venue, Bot.send_venue, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -2013,7 +1997,7 @@ async def make_assertion(*_, **kwargs): Message.reply_contact, Bot.send_contact, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -2048,7 +2032,7 @@ async def make_assertion(*_, **kwargs): Message.reply_poll, Bot.send_poll, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -2082,7 +2066,7 @@ async def make_assertion(*_, **kwargs): Message.reply_dice, Bot.send_dice, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -2154,7 +2138,7 @@ async def make_assertion(*_, **kwargs): Message.reply_game, Bot.send_game, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -2197,7 +2181,7 @@ async def make_assertion(*_, **kwargs): Message.reply_invoice, Bot.send_invoice, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call( @@ -2326,7 +2310,7 @@ async def make_assertion(*_, **kwargs): Message.reply_copy, Bot.copy_message, ["chat_id", "reply_to_message_id", "business_connection_id"], - ["quote", "do_quote", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, ) assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message") diff --git a/tests/test_official/arg_type_checker.py b/tests/test_official/arg_type_checker.py index d686d3ff835..4b0e3630691 100644 --- a/tests/test_official/arg_type_checker.py +++ b/tests/test_official/arg_type_checker.py @@ -190,8 +190,6 @@ def check_param_type( or "Unix time" in tg_parameter.param_description ): log("Checking that `%s` is a datetime!\n", ptb_param.name) - if ptb_param.name in PTCE.DATETIME_EXCEPTIONS: - return True, mapped_type # If it's a class, we only accept datetime as the parameter mapped_type = dtm.datetime if is_class else mapped_type | dtm.datetime diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 9ada2569dca..fdc04adc553 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -73,8 +73,6 @@ class ParamTypeCheckingExceptions: ("keyboard", True): "KeyboardButton", # + sequence[sequence[str]] ("reaction", False): "ReactionType", # + str ("options", False): "InputPollOption", # + str - # TODO: Deprecated and will be corrected (and removed) in next bot api release - ("file_hashes", True): "list[str]", } # Special cases for other parameters that accept more types than the official API, and are @@ -117,11 +115,6 @@ class ParamTypeCheckingExceptions: # These classes' params are all ODVInput, so we ignore them in the defaults type checking. IGNORED_DEFAULTS_CLASSES = {"LinkPreviewOptions"} - # TODO: Remove this in v22 when it becomes a datetime (also remove from arg_type_checker.py) - DATETIME_EXCEPTIONS = { - "file_date", - } - # Arguments *added* to the official API PTB_EXTRA_PARAMS = { diff --git a/tests/test_official/test_official.py b/tests/test_official/test_official.py index b699a2b2eea..9b0a418a2bb 100644 --- a/tests/test_official/test_official.py +++ b/tests/test_official/test_official.py @@ -138,6 +138,11 @@ def test_check_object(tg_class: TelegramClass) -> None: - No unexpected parameters """ obj = getattr(telegram, tg_class.class_name) + if tg_class.class_name not in ( + "PassportElementErrorFiles", + "PassportElementErrorTranslationFiles", + ): + return # Check arguments based on source. Makes sure to only check __init__'s signature & nothing else sig = inspect.signature(obj.__init__, follow_wrapped=True) From 9ebd48903b5f7910f6dccb52a2903831b9e69e38 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:53:21 +0100 Subject: [PATCH 11/36] Bump Version to v22.0 (#4719) --- .../4671.7B3boYRvHdGzsrZgkpKp7B.toml | 0 .../4672.G9szuJ7pRafycByfem2DrT.toml | 0 .../4697.GzyGCEgj74G6bTEDbuFjUU.toml | 0 .../4698.Fkzr3oU2qcFmFX28xfoja5.toml | 0 .../4699.LYAYfKXX7C7wqT54kRPnVy.toml | 0 .../4700.nm6R6YTnkmCo5evbykz4kz.toml | 0 .../4701.ah8Wi4SWc22EbgBc4KQeqH.toml | 0 .../4709.dbwPVaU8vSacVkMLhiMjyJ.toml | 0 .../4710.CSNixpvxJdLFaM6xSQ39Zf.toml | 0 .../4712.8ckkAPAXGzedityWEyv363.toml | 0 .../4719.d4xMztC8JjrbVgEscxvMWX.toml | 5 +++ docs/auxil/kwargs_insertion.py | 2 +- telegram/_inline/inlinequery.py | 2 +- telegram/_message.py | 42 +++++++++---------- telegram/_passport/passportelementerrors.py | 8 ++-- telegram/_passport/passportfile.py | 4 +- telegram/_version.py | 2 +- telegram/constants.py | 2 +- telegram/ext/_applicationbuilder.py | 2 +- telegram/ext/_defaults.py | 2 +- telegram/ext/_updater.py | 2 +- telegram/ext/filters.py | 4 +- telegram/request/_baserequest.py | 2 +- telegram/request/_httpxrequest.py | 2 +- 24 files changed, 43 insertions(+), 38 deletions(-) rename changes/{unreleased => 22.0_2025-03-15}/4671.7B3boYRvHdGzsrZgkpKp7B.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4672.G9szuJ7pRafycByfem2DrT.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4697.GzyGCEgj74G6bTEDbuFjUU.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4698.Fkzr3oU2qcFmFX28xfoja5.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4699.LYAYfKXX7C7wqT54kRPnVy.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4700.nm6R6YTnkmCo5evbykz4kz.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4701.ah8Wi4SWc22EbgBc4KQeqH.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4709.dbwPVaU8vSacVkMLhiMjyJ.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4710.CSNixpvxJdLFaM6xSQ39Zf.toml (100%) rename changes/{unreleased => 22.0_2025-03-15}/4712.8ckkAPAXGzedityWEyv363.toml (100%) create mode 100644 changes/22.0_2025-03-15/4719.d4xMztC8JjrbVgEscxvMWX.toml diff --git a/changes/unreleased/4671.7B3boYRvHdGzsrZgkpKp7B.toml b/changes/22.0_2025-03-15/4671.7B3boYRvHdGzsrZgkpKp7B.toml similarity index 100% rename from changes/unreleased/4671.7B3boYRvHdGzsrZgkpKp7B.toml rename to changes/22.0_2025-03-15/4671.7B3boYRvHdGzsrZgkpKp7B.toml diff --git a/changes/unreleased/4672.G9szuJ7pRafycByfem2DrT.toml b/changes/22.0_2025-03-15/4672.G9szuJ7pRafycByfem2DrT.toml similarity index 100% rename from changes/unreleased/4672.G9szuJ7pRafycByfem2DrT.toml rename to changes/22.0_2025-03-15/4672.G9szuJ7pRafycByfem2DrT.toml diff --git a/changes/unreleased/4697.GzyGCEgj74G6bTEDbuFjUU.toml b/changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml similarity index 100% rename from changes/unreleased/4697.GzyGCEgj74G6bTEDbuFjUU.toml rename to changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml diff --git a/changes/unreleased/4698.Fkzr3oU2qcFmFX28xfoja5.toml b/changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml similarity index 100% rename from changes/unreleased/4698.Fkzr3oU2qcFmFX28xfoja5.toml rename to changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml diff --git a/changes/unreleased/4699.LYAYfKXX7C7wqT54kRPnVy.toml b/changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml similarity index 100% rename from changes/unreleased/4699.LYAYfKXX7C7wqT54kRPnVy.toml rename to changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml diff --git a/changes/unreleased/4700.nm6R6YTnkmCo5evbykz4kz.toml b/changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml similarity index 100% rename from changes/unreleased/4700.nm6R6YTnkmCo5evbykz4kz.toml rename to changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml diff --git a/changes/unreleased/4701.ah8Wi4SWc22EbgBc4KQeqH.toml b/changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml similarity index 100% rename from changes/unreleased/4701.ah8Wi4SWc22EbgBc4KQeqH.toml rename to changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml diff --git a/changes/unreleased/4709.dbwPVaU8vSacVkMLhiMjyJ.toml b/changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml similarity index 100% rename from changes/unreleased/4709.dbwPVaU8vSacVkMLhiMjyJ.toml rename to changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml diff --git a/changes/unreleased/4710.CSNixpvxJdLFaM6xSQ39Zf.toml b/changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml similarity index 100% rename from changes/unreleased/4710.CSNixpvxJdLFaM6xSQ39Zf.toml rename to changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml diff --git a/changes/unreleased/4712.8ckkAPAXGzedityWEyv363.toml b/changes/22.0_2025-03-15/4712.8ckkAPAXGzedityWEyv363.toml similarity index 100% rename from changes/unreleased/4712.8ckkAPAXGzedityWEyv363.toml rename to changes/22.0_2025-03-15/4712.8ckkAPAXGzedityWEyv363.toml diff --git a/changes/22.0_2025-03-15/4719.d4xMztC8JjrbVgEscxvMWX.toml b/changes/22.0_2025-03-15/4719.d4xMztC8JjrbVgEscxvMWX.toml new file mode 100644 index 00000000000..c220a3eb6f3 --- /dev/null +++ b/changes/22.0_2025-03-15/4719.d4xMztC8JjrbVgEscxvMWX.toml @@ -0,0 +1,5 @@ +internal = "Bump Version to v22.0" +[[pull_requests]] +uid = "4719" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/docs/auxil/kwargs_insertion.py b/docs/auxil/kwargs_insertion.py index 997baf91581..e24003f1d71 100644 --- a/docs/auxil/kwargs_insertion.py +++ b/docs/auxil/kwargs_insertion.py @@ -68,7 +68,7 @@ " seconds are used as write timeout." "", "", - " .. versionchanged:: NEXT.VERSION", + " .. versionchanged:: 22.0", " The default value changed to " " :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.", "", diff --git a/telegram/_inline/inlinequery.py b/telegram/_inline/inlinequery.py index 264936fd568..73bb3b43b4d 100644 --- a/telegram/_inline/inlinequery.py +++ b/telegram/_inline/inlinequery.py @@ -62,7 +62,7 @@ class InlineQuery(TelegramObject): ``auto_pagination``. Use a named argument for those, and notice that some positional arguments changed position as a result. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Removed constants ``MIN_START_PARAMETER_LENGTH`` and ``MAX_START_PARAMETER_LENGTH``. Use :attr:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` and :attr:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH` diff --git a/telegram/_message.py b/telegram/_message.py index 0919469e0f4..646266be84f 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -1755,7 +1755,7 @@ async def reply_text( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -1836,7 +1836,7 @@ async def reply_markdown( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Note: @@ -1920,7 +1920,7 @@ async def reply_markdown_v2( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2000,7 +2000,7 @@ async def reply_html( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2078,7 +2078,7 @@ async def reply_media_group( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2159,7 +2159,7 @@ async def reply_photo( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2244,7 +2244,7 @@ async def reply_audio( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2329,7 +2329,7 @@ async def reply_document( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2416,7 +2416,7 @@ async def reply_animation( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2498,7 +2498,7 @@ async def reply_sticker( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2583,7 +2583,7 @@ async def reply_video( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2671,7 +2671,7 @@ async def reply_video_note( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2751,7 +2751,7 @@ async def reply_voice( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2833,7 +2833,7 @@ async def reply_location( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -2918,7 +2918,7 @@ async def reply_venue( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -3001,7 +3001,7 @@ async def reply_contact( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -3089,7 +3089,7 @@ async def reply_poll( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -3173,7 +3173,7 @@ async def reply_dice( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -3288,7 +3288,7 @@ async def reply_game( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: @@ -3380,7 +3380,7 @@ async def reply_invoice( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Warning: @@ -3602,7 +3602,7 @@ async def reply_copy( .. versionchanged:: 21.1 |reply_same_thread| - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |quote_removed| Keyword Args: diff --git a/telegram/_passport/passportelementerrors.py b/telegram/_passport/passportelementerrors.py index ef0a261221b..00c5bd50d7e 100644 --- a/telegram/_passport/passportelementerrors.py +++ b/telegram/_passport/passportelementerrors.py @@ -170,7 +170,7 @@ class PassportElementErrorFiles(PassportElementError): ``"passport_registration"``, ``"temporary_registration"``. file_hashes (Sequence[:obj:`str`]): List of base64-encoded file hashes. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |sequenceargs| message (:obj:`str`): Error message. @@ -180,7 +180,7 @@ class PassportElementErrorFiles(PassportElementError): ``"passport_registration"``, ``"temporary_registration"``. file_hashes (tuple[:obj:`str`]): List of base64-encoded file hashes. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |tupleclassattrs| message (:obj:`str`): Error message. @@ -372,7 +372,7 @@ class PassportElementErrorTranslationFiles(PassportElementError): ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hashes (Sequence[:obj:`str`]): List of base64-encoded file hashes. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |sequenceargs| message (:obj:`str`): Error message. @@ -383,7 +383,7 @@ class PassportElementErrorTranslationFiles(PassportElementError): ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hashes (tuple[:obj:`str`]): List of base64-encoded file hashes. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 |tupleclassattrs| message (:obj:`str`): Error message. diff --git a/telegram/_passport/passportfile.py b/telegram/_passport/passportfile.py index 6c6ed35f807..1f944a2610d 100644 --- a/telegram/_passport/passportfile.py +++ b/telegram/_passport/passportfile.py @@ -47,7 +47,7 @@ class PassportFile(TelegramObject): file_size (:obj:`int`): File size in bytes. file_date (:class:`datetime.datetime`): Time when the file was uploaded. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Accepts only :class:`datetime.datetime` instead of :obj:`int`. |datetime_localization| @@ -60,7 +60,7 @@ class PassportFile(TelegramObject): file_size (:obj:`int`): File size in bytes. file_date (:class:`datetime.datetime`): Time when the file was uploaded. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Returns :class:`datetime.datetime` instead of :obj:`int`. |datetime_localization| """ diff --git a/telegram/_version.py b/telegram/_version.py index 22c0fdc63d6..88eb033c8b8 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=11, micro=1, releaselevel="final", serial=0 + major=22, minor=0, micro=0, releaselevel="final", serial=0 ) __version__: Final[str] = str(__version_info__) diff --git a/telegram/constants.py b/telegram/constants.py index 681864662af..e9de34abb25 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -1376,7 +1376,7 @@ class InlineQueryLimit(IntEnum): .. versionadded:: 20.0 - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Removed deprecated attributes ``InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH`` and ``InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH``. Please instead use :attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` and diff --git a/telegram/ext/_applicationbuilder.py b/telegram/ext/_applicationbuilder.py index 5ceafa3ef0f..0b258584ab5 100644 --- a/telegram/ext/_applicationbuilder.py +++ b/telegram/ext/_applicationbuilder.py @@ -122,7 +122,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): .. seealso:: :wiki:`Your First Bot `, :wiki:`Builder Pattern ` - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Removed deprecated methods ``proxy_url`` and ``get_updates_proxy_url``. .. _`builder pattern`: https://en.wikipedia.org/wiki/Builder_pattern diff --git a/telegram/ext/_defaults.py b/telegram/ext/_defaults.py index 8de8a008e39..d7134766695 100644 --- a/telegram/ext/_defaults.py +++ b/telegram/ext/_defaults.py @@ -39,7 +39,7 @@ class Defaults: Removed the argument and attribute ``timeout``. Specify default timeout behavior for the networking backend directly via :class:`telegram.ext.ApplicationBuilder` instead. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Removed deprecated arguments and properties ``disable_web_page_preview`` and ``quote``. Use :paramref:`link_preview_options` and :paramref:`do_quote` instead. diff --git a/telegram/ext/_updater.py b/telegram/ext/_updater.py index fccb6a65f60..a2a921672dd 100644 --- a/telegram/ext/_updater.py +++ b/telegram/ext/_updater.py @@ -217,7 +217,7 @@ async def start_polling( .. versionchanged:: 20.0 Removed the ``clean`` argument in favor of :paramref:`drop_pending_updates`. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Removed the deprecated arguments ``read_timeout``, ``write_timeout``, ``connect_timeout``, and ``pool_timeout`` in favor of setting the timeouts via the corresponding methods of :class:`telegram.ext.ApplicationBuilder`. or diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index ebdfcae3616..8757fe3e7d8 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -34,7 +34,7 @@ * Filters which do both (like ``Filters.text``) are now split as ready-to-use version ``filters.TEXT`` and class version ``filters.Text(...)``. -.. versionchanged:: NEXT.VERSION +.. versionchanged:: 22.0 Removed deprecated attribute `CHAT`. """ @@ -1903,7 +1903,7 @@ class StatusUpdate: Caution: ``filters.StatusUpdate`` itself is *not* a filter, but just a convenience namespace. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Removed deprecated attribute `USER_SHARED`. """ diff --git a/telegram/request/_baserequest.py b/telegram/request/_baserequest.py index 9d8b3e2f3e3..666f2d042db 100644 --- a/telegram/request/_baserequest.py +++ b/telegram/request/_baserequest.py @@ -138,7 +138,7 @@ def read_timeout(self) -> Optional[float]: :paramref:`post.read_timeout` of :meth:post` is not passed/equal to :attr:`DEFAULT_NONE`. .. versionadded:: 20.7 - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 This property is now required to be implemented by subclasses. Returns: diff --git a/telegram/request/_httpxrequest.py b/telegram/request/_httpxrequest.py index 78307947592..32b639526ee 100644 --- a/telegram/request/_httpxrequest.py +++ b/telegram/request/_httpxrequest.py @@ -43,7 +43,7 @@ class HTTPXRequest(BaseRequest): .. versionadded:: 20.0 - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.0 Removed the deprecated parameter ``proxy_url``. Use :paramref:`proxy` instead. Args: From 83676dec16d2734fb4f31d738c1633c45d837d15 Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 6 Apr 2025 21:04:00 +0300 Subject: [PATCH 12/36] Reenable `test_official` Blocked by Debug Remnant (#4746) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml | 5 +++++ tests/test_official/test_official.py | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml diff --git a/changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml b/changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml new file mode 100644 index 00000000000..c24204d45c4 --- /dev/null +++ b/changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml @@ -0,0 +1,5 @@ +internal = "Reenable ``test_official`` Blocked by Debug Remnant" +[[pull_requests]] +uid = "4746" +author_uid = "aelkheir" +closes_threads = [] diff --git a/tests/test_official/test_official.py b/tests/test_official/test_official.py index 9b0a418a2bb..b699a2b2eea 100644 --- a/tests/test_official/test_official.py +++ b/tests/test_official/test_official.py @@ -138,11 +138,6 @@ def test_check_object(tg_class: TelegramClass) -> None: - No unexpected parameters """ obj = getattr(telegram, tg_class.class_name) - if tg_class.class_name not in ( - "PassportElementErrorFiles", - "PassportElementErrorTranslationFiles", - ): - return # Check arguments based on source. Makes sure to only check __init__'s signature & nothing else sig = inspect.signature(obj.__init__, follow_wrapped=True) From 3cd8a409ee2c637b488391921d7bbf5cdd056dbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 20:10:00 +0200 Subject: [PATCH 13/36] Bump `actions/download-artifact` from 4.1.8 to 4.2.1 (#4745) 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> --- .github/workflows/release_pypi.yml | 6 +++--- .github/workflows/release_test_pypi.yml | 6 +++--- changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index c59a4579561..633c0b5055a 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -55,7 +55,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: python-package-distributions path: dist/ @@ -74,7 +74,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: python-package-distributions path: dist/ @@ -111,7 +111,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: python-package-distributions-and-signatures path: dist/ diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index ac8872ff509..aee88e62565 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -55,7 +55,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: python-package-distributions path: dist/ @@ -76,7 +76,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: python-package-distributions path: dist/ @@ -113,7 +113,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: python-package-distributions-and-signatures path: dist/ diff --git a/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml b/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml new file mode 100644 index 00000000000..46cfa35817e --- /dev/null +++ b/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml @@ -0,0 +1,5 @@ +internal = "Bump actions/download-artifact from 4.1.8 to 4.2.1" +[[pull_requests]] +uid = "4745" +author_uid = "dependabot[bot]" +closes_threads = [] From 036910ec0c2338701d150b512f19b2c7387ff8fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 20:25:13 +0200 Subject: [PATCH 14/36] Bump `astral-sh/setup-uv` from 5.3.1 to 5.4.1 (#4744) 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> --- .github/workflows/gha_security.yml | 2 +- changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml index aaa72e61cae..9a18a6710e4 100644 --- a/.github/workflows/gha_security.yml +++ b/.github/workflows/gha_security.yml @@ -21,7 +21,7 @@ jobs: with: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5.3.1 + uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 - name: Run zizmor run: uvx zizmor --persona=pedantic --format sarif . > results.sarif env: diff --git a/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml b/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml new file mode 100644 index 00000000000..2f2aa2749be --- /dev/null +++ b/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml @@ -0,0 +1,5 @@ +internal = "Bump astral-sh/setup-uv from 5.3.1 to 5.4.1" +[[pull_requests]] +uid = "4744" +author_uid = "dependabot[bot]" +closes_threads = [] From 511222c19171a75ae6ddf044e5f8c91ded247a68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 20:25:39 +0200 Subject: [PATCH 15/36] Bump `codecov/test-results-action` from 1.0.2 to 1.1.0 (#4741) 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> --- .github/workflows/unit_tests.yml | 2 +- changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index fd914bf91b4..7c14dd51891 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -99,7 +99,7 @@ jobs: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov - uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2 + uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0 if: ${{ !cancelled() }} with: files: .test_report_no_optionals_junit.xml,.test_report_optionals_junit.xml diff --git a/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml b/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml new file mode 100644 index 00000000000..242308bcf0b --- /dev/null +++ b/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml @@ -0,0 +1,5 @@ +internal = "Bump codecov/test-results-action from 1.0.2 to 1.1.0" +[[pull_requests]] +uid = "4741" +author_uid = "dependabot[bot]" +closes_threads = [] From e69069d2c866e6cf46847afec1a349c5a85bad5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 20:32:46 +0200 Subject: [PATCH 16/36] Bump `github/codeql-action` from 3.28.10 to 3.28.13 (#4743) 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> --- .github/workflows/gha_security.yml | 2 +- changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml index 9a18a6710e4..c69f88c9c57 100644 --- a/.github/workflows/gha_security.yml +++ b/.github/workflows/gha_security.yml @@ -27,7 +27,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 with: sarif_file: results.sarif category: zizmor \ No newline at end of file diff --git a/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml b/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml new file mode 100644 index 00000000000..36a54cf340f --- /dev/null +++ b/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml @@ -0,0 +1,5 @@ +internal = "Bump github/codeql-action from 3.28.10 to 3.28.13" +[[pull_requests]] +uid = "4743" +author_uid = "dependabot[bot]" +closes_threads = [] From a9e53af3d1d4a921312efd73a6587dec303f16cf Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:55:31 +0200 Subject: [PATCH 17/36] Update `AUTHORS.rst`, Adding @aelkheir to Active Development Team (#4747) --- AUTHORS.rst | 8 ++++---- changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml diff --git a/AUTHORS.rst b/AUTHORS.rst index 98e5b686de1..61535397919 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -7,10 +7,8 @@ The current development team includes - `Hinrich Mahler `_ (maintainer) - `Poolitzer `_ (community liaison) -- `Shivam `_ - `Harshil `_ -- `Dmitry Kolomatskiy `_ -- `Aditya `_ +- `Abdelrahman `_ Emeritus maintainers include `Jannes Hรถke `_ (`@jh0ker `_ on Telegram), @@ -21,7 +19,7 @@ Contributors The following wonderful people contributed directly or indirectly to this project: -- `Abdelrahman `_ +- `Aditya `_ - `Abshar `_ - `Abubakar Alaya `_ - `Alateas `_ @@ -42,6 +40,7 @@ The following wonderful people contributed directly or indirectly to this projec - `daimajia `_ - `Daniel Reed `_ - `D David Livingston `_ +- `Dmitry Kolomatskiy `_ - `DonalDuck004 `_ - `Eana Hufwe `_ - `Ehsan Online `_ @@ -122,6 +121,7 @@ The following wonderful people contributed directly or indirectly to this projec - `Sam Mosleh `_ - `Sascha `_ - `Shelomentsev D `_ +- `Shivam `_ - `Shivam Saini `_ - `Siloรฉ Garcez `_ - `Simon Schรผrrle `_ diff --git a/changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml b/changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml new file mode 100644 index 00000000000..e6bb47332f9 --- /dev/null +++ b/changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml @@ -0,0 +1,5 @@ +documentation = "Update ``AUTHORS.rst``, Adding `@aelkheir `_ to Active Development Team" +[[pull_requests]] +uid = "4747" +author_uid = "Bibo-Joshi" +closes_threads = [] From 7823822a41c2b9d750399d8e26e504a9649b5904 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:26:31 +0200 Subject: [PATCH 18/36] Bump `actions/setup-python` from 5.4.0 to 5.5.0 (#4742) 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> --- .github/workflows/docs-admonitions.yml | 2 +- .github/workflows/docs-linkcheck.yml | 4 ++-- .github/workflows/release_pypi.yml | 2 +- .github/workflows/release_test_pypi.yml | 2 +- .github/workflows/test_official.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml | 2 +- changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml | 2 +- changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml | 2 +- changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml | 2 +- changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml | 2 +- changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml | 2 +- changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml | 2 +- changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml | 2 +- changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml | 5 +++++ changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml | 2 +- changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml | 2 +- changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml | 2 +- telegram/_payment/stars/transactionpartner.py | 2 +- 19 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml diff --git a/.github/workflows/docs-admonitions.yml b/.github/workflows/docs-admonitions.yml index 52dac4cef94..00b03ae4cca 100644 --- a/.github/workflows/docs-admonitions.yml +++ b/.github/workflows/docs-admonitions.yml @@ -28,7 +28,7 @@ jobs: with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.github/workflows/docs-linkcheck.yml b/.github/workflows/docs-linkcheck.yml index 1c6f56ab8bc..b25405b75cc 100644 --- a/.github/workflows/docs-linkcheck.yml +++ b/.github/workflows/docs-linkcheck.yml @@ -23,7 +23,7 @@ jobs: with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -31,7 +31,7 @@ jobs: python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -r requirements-dev-all.txt - name: Check Links - run: sphinx-build docs/source docs/build/html -W --keep-going -j auto -b linkcheck + run: sphinx-build docs/source docs/build/html --keep-going -j auto -b linkcheck - name: Upload linkcheck output # Run also if the previous steps failed if: always() diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index 633c0b5055a..e513b03b3bf 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -21,7 +21,7 @@ jobs: with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: "3.x" - name: Install pypa/build diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index aee88e62565..1130d2e9e7c 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -21,7 +21,7 @@ jobs: with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: "3.x" - name: Install pypa/build diff --git a/.github/workflows/test_official.yml b/.github/workflows/test_official.yml index 6eae5e4bcf6..14224d0901a 100644 --- a/.github/workflows/test_official.yml +++ b/.github/workflows/test_official.yml @@ -27,7 +27,7 @@ jobs: with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 7c14dd51891..a24b39a9941 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -30,7 +30,7 @@ jobs: with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml b/changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml index d1bdf38d738..abdb8f95575 100644 --- a/changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml +++ b/changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml @@ -1,5 +1,5 @@ internal = "Bump github/codeql-action from 3.28.8 to 3.28.10" [[pull_requests]] uid = "4697" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml b/changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml index 6ba893a7200..93eb25aa5b5 100644 --- a/changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml +++ b/changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml @@ -1,5 +1,5 @@ internal = "Bump srvaroa/labeler from 1.12.0 to 1.13.0" [[pull_requests]] uid = "4698" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml b/changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml index 23bfcbfd4c7..6d028a80509 100644 --- a/changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml +++ b/changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml @@ -1,5 +1,5 @@ internal = "Bump astral-sh/setup-uv from 5.2.2 to 5.3.1" [[pull_requests]] uid = "4699" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml b/changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml index 6dc7d8a9e07..5f5355f6d2e 100644 --- a/changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml +++ b/changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml @@ -1,5 +1,5 @@ internal = "Bump Bibo-Joshi/chango from 0.3.1 to 0.3.2" [[pull_requests]] uid = "4700" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml b/changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml index a402f0e8990..ac941f29246 100644 --- a/changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml +++ b/changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml @@ -1,5 +1,5 @@ internal = "Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4" [[pull_requests]] uid = "4701" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml b/changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml index a697b214f38..5cc432cd401 100644 --- a/changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml +++ b/changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml @@ -1,5 +1,5 @@ internal = "Bump pytest from 8.3.4 to 8.3.5" [[pull_requests]] uid = "4709" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml b/changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml index 2d7787a14a9..4219b05acba 100644 --- a/changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml +++ b/changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml @@ -1,5 +1,5 @@ internal = "Bump sphinx from 8.1.3 to 8.2.3" [[pull_requests]] uid = "4710" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml b/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml index 242308bcf0b..aacb5f2d501 100644 --- a/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml +++ b/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml @@ -1,5 +1,5 @@ internal = "Bump codecov/test-results-action from 1.0.2 to 1.1.0" [[pull_requests]] uid = "4741" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml b/changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml new file mode 100644 index 00000000000..97463ed483f --- /dev/null +++ b/changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml @@ -0,0 +1,5 @@ +internal = "Bump actions/setup-python from 5.4.0 to 5.5.0" +[[pull_requests]] +uid = "4742" +author_uid = "dependabot" +closes_threads = [] diff --git a/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml b/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml index 36a54cf340f..b6724ab2917 100644 --- a/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml +++ b/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml @@ -1,5 +1,5 @@ internal = "Bump github/codeql-action from 3.28.10 to 3.28.13" [[pull_requests]] uid = "4743" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml b/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml index 2f2aa2749be..cb5f24ea554 100644 --- a/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml +++ b/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml @@ -1,5 +1,5 @@ internal = "Bump astral-sh/setup-uv from 5.3.1 to 5.4.1" [[pull_requests]] uid = "4744" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml b/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml index 46cfa35817e..cae16287a79 100644 --- a/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml +++ b/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml @@ -1,5 +1,5 @@ internal = "Bump actions/download-artifact from 4.1.8 to 4.2.1" [[pull_requests]] uid = "4745" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/telegram/_payment/stars/transactionpartner.py b/telegram/_payment/stars/transactionpartner.py index 4efe50cb7ee..811947581ee 100644 --- a/telegram/_payment/stars/transactionpartner.py +++ b/telegram/_payment/stars/transactionpartner.py @@ -56,7 +56,7 @@ class TransactionPartner(TelegramObject): .. versionadded:: 21.4 - ..versionchanged:: 21.11 + .. versionchanged:: 21.11 Added :class:`TransactionPartnerChat` Args: From ed9496b91a3d9a2009166ad8fdd890dc7a0d70fa Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Tue, 8 Apr 2025 19:45:10 +0200 Subject: [PATCH 19/36] Ensure Proper Execution of `Bot.shutdown` (#4733) --- .../unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml | 5 +++++ telegram/_bot.py | 4 +++- tests/test_bot.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml diff --git a/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml b/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml new file mode 100644 index 00000000000..579b6c3b37d --- /dev/null +++ b/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml @@ -0,0 +1,5 @@ +bugfixes = "Ensure execution of ``Bot.shutdown`` even if ``Bot.get_me()`` fails in ``Bot.initialize()``" +[[pull_requests]] +uid = "4733" +author_uid = "Poolitzer" +closes_threads = [] diff --git a/telegram/_bot.py b/telegram/_bot.py index cd868678611..c83a4791401 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -829,13 +829,15 @@ async def initialize(self) -> None: return await asyncio.gather(self._request[0].initialize(), self._request[1].initialize()) + # this needs to be set before we call get_me, since this can trigger an error in the + # request backend, which would then NOT lead to a proper shutdown if this flag isn't set + self._initialized = True # Since the bot is to be initialized only once, we can also use it for # verifying the token passed and raising an exception if it's invalid. try: await self.get_me() except InvalidToken as exc: raise InvalidToken(f"The token `{self._token}` was rejected by the server.") from exc - self._initialized = True async def shutdown(self) -> None: """Stop & clear resources used by this class. Currently just calls diff --git a/tests/test_bot.py b/tests/test_bot.py index eefd07b568d..2f8f2431282 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -402,6 +402,21 @@ async def shutdown(): assert self.test_flag == "stop" + async def test_shutdown_at_error_in_request_in_init(self, monkeypatch, offline_bot): + async def get_me_error(): + raise httpx.HTTPError("BadRequest wrong token sry :(") + + async def shutdown(*args): + self.test_flag = "stop" + + monkeypatch.setattr(offline_bot, "get_me", get_me_error) + monkeypatch.setattr(offline_bot, "shutdown", shutdown) + + async with offline_bot: + pass + + assert self.test_flag == "stop" + async def test_equality(self): async with ( make_bot(token=FALLBACKS[0]["token"]) as a, From c6e12b195862a626133d1eb2b25c2a4f85fd4dfc Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 11 Apr 2025 19:16:31 +0200 Subject: [PATCH 20/36] Drop Backward Compatibility for `user_id` in `send_gift` (#4692) --- .../4692.dVZs28GuwTFnNJdWkvPbNv.toml | 5 +++ telegram/_bot.py | 16 +++---- telegram/ext/_extbot.py | 4 +- tests/auxil/bot_method_checks.py | 3 -- tests/test_chat.py | 10 +---- tests/test_gifts.py | 44 ++----------------- tests/test_official/exceptions.py | 10 +---- tests/test_user.py | 10 +---- 8 files changed, 22 insertions(+), 80 deletions(-) create mode 100644 changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml diff --git a/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml b/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml new file mode 100644 index 00000000000..aebbd7e67c1 --- /dev/null +++ b/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml @@ -0,0 +1,5 @@ +breaking = "Drop backward compatibility for `user_id` in `send_gift` by updating the order of parameters. Please adapt your code accordingly or use keyword arguments." +[[pull_requests]] +uid = "4692" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/telegram/_bot.py b/telegram/_bot.py index c83a4791401..56a0d08a538 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9845,13 +9845,13 @@ async def get_available_gifts( async def send_gift( self, - user_id: Optional[int] = None, - gift_id: Union[str, Gift] = None, # type: ignore + gift_id: Union[str, Gift], text: Optional[str] = None, text_parse_mode: ODVInput[str] = DEFAULT_NONE, text_entities: Optional[Sequence["MessageEntity"]] = None, pay_for_upgrade: Optional[bool] = None, chat_id: Optional[Union[str, int]] = None, + user_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -9863,15 +9863,18 @@ async def send_gift( The gift can't be converted to Telegram Stars by the receiver. .. versionadded:: 21.8 + .. versionchanged:: NEXT.VERSION + Bot API 8.3 made :paramref:`user_id` optional. In version NEXT.VERSION, the methods + signature was changed accordingly. Args: + gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a + :class:`~telegram.Gift` object user_id (:obj:`int`, optional): Required if :paramref:`chat_id` is not specified. Unique identifier of the target user that will receive the gift. .. versionchanged:: 21.11 Now optional. - gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a - :class:`~telegram.Gift` object chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`user_id` is not specified. |chat_id_channel| It will receive the gift. @@ -9902,11 +9905,6 @@ async def send_gift( Raises: :class:`telegram.error.TelegramError` """ - # TODO: Remove when stability policy allows, tags: deprecated 21.11 - # also we should raise a deprecation warnung if anything is passed by - # position since it will be moved, not sure how - if gift_id is None: - raise TypeError("Missing required argument `gift_id`.") data: JSONDict = { "user_id": user_id, "gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id, diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index f77cfbf631b..20a72917074 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4476,13 +4476,13 @@ async def get_available_gifts( async def send_gift( self, - user_id: Optional[int] = None, - gift_id: Union[str, Gift] = None, # type: ignore + gift_id: Union[str, Gift], text: Optional[str] = None, text_parse_mode: ODVInput[str] = DEFAULT_NONE, text_entities: Optional[Sequence["MessageEntity"]] = None, pay_for_upgrade: Optional[bool] = None, chat_id: Optional[Union[str, int]] = None, + user_id: Optional[int] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index ca7b041be5c..d396a462725 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -351,9 +351,6 @@ def build_kwargs( allow_sending_without_reply=manually_passed_value, quote_parse_mode=manually_passed_value, ) - # TODO remove when gift_id isnt marked as optional anymore, tags: deprecated 21.11 - elif name == "gift_id": - kws[name] = "GIFT-ID" return kws diff --git a/tests/test_chat.py b/tests/test_chat.py index 5e01ad526a4..f53a0fdd2fe 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1330,15 +1330,7 @@ async def make_assertion_channel(*_, **kwargs): and kwargs["text_entities"] == "text_entities" ) - # TODO discuss if better way exists - # tags: deprecated 21.11 - with pytest.raises( - Exception, - match="Default for argument gift_id does not match the default of the Bot method.", - ): - assert check_shortcut_signature( - Chat.send_gift, Bot.send_gift, ["user_id", "chat_id"], [] - ) + assert check_shortcut_signature(Chat.send_gift, Bot.send_gift, ["user_id", "chat_id"], []) assert await check_shortcut_call( chat.send_gift, chat.get_bot(), "send_gift", ["user_id", "chat_id"] ) diff --git a/tests/test_gifts.py b/tests/test_gifts.py index 5af1dc58cf1..3b3ef52cb39 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -135,40 +135,8 @@ def test_equality(self, gift): ], ids=["string", "Gift"], ) - async def test_send_gift(self, offline_bot, gift, monkeypatch): - # We can't send actual gifts, so we just check that the correct parameters are passed - text_entities = [ - MessageEntity(MessageEntity.TEXT_LINK, 0, 4, "url"), - MessageEntity(MessageEntity.BOLD, 5, 9), - ] - - async def make_assertion(url, request_data: RequestData, *args, **kwargs): - user_id = request_data.parameters["user_id"] == "user_id" - gift_id = request_data.parameters["gift_id"] == "gift_id" - text = request_data.parameters["text"] == "text" - text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode" - tes = request_data.parameters["text_entities"] == [ - me.to_dict() for me in text_entities - ] - pay_for_upgrade = request_data.parameters["pay_for_upgrade"] is True - - return user_id and gift_id and text and text_parse_mode and tes and pay_for_upgrade - - monkeypatch.setattr(offline_bot.request, "post", make_assertion) - assert await offline_bot.send_gift( - "user_id", - gift, - "text", - text_parse_mode="text_parse_mode", - text_entities=text_entities, - pay_for_upgrade=True, - ) - @pytest.mark.parametrize("id_name", ["user_id", "chat_id"]) - async def test_send_gift_user_chat_id(self, offline_bot, gift, monkeypatch, id_name): - # Only here because we have to temporarily mark gift_id as optional. - # tags: deprecated 21.11 - + async def test_send_gift(self, offline_bot, gift, monkeypatch, id_name): # We can't send actual gifts, so we just check that the correct parameters are passed text_entities = [ MessageEntity(MessageEntity.TEXT_LINK, 0, 4, "url"), @@ -177,7 +145,7 @@ async def test_send_gift_user_chat_id(self, offline_bot, gift, monkeypatch, id_n async def make_assertion(url, request_data: RequestData, *args, **kwargs): received_id = request_data.parameters[id_name] == id_name - gift_id = request_data.parameters["gift_id"] == "some_id" + gift_id = request_data.parameters["gift_id"] == "gift_id" text = request_data.parameters["text"] == "text" text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode" tes = request_data.parameters["text_entities"] == [ @@ -189,18 +157,14 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): monkeypatch.setattr(offline_bot.request, "post", make_assertion) assert await offline_bot.send_gift( - gift_id=gift, - text="text", + gift, + "text", text_parse_mode="text_parse_mode", text_entities=text_entities, pay_for_upgrade=True, **{id_name: id_name}, ) - async def test_send_gift_without_gift_id(self, offline_bot): - with pytest.raises(TypeError, match="Missing required argument `gift_id`."): - await offline_bot.send_gift() - @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) @pytest.mark.parametrize( ("passed_value", "expected_value"), diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index fdc04adc553..d0d73fa4b7b 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -19,7 +19,7 @@ """This module contains exceptions to our API compared to the official API.""" import datetime as dtm -from telegram import Animation, Audio, Document, PhotoSize, Sticker, Video, VideoNote, Voice +from telegram import Animation, Audio, Document, Gift, PhotoSize, Sticker, Video, VideoNote, Voice from tests.test_official.helpers import _get_params_base IGNORED_OBJECTS = ("ResponseParameters",) @@ -47,8 +47,7 @@ class ParamTypeCheckingExceptions: "animation": Animation, "voice": Voice, "sticker": Sticker, - # TODO: Deprecated and will be corrected (and readded) in next major bot API release: - # "gift_id": Gift, + "gift_id": Gift, }, "(delete|set)_sticker.*": { "sticker$": Sticker, @@ -101,9 +100,6 @@ class ParamTypeCheckingExceptions: "EncryptedPassportElement": { "data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress] }, - # TODO: Deprecated and will be corrected (and removed) in next major PTB - # version: - "send_gift": {"gift_id": str}, # actual: Non optional } # param names ignored in the param type checking in classes for the `tg.Defaults` case. @@ -196,8 +192,6 @@ def ptb_ignored_params(object_name: str) -> set[str]: "send_venue": {"latitude", "longitude", "title", "address"}, "send_contact": {"phone_number", "first_name"}, # ----> - # here for backwards compatibility. Todo: remove on next bot api release - "send_gift": {"gift_id"}, } diff --git a/tests/test_user.py b/tests/test_user.py index 815785bcb7f..b7ea5f8bd26 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -731,15 +731,7 @@ async def make_assertion(*_, **kwargs): and kwargs["text_entities"] == "text_entities" ) - # TODO discuss if better way exists - # tags: deprecated 21.11 - with pytest.raises( - Exception, - match="Default for argument gift_id does not match the default of the Bot method.", - ): - assert check_shortcut_signature( - user.send_gift, Bot.send_gift, ["user_id", "chat_id"], [] - ) + assert check_shortcut_signature(user.send_gift, Bot.send_gift, ["user_id", "chat_id"], []) assert await check_shortcut_call( user.send_gift, user.get_bot(), "send_gift", ["chat_id", "user_id"] ) From 17ae6a7028fcd7638561d4daca2eb89cc94c3639 Mon Sep 17 00:00:00 2001 From: ngrogolev <37971059+ngrogolev@users.noreply.github.com> Date: Sat, 19 Apr 2025 18:02:43 +0300 Subject: [PATCH 21/36] Fix Handling of `Defaults` for `InputPaidMedia` (#4761) Co-authored-by: Nikita Grogolev --- changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml | 6 ++++++ telegram/ext/_extbot.py | 8 ++++++-- tests/auxil/bot_method_checks.py | 13 ++++++++++--- tests/test_bot.py | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml diff --git a/changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml b/changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml new file mode 100644 index 00000000000..a47dcdcc3b1 --- /dev/null +++ b/changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml @@ -0,0 +1,6 @@ +bugfixes = "Fix Handling of ``Defaults`` for ``InputPaidMedia``" + +[[pull_requests]] +uid = "4761" +author_uid = "ngrogolev" +closes_threads = ["4753"] diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 20a72917074..f74304b5b64 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -62,6 +62,7 @@ InlineKeyboardMarkup, InlineQueryResultsButton, InputMedia, + InputPaidMedia, InputPollOption, LinkPreviewOptions, Location, @@ -114,7 +115,6 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, - InputPaidMedia, InputSticker, LabeledPrice, MessageEntity, @@ -466,7 +466,11 @@ def _insert_defaults(self, data: dict[str, object]) -> None: with copied_val._unfrozen(): copied_val.parse_mode = self.defaults.parse_mode data[key] = copied_val - 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(media) for media in val] for media in copy_list: diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index d396a462725..18f7dd93e45 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -35,6 +35,7 @@ InlineQueryResultArticle, InlineQueryResultCachedPhoto, InputMediaPhoto, + InputPaidMediaPhoto, InputTextMessageContent, LinkPreviewOptions, ReplyParameters, @@ -285,8 +286,13 @@ def build_kwargs( elif name in ["prices", "commands", "errors"]: kws[name] = [] elif name == "media": - media = InputMediaPhoto("media", parse_mode=manually_passed_value) - if "list" in str(param.annotation).lower(): + if "star_count" in signature.parameters: + media = InputPaidMediaPhoto("media") + else: + media = InputMediaPhoto("media", parse_mode=manually_passed_value) + + param_annotation = str(param.annotation).lower() + if "sequence" in param_annotation or "list" in param_annotation: kws[name] = [media] else: kws[name] = media @@ -507,7 +513,8 @@ def check_input_media(m: dict): ) media = data.pop("media", None) - if media: + paid_media = media and data.pop("star_count", None) + if media and not paid_media: if isinstance(media, dict) and isinstance(media.get("type", None), InputMediaType): check_input_media(media) else: diff --git a/tests/test_bot.py b/tests/test_bot.py index 2f8f2431282..22ffc9accc7 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -543,7 +543,7 @@ def test_api_kwargs_and_timeouts_present(self, bot_class, bot_method_name, bot_m if bot_method_name.replace("_", "").lower() != "getupdates" and bot_class is ExtBot: assert rate_arg in param_names, f"{bot_method} is missing the parameter `{rate_arg}`" - @bot_methods(ext_bot=False) + @bot_methods() async def test_defaults_handling( self, bot_class, From 54ce1d8d8245eeb4115c20408540bb7b0b53e145 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 19 Apr 2025 18:40:47 +0200 Subject: [PATCH 22/36] Fine Tune `chango` and Release Workflows (#4758) --- .github/workflows/chango.yml | 66 +++++++++++++++++++ .github/workflows/chango_fragment.yml | 30 --------- .github/workflows/release_pypi.yml | 6 ++ .github/workflows/release_test_pypi.yml | 3 + changes/config.py | 29 +++++++- .../4758.dSyCdBJWEJroH2GynR2VaJ.toml | 5 ++ 6 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/chango.yml delete mode 100644 .github/workflows/chango_fragment.yml create mode 100644 changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml diff --git a/.github/workflows/chango.yml b/.github/workflows/chango.yml new file mode 100644 index 00000000000..eb1db8ef440 --- /dev/null +++ b/.github/workflows/chango.yml @@ -0,0 +1,66 @@ +name: Chango +on: + pull_request: + types: + - opened + - reopened + - synchronize + +permissions: {} + +jobs: + create-chango-fragment: + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push the + # added or changed files to the repository. + contents: write + name: Create chango Fragment + runs-on: ubuntu-latest + outputs: + IS_RELEASE_PR: ${{ steps.check_title.outputs.IS_RELEASE_PR }} + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # needed for commit and push step at the end + persist-credentials: true + - name: Check PR Title + id: check_title + run: | # zizmor: ignore[template-injection] + if [[ "$(echo "${{ github.event.pull_request.title }}" | tr '[:upper:]' '[:lower:]')" =~ ^bump\ version\ to\ .* ]]; then + echo "COMMIT_AND_PUSH=false" >> $GITHUB_OUTPUT + echo "IS_RELEASE_PR=true" >> $GITHUB_OUTPUT + else + echo "COMMIT_AND_PUSH=true" >> $GITHUB_OUTPUT + echo "IS_RELEASE_PR=false" >> $GITHUB_OUTPUT + fi + + # Create the new fragment + - uses: Bibo-Joshi/chango@9d6bd9d7612eca5fab2c5161687011be59baaf19 # v0.4.0 + with: + github-token: ${{ secrets.CHANGO_PAT }} + query-issue-types: true + commit-and-push: ${{ steps.check_title.outputs.COMMIT_AND_PUSH }} + + # Run `chango release` if applicable - needs some additional setup. + - name: Set up Python + if: steps.check_title.outputs.IS_RELEASE_PR == 'true' + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + with: + python-version: "3.x" + + - name: Do Release + if: steps.check_title.outputs.IS_RELEASE_PR == 'true' + run: | + cd ./target-repo + git add changes/unreleased/* + pip install . -r docs/requirements-docs.txt + VERSION_TAG=$(python -c "from telegram import __version__; print(f'{__version__}')") + chango release --uid $VERSION_TAG + + - name: Commit & Push + if: steps.check_title.outputs.IS_RELEASE_PR == 'true' + uses: stefanzweifel/git-auto-commit-action@e348103e9026cc0eee72ae06630dbe30c8bf7a79 # v5.1.0 + with: + commit_message: "Do chango Release" + repository: ./target-repo diff --git a/.github/workflows/chango_fragment.yml b/.github/workflows/chango_fragment.yml deleted file mode 100644 index 8a1cc25984c..00000000000 --- a/.github/workflows/chango_fragment.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Create Chango Fragment -on: - pull_request: - types: - - opened - - reopened - - synchronize - -permissions: {} - -jobs: - create-chango-fragment: - permissions: - # Give the default GITHUB_TOKEN write permission to commit and push the - # added or changed files to the repository. - contents: write - name: create-chango-fragment - runs-on: ubuntu-latest - steps: - - # Create the new fragment - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - uses: Bibo-Joshi/chango@9d6bd9d7612eca5fab2c5161687011be59baaf19 # v0.4.0 - with: - github-token: ${{ secrets.CHANGO_PAT }} - query-issue-types: true - - diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index e513b03b3bf..dcb22f3c6e4 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -110,6 +110,9 @@ jobs: actions: read # for downloading artifacts steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Download all the dists uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: @@ -150,6 +153,9 @@ jobs: permissions: {} steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Publish to Telegram Channel env: TAG: ${{ needs.build.outputs.TAG }} diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index 1130d2e9e7c..b4b82be06c2 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -112,6 +112,9 @@ jobs: actions: read # for downloading artifacts steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Download all the dists uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: diff --git a/changes/config.py b/changes/config.py index d7557990bad..1fd95fa9767 100644 --- a/changes/config.py +++ b/changes/config.py @@ -2,9 +2,12 @@ # pylint: disable=import-error """Configuration for the chango changelog tool""" +import re from collections.abc import Collection +from pathlib import Path from typing import Optional +from chango import Version from chango.concrete import DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory from chango.concrete.sections import GitHubSectionChangeNote, Section, SectionVersionNote @@ -70,7 +73,31 @@ def get_sections( return found or {"other"} -chango_instance = DirectoryChanGo( +class CustomChango(DirectoryChanGo): + """Custom ChanGo class for overriding release""" + + def release(self, version: Version) -> bool: + """replace "14.5" with version.uid except in the contrib guide + then call super + """ + root = Path(__file__).parent.parent / "telegram" + python_files = root.rglob("*.py") + pattern = re.compile(r"NEXT\.VERSION") + excluded_paths = {root / "docs/source/contribute.rst"} + for file_path in python_files: + if str(file_path) in excluded_paths: + continue + + content = file_path.read_text(encoding="utf-8") + modified = pattern.sub(version.uid, content) + + if content != modified: + file_path.write_text(modified, encoding="utf-8") + + return super().release(version) + + +chango_instance = CustomChango( change_note_type=ChangoSectionChangeNote, version_note_type=SectionVersionNote, version_history_type=HeaderVersionHistory, diff --git a/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml b/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml new file mode 100644 index 00000000000..23ffc153339 --- /dev/null +++ b/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml @@ -0,0 +1,5 @@ +internal = "Fine Tune ``chango`` and Release Workflows" +[[pull_requests]] +uid = "4758" +author_uid = "Bibo-Joshi" +closes_threads = ["4720"] From 486ceaa6cf48d3b451a5c039536c4c5d7ea72a2b Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 20 Apr 2025 10:48:51 +0200 Subject: [PATCH 23/36] Clarify Documentation and Type Hints of `Input(Paid)Media` (#4762) Co-authored-by: aelkheir <90580077+aelkheir@users.noreply.github.com> --- .../4762.PbcJGM8KPBMbKri3fdHKjh.toml | 5 +++++ telegram/_files/inputmedia.py | 20 ++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) create mode 100644 changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml diff --git a/changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml b/changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml new file mode 100644 index 00000000000..0aeebe750b6 --- /dev/null +++ b/changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml @@ -0,0 +1,5 @@ +documentation = "Clarify Documentation and Type Hints of ``InputMedia`` and ``InputPaidMedia``. Note that the ``media`` parameter accepts only objects of type ``str`` and ``InputFile``. The respective subclasses of ``Input(Paid)Media`` each accept a broader range of input type for the ``media`` parameter." +[[pull_requests]] +uid = "4762" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index a36590f9746..017e1b423fe 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -51,13 +51,8 @@ class InputMedia(TelegramObject): Args: media_type (:obj:`str`): Type of media that the instance represents. - media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ - :class:`pathlib.Path` | :class:`telegram.Animation` | :class:`telegram.Audio` | \ - :class:`telegram.Document` | :class:`telegram.PhotoSize` | \ - :class:`telegram.Video`): File to send. + media (:obj:`str` | :class:`~telegram.InputFile`): File to send. |fileinputnopath| - Lastly you can pass an existing telegram media object of the corresponding type - to send. caption (:obj:`str`, optional): Caption of the media to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -89,7 +84,7 @@ class InputMedia(TelegramObject): def __init__( self, media_type: str, - media: Union[str, InputFile, MediaType], + media: Union[str, InputFile], caption: Optional[str] = None, caption_entities: Optional[Sequence[MessageEntity]] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, @@ -98,7 +93,7 @@ def __init__( ): super().__init__(api_kwargs=api_kwargs) self.type: str = enum.get_member(constants.InputMediaType, media_type, media_type) - self.media: Union[str, InputFile, Animation, Audio, Document, PhotoSize, Video] = media + self.media: Union[str, InputFile] = media self.caption: Optional[str] = caption self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) self.parse_mode: ODVInput[str] = parse_mode @@ -129,11 +124,8 @@ class InputPaidMedia(TelegramObject): Args: type (:obj:`str`): Type of media that the instance represents. - media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ - :class:`pathlib.Path` | :class:`telegram.PhotoSize` | :class:`telegram.Video`): File + media (:obj:`str` | :class:`~telegram.InputFile`): 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. @@ -150,13 +142,13 @@ class InputPaidMedia(TelegramObject): def __init__( self, type: str, # pylint: disable=redefined-builtin - media: Union[str, InputFile, PhotoSize, Video], + media: Union[str, InputFile], *, 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.media: Union[str, InputFile] = media self._freeze() From 4868565b71400be83cc83238bbc63afe8087f059 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 May 2025 20:38:58 +0200 Subject: [PATCH 24/36] Bump `github/codeql-action` from 3.28.13 to 3.28.16 (#4778) 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> --- .github/workflows/gha_security.yml | 2 +- changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml index c69f88c9c57..df0d0f10bb5 100644 --- a/.github/workflows/gha_security.yml +++ b/.github/workflows/gha_security.yml @@ -27,7 +27,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: sarif_file: results.sarif category: zizmor \ No newline at end of file diff --git a/changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml b/changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml new file mode 100644 index 00000000000..f25371c42ea --- /dev/null +++ b/changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml @@ -0,0 +1,5 @@ +internal = "Bump github/codeql-action from 3.28.13 to 3.28.16" +[[pull_requests]] +uid = "4778" +author_uid = "dependabot[bot]" +closes_threads = [] From b0faae9d47bc3ba3065e40eeb7c17963aada6cfe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 May 2025 20:39:19 +0200 Subject: [PATCH 25/36] Bump `stefanzweifel/git-auto-commit-action` from 5.1.0 to 5.2.0 (#4777) 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> --- .github/workflows/chango.yml | 2 +- changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml diff --git a/.github/workflows/chango.yml b/.github/workflows/chango.yml index eb1db8ef440..d845f6bc019 100644 --- a/.github/workflows/chango.yml +++ b/.github/workflows/chango.yml @@ -60,7 +60,7 @@ jobs: - name: Commit & Push if: steps.check_title.outputs.IS_RELEASE_PR == 'true' - uses: stefanzweifel/git-auto-commit-action@e348103e9026cc0eee72ae06630dbe30c8bf7a79 # v5.1.0 + uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0 with: commit_message: "Do chango Release" repository: ./target-repo diff --git a/changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml b/changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml new file mode 100644 index 00000000000..76660e69223 --- /dev/null +++ b/changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml @@ -0,0 +1,5 @@ +internal = "Bump stefanzweifel/git-auto-commit-action from 5.1.0 to 5.2.0" +[[pull_requests]] +uid = "4777" +author_uid = "dependabot[bot]" +closes_threads = [] From 4c614033226742ac7c3bf92cd94818df40f433e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 May 2025 20:40:02 +0200 Subject: [PATCH 26/36] Bump `codecov/codecov-action` from 5.1.2 to 5.4.2 (#4775) 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> --- .github/workflows/unit_tests.yml | 2 +- changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index a24b39a9941..affb519fce2 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -92,7 +92,7 @@ jobs: .test_report_optionals_junit.xml - name: Submit coverage - uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 with: env_vars: OS,PYTHON name: ${{ matrix.os }}-${{ matrix.python-version }} diff --git a/changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml b/changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml new file mode 100644 index 00000000000..f70c7395590 --- /dev/null +++ b/changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml @@ -0,0 +1,5 @@ +internal = "Bump codecov/codecov-action from 5.1.2 to 5.4.2" +[[pull_requests]] +uid = "4775" +author_uid = "dependabot[bot]" +closes_threads = [] From 08006013c3442130a63d5e40d7e76b3beb370edf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 May 2025 20:40:35 +0200 Subject: [PATCH 27/36] Bump `actions/download-artifact` from 4.2.1 to 4.3.0 (#4779) 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> --- .github/workflows/release_pypi.yml | 6 +++--- .github/workflows/release_test_pypi.yml | 6 +++--- changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index dcb22f3c6e4..69e6438bd33 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -55,7 +55,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: python-package-distributions path: dist/ @@ -74,7 +74,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: python-package-distributions path: dist/ @@ -114,7 +114,7 @@ jobs: with: persist-credentials: false - name: Download all the dists - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: python-package-distributions-and-signatures path: dist/ diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index b4b82be06c2..25937ec564b 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -55,7 +55,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: python-package-distributions path: dist/ @@ -76,7 +76,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: python-package-distributions path: dist/ @@ -116,7 +116,7 @@ jobs: with: persist-credentials: false - name: Download all the dists - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: python-package-distributions-and-signatures path: dist/ diff --git a/changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml b/changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml new file mode 100644 index 00000000000..6aa2e548756 --- /dev/null +++ b/changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml @@ -0,0 +1,5 @@ +internal = "Bump actions/download-artifact from 4.2.1 to 4.3.0" +[[pull_requests]] +uid = "4779" +author_uid = "dependabot[bot]" +closes_threads = [] From 2fc04e1e1033cd57003148845ab70488b6395a90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 May 2025 21:03:39 +0200 Subject: [PATCH 28/36] Bump `actions/upload-artifact` from 4.5.0 to 4.6.2 (#4776) 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> --- .github/workflows/docs-linkcheck.yml | 2 +- .github/workflows/release_pypi.yml | 4 ++-- .github/workflows/release_test_pypi.yml | 4 ++-- README.rst | 2 +- changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml | 2 +- changes/unreleased/4776.g83DxRk4WVWCC8rCd6ocFC.toml | 5 +++++ changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml | 2 +- changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml | 2 +- changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml | 2 +- docs/source/conf.py | 2 ++ 10 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 changes/unreleased/4776.g83DxRk4WVWCC8rCd6ocFC.toml diff --git a/.github/workflows/docs-linkcheck.yml b/.github/workflows/docs-linkcheck.yml index b25405b75cc..65453ad11f3 100644 --- a/.github/workflows/docs-linkcheck.yml +++ b/.github/workflows/docs-linkcheck.yml @@ -35,7 +35,7 @@ jobs: - name: Upload linkcheck output # Run also if the previous steps failed if: always() - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: linkcheck-output path: docs/build/html/output.* diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index 69e6438bd33..a9e9e468010 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -30,7 +30,7 @@ jobs: - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions path: dist/ @@ -92,7 +92,7 @@ jobs: ./dist/*.tar.gz ./dist/*.whl - name: Store the distribution packages and signatures - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions-and-signatures path: dist/ diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index 25937ec564b..a59baec5e67 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -30,7 +30,7 @@ jobs: - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions path: dist/ @@ -94,7 +94,7 @@ jobs: ./dist/*.tar.gz ./dist/*.whl - name: Store the distribution packages and signatures - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions-and-signatures path: dist/ diff --git a/README.rst b/README.rst index d19a93d4d3f..d847fd3140c 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ :target: https://pypistats.org/packages/python-telegram-bot :alt: PyPi Package Monthly Download -.. image:: https://readthedocs.org/projects/python-telegram-bot/badge/?version=stable +.. image:: https://app.readthedocs.org/projects/python-telegram-bot/badge/?version=stable :target: https://docs.python-telegram-bot.org/en/stable/ :alt: Documentation Status diff --git a/changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml b/changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml index f70c7395590..b01e19eb5ec 100644 --- a/changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml +++ b/changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml @@ -1,5 +1,5 @@ internal = "Bump codecov/codecov-action from 5.1.2 to 5.4.2" [[pull_requests]] uid = "4775" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/unreleased/4776.g83DxRk4WVWCC8rCd6ocFC.toml b/changes/unreleased/4776.g83DxRk4WVWCC8rCd6ocFC.toml new file mode 100644 index 00000000000..2af8ebcd2d6 --- /dev/null +++ b/changes/unreleased/4776.g83DxRk4WVWCC8rCd6ocFC.toml @@ -0,0 +1,5 @@ +internal = "Bump actions/upload-artifact from 4.5.0 to 4.6.2" +[[pull_requests]] +uid = "4776" +author_uid = "dependabot" +closes_threads = [] diff --git a/changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml b/changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml index 76660e69223..4b5e40bad26 100644 --- a/changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml +++ b/changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml @@ -1,5 +1,5 @@ internal = "Bump stefanzweifel/git-auto-commit-action from 5.1.0 to 5.2.0" [[pull_requests]] uid = "4777" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml b/changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml index f25371c42ea..c14276f7821 100644 --- a/changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml +++ b/changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml @@ -1,5 +1,5 @@ internal = "Bump github/codeql-action from 3.28.13 to 3.28.16" [[pull_requests]] uid = "4778" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml b/changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml index 6aa2e548756..b6917ffef1b 100644 --- a/changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml +++ b/changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml @@ -1,5 +1,5 @@ internal = "Bump actions/download-artifact from 4.2.1 to 4.3.0" [[pull_requests]] uid = "4779" -author_uid = "dependabot[bot]" +author_uid = "dependabot" closes_threads = [] diff --git a/docs/source/conf.py b/docs/source/conf.py index fbb8b43168e..a0352d2c509 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -125,6 +125,8 @@ # The doc-fixes branch may not always exist - doesn't matter, we only link to it from the # contributing guide re.escape("https://docs.python-telegram-bot.org/en/doc-fixes"), + # Apparently has some human-verification check and gives 403 in the sphinx build + re.escape("https://stackoverflow.com/questions/tagged/python-telegram-bot"), ] linkcheck_allowed_redirects = { # Redirects to the default version are okay From c34e19edfdaaf7e592fbbef6c0fc3b470c519f0e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 21:23:25 +0200 Subject: [PATCH 29/36] Bump `pre-commit` Hooks to Latest Versions (#4748) 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 | 12 ++++++------ changes/unreleased/4748.j3cKusZZKqTLbc542K4sqJ.toml | 5 +++++ docs/auxil/sphinx_hooks.py | 10 +++++++--- examples/arbitrarycallbackdatabot.py | 2 +- pyproject.toml | 3 +-- telegram/_bot.py | 4 ++-- telegram/_telegramobject.py | 2 +- telegram/_utils/files.py | 4 ++-- telegram/ext/_application.py | 2 +- telegram/ext/_baseupdateprocessor.py | 2 +- telegram/ext/_callbackdatacache.py | 2 +- telegram/ext/_dictpersistence.py | 2 +- telegram/ext/_extbot.py | 2 +- telegram/ext/_handlers/callbackqueryhandler.py | 2 +- telegram/ext/_handlers/choseninlineresulthandler.py | 2 +- telegram/ext/_handlers/conversationhandler.py | 4 ++-- telegram/ext/_handlers/inlinequeryhandler.py | 2 +- telegram/ext/_picklepersistence.py | 2 +- telegram/ext/_updater.py | 2 +- telegram/ext/filters.py | 4 ++-- tests/_files/test_inputmedia.py | 1 - tests/auxil/bot_method_checks.py | 2 +- tests/ext/test_application.py | 3 +-- tests/ext/test_applicationbuilder.py | 1 - tests/ext/test_conversationhandler.py | 3 +-- 25 files changed, 42 insertions(+), 38 deletions(-) create mode 100644 changes/unreleased/4748.j3cKusZZKqTLbc542K4sqJ.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b56f457ed3..a6002c846bf 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.8.6' + rev: 'v0.11.9' hooks: - id: ruff name: ruff @@ -18,18 +18,18 @@ repos: - cachetools>=5.3.3,<5.5.0 - aiolimiter~=1.1,<1.3 - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.10.0 + rev: 25.1.0 hooks: - id: black args: - --diff - --check - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 + rev: 7.2.0 hooks: - id: flake8 - repo: https://github.com/PyCQA/pylint - rev: v3.3.3 + rev: v3.3.6 hooks: - id: pylint files: ^(?!(tests|docs)).*\.py$ @@ -41,7 +41,7 @@ repos: - aiolimiter~=1.1,<1.3 - . # this basically does `pip install -e .` - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 + rev: v1.15.0 hooks: - id: mypy name: mypy-ptb @@ -74,7 +74,7 @@ repos: args: - --py39-plus - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort name: isort diff --git a/changes/unreleased/4748.j3cKusZZKqTLbc542K4sqJ.toml b/changes/unreleased/4748.j3cKusZZKqTLbc542K4sqJ.toml new file mode 100644 index 00000000000..a719b9ecd07 --- /dev/null +++ b/changes/unreleased/4748.j3cKusZZKqTLbc542K4sqJ.toml @@ -0,0 +1,5 @@ +internal = "Bump `pre-commit` Hooks to Latest Versions" +[[pull_requests]] +uid = "4748" +author_uid = "pre-commit-ci" +closes_threads = [] diff --git a/docs/auxil/sphinx_hooks.py b/docs/auxil/sphinx_hooks.py index 53f3c4edea0..47fd9c9281c 100644 --- a/docs/auxil/sphinx_hooks.py +++ b/docs/auxil/sphinx_hooks.py @@ -15,12 +15,12 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -import collections.abc import contextlib import inspect import re import typing from pathlib import Path +from typing import TYPE_CHECKING from sphinx.application import Sphinx @@ -37,6 +37,10 @@ ) from docs.auxil.link_code import LINE_NUMBERS +if TYPE_CHECKING: + import collections.abc + + ADMONITION_INSERTER = AdmonitionInserter() # Some base classes are implementation detail @@ -128,7 +132,7 @@ def autodoc_process_docstring( insert_idx += len(effective_insert) ADMONITION_INSERTER.insert_admonitions( - obj=typing.cast(collections.abc.Callable, obj), + obj=typing.cast("collections.abc.Callable", obj), docstring_lines=lines, ) @@ -136,7 +140,7 @@ def autodoc_process_docstring( # (where applicable) if what == "class": ADMONITION_INSERTER.insert_admonitions( - obj=typing.cast(type, obj), # since "what" == class, we know it's not just object + obj=typing.cast("type", obj), # since "what" == class, we know it's not just object docstring_lines=lines, ) diff --git a/examples/arbitrarycallbackdatabot.py b/examples/arbitrarycallbackdatabot.py index e11620c1670..64971817bfb 100644 --- a/examples/arbitrarycallbackdatabot.py +++ b/examples/arbitrarycallbackdatabot.py @@ -69,7 +69,7 @@ async def list_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non # Get the data from the callback_data. # If you're using a type checker like MyPy, you'll have to use typing.cast # to make the checker get the expected type of the callback_data - number, number_list = cast(tuple[int, list[int]], query.data) + number, number_list = cast("tuple[int, list[int]]", query.data) # append the number to the list number_list.append(number) diff --git a/pyproject.toml b/pyproject.toml index f036d57a533..1ffe02f8efe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,8 +125,7 @@ line-length = 99 show-fixes = true [tool.ruff.lint] -preview = true -explicit-preview-rules = true # TODO: Drop this when RUF022 and RUF023 are out of preview +typing-extensions = false 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", diff --git a/telegram/_bot.py b/telegram/_bot.py index 56a0d08a538..49847efd3d4 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -3928,7 +3928,7 @@ async def get_file( api_kwargs=api_kwargs, ) - file_path = cast(dict, result).get("file_path") + file_path = cast("dict", result).get("file_path") if file_path and not is_local_file(file_path): result["file_path"] = f"{self._base_file_url}/{file_path}" @@ -4591,7 +4591,7 @@ async def get_updates( # waiting for the server to return and there's no way of knowing the connection had been # dropped in real time. result = cast( - list[JSONDict], + "list[JSONDict]", await self._post( "getUpdates", data, diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index 1b1b30ed092..ca0d20555eb 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -290,7 +290,7 @@ def __setstate__(self, state: dict[str, object]) -> None: self._bot = None # get api_kwargs first because we may need to add entries to it (see try-except below) - api_kwargs = cast(dict[str, object], state.pop("api_kwargs", {})) + api_kwargs = cast("dict[str, object]", state.pop("api_kwargs", {})) # get _frozen before the loop to avoid setting it to True in the loop frozen = state.pop("_frozen", False) diff --git a/telegram/_utils/files.py b/telegram/_utils/files.py index 8bce30d64c5..a750e1512e1 100644 --- a/telegram/_utils/files.py +++ b/telegram/_utils/files.py @@ -59,7 +59,7 @@ def load_file( try: contents = obj.read() # type: ignore[union-attr] except AttributeError: - return None, cast(Union[bytes, "InputFile", str, Path], obj) + return None, cast("Union[bytes, InputFile, str, Path]", obj) filename = guess_file_name(obj) @@ -151,7 +151,7 @@ def parse_file_input( # pylint: disable=too-many-return-statements if isinstance(file_input, bytes): return InputFile(file_input, filename=filename, attach=attach) if hasattr(file_input, "read"): - return InputFile(cast(IO, file_input), filename=filename, attach=attach) + return InputFile(cast("IO", file_input), filename=filename, attach=attach) if tg_type and isinstance(file_input, tg_type): return file_input.file_id # type: ignore[attr-defined] return file_input diff --git a/telegram/ext/_application.py b/telegram/ext/_application.py index 423e08b4f2f..e856fa85321 100644 --- a/telegram/ext/_application.py +++ b/telegram/ext/_application.py @@ -355,7 +355,7 @@ def __init__( self.__create_task_tasks: set[asyncio.Task] = set() # Used for awaiting tasks upon exit self.__stop_running_marker = asyncio.Event() - async def __aenter__(self: _AppType) -> _AppType: # noqa: PYI019 + async def __aenter__(self: _AppType) -> _AppType: """|async_context_manager| :meth:`initializes ` the App. Returns: diff --git a/telegram/ext/_baseupdateprocessor.py b/telegram/ext/_baseupdateprocessor.py index ea9649d1f68..c08afec0b41 100644 --- a/telegram/ext/_baseupdateprocessor.py +++ b/telegram/ext/_baseupdateprocessor.py @@ -74,7 +74,7 @@ def __init__(self, max_concurrent_updates: int): raise ValueError("`max_concurrent_updates` must be a positive integer!") self._semaphore = TrackedBoundedSemaphore(self.max_concurrent_updates) - async def __aenter__(self: _BUPT) -> _BUPT: # noqa: PYI019 + async def __aenter__(self: _BUPT) -> _BUPT: """|async_context_manager| :meth:`initializes ` the Processor. Returns: diff --git a/telegram/ext/_callbackdatacache.py b/telegram/ext/_callbackdatacache.py index 1052bd5a2a5..a24befd719d 100644 --- a/telegram/ext/_callbackdatacache.py +++ b/telegram/ext/_callbackdatacache.py @@ -347,7 +347,7 @@ def __process_message(self, message: Message) -> Optional[str]: for row in message.reply_markup.inline_keyboard: for button in row: if button.callback_data: - button_data = cast(str, button.callback_data) + button_data = cast("str", button.callback_data) keyboard_id, callback_data = self.__get_keyboard_uuid_and_button_data( button_data ) diff --git a/telegram/ext/_dictpersistence.py b/telegram/ext/_dictpersistence.py index da9749dfc85..758a8dd5436 100644 --- a/telegram/ext/_dictpersistence.py +++ b/telegram/ext/_dictpersistence.py @@ -145,7 +145,7 @@ def __init__( self._callback_data = None else: self._callback_data = cast( - CDCData, + "CDCData", ([(one, float(two), three) for one, two, three in data[0]], data[1]), ) self._callback_data_json = callback_data_json diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index f74304b5b64..15b969fd85d 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -253,7 +253,7 @@ def __init__( return if not isinstance(arbitrary_callback_data, bool): - maxsize = cast(int, arbitrary_callback_data) + maxsize = cast("int", arbitrary_callback_data) else: maxsize = 1024 diff --git a/telegram/ext/_handlers/callbackqueryhandler.py b/telegram/ext/_handlers/callbackqueryhandler.py index b09f8249c35..27ddc5b2ec4 100644 --- a/telegram/ext/_handlers/callbackqueryhandler.py +++ b/telegram/ext/_handlers/callbackqueryhandler.py @@ -204,5 +204,5 @@ def collect_additional_context( :attr:`CallbackContext.matches` as list with one element. """ if self.pattern: - check_result = cast(Match, check_result) + check_result = cast("Match", check_result) context.matches = [check_result] diff --git a/telegram/ext/_handlers/choseninlineresulthandler.py b/telegram/ext/_handlers/choseninlineresulthandler.py index 3bc80ed144b..2faa0bc862c 100644 --- a/telegram/ext/_handlers/choseninlineresulthandler.py +++ b/telegram/ext/_handlers/choseninlineresulthandler.py @@ -118,5 +118,5 @@ def collect_additional_context( :attr:`telegram.ext.CallbackContext.matches`. """ if self.pattern: - check_result = cast(Match, check_result) + check_result = cast("Match", check_result) context.matches = [check_result] diff --git a/telegram/ext/_handlers/conversationhandler.py b/telegram/ext/_handlers/conversationhandler.py index 1bc5f0398de..dd824bf5601 100644 --- a/telegram/ext/_handlers/conversationhandler.py +++ b/telegram/ext/_handlers/conversationhandler.py @@ -599,7 +599,7 @@ async def _initialize_persistence( current_conversations = self._conversations self._conversations = cast( - TrackingDict[ConversationKey, object], + "TrackingDict[ConversationKey, object]", TrackingDict(), ) # In the conversation already processed updates @@ -924,7 +924,7 @@ async def _trigger_timeout(self, context: CCT) -> None: :obj:`True` is handled. """ job = cast("Job", context.job) - ctxt = cast(_ConversationTimeoutContext, job.data) + ctxt = cast("_ConversationTimeoutContext", job.data) _LOGGER.debug( "Conversation timeout was triggered for conversation %s!", ctxt.conversation_key diff --git a/telegram/ext/_handlers/inlinequeryhandler.py b/telegram/ext/_handlers/inlinequeryhandler.py index 31106ba33a6..0285d259c25 100644 --- a/telegram/ext/_handlers/inlinequeryhandler.py +++ b/telegram/ext/_handlers/inlinequeryhandler.py @@ -139,5 +139,5 @@ def collect_additional_context( :attr:`CallbackContext.matches` as list with one element. """ if self.pattern: - check_result = cast(Match, check_result) + check_result = cast("Match", check_result) context.matches = [check_result] diff --git a/telegram/ext/_picklepersistence.py b/telegram/ext/_picklepersistence.py index c2f4c01e383..1602eabed0e 100644 --- a/telegram/ext/_picklepersistence.py +++ b/telegram/ext/_picklepersistence.py @@ -237,7 +237,7 @@ def __init__( self.callback_data: Optional[CDCData] = None self.conversations: Optional[dict[str, dict[tuple[Union[int, str], ...], object]]] = None self.context_types: ContextTypes[Any, UD, CD, BD] = cast( - ContextTypes[Any, UD, CD, BD], context_types or ContextTypes() + "ContextTypes[Any, UD, CD, BD]", context_types or ContextTypes() ) def _load_singlefile(self) -> None: diff --git a/telegram/ext/_updater.py b/telegram/ext/_updater.py index a2a921672dd..95f7e225ed1 100644 --- a/telegram/ext/_updater.py +++ b/telegram/ext/_updater.py @@ -124,7 +124,7 @@ def __init__( self.__polling_task_stop_event: asyncio.Event = asyncio.Event() self.__polling_cleanup_cb: Optional[Callable[[], Coroutine[Any, Any, None]]] = None - async def __aenter__(self: _UpdaterType) -> _UpdaterType: # noqa: PYI019 + async def __aenter__(self: _UpdaterType) -> _UpdaterType: """ |async_context_manager| :meth:`initializes ` the Updater. diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 8757fe3e7d8..a1e8030f7b4 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1588,10 +1588,10 @@ class Language(MessageFilter): def __init__(self, lang: SCT[str]): if isinstance(lang, str): - lang = cast(str, lang) + lang = cast("str", lang) self.lang: Sequence[str] = [lang] else: - lang = cast(list[str], lang) + lang = cast("list[str]", lang) self.lang = lang super().__init__(name=f"filters.Language({self.lang})") diff --git a/tests/_files/test_inputmedia.py b/tests/_files/test_inputmedia.py index b362411cbd8..a077c309cc5 100644 --- a/tests/_files/test_inputmedia.py +++ b/tests/_files/test_inputmedia.py @@ -710,7 +710,6 @@ async def test_send_media_group_with_thumbs( self, offline_bot, chat_id, video_file, photo_file, monkeypatch ): async def make_assertion(method, url, request_data: RequestData, *args, **kwargs): - nonlocal input_video files = request_data.multipart_data video_check = files[input_video.media.attach_name] == input_video.media.field_tuple thumb_check = ( diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 18f7dd93e45..f7f43088681 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -621,7 +621,7 @@ async def check_defaults_handling( kwargs_need_default.remove("parse_mode") defaults_no_custom_defaults = Defaults() - kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters} + kwargs = dict.fromkeys(inspect.signature(Defaults).parameters, "custom_default") kwargs["tzinfo"] = zoneinfo.ZoneInfo("America/New_York") kwargs["link_preview_options"] = LinkPreviewOptions( url="custom_default", show_above_text="custom_default" diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index 8de4c8ed0d0..fd96aa99e1f 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -16,8 +16,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/]. -"""The integration of persistence into the application is tested in test_basepersistence. -""" +"""The integration of persistence into the application is tested in test_basepersistence.""" import asyncio import functools import inspect diff --git a/tests/ext/test_applicationbuilder.py b/tests/ext/test_applicationbuilder.py index 519b8b28194..15e85b6416e 100644 --- a/tests/ext/test_applicationbuilder.py +++ b/tests/ext/test_applicationbuilder.py @@ -426,7 +426,6 @@ def test_custom_socket_options(self, builder, monkeypatch, bot): httpx_request_init = HTTPXRequest.__init__ def init_transport(*args, **kwargs): - nonlocal httpx_request_kwargs # This is called once for request and once for get_updates_request, so we make # it a list httpx_request_kwargs.append(kwargs.copy()) diff --git a/tests/ext/test_conversationhandler.py b/tests/ext/test_conversationhandler.py index 7d8b7ddb946..e57c1faa373 100644 --- a/tests/ext/test_conversationhandler.py +++ b/tests/ext/test_conversationhandler.py @@ -1438,7 +1438,7 @@ async def test_conversation_handler_timeout_update_and_context(self, app, bot, u context = None async def start_callback(u, c): - nonlocal context, self + nonlocal context context = c return await self.start(u, c) @@ -1459,7 +1459,6 @@ async def start_callback(u, c): update = Update(update_id=0, message=message) async def timeout_callback(u, c): - nonlocal update, context assert u is update assert c is context From 7078059e80cd1052d98b09f7af866153e61f8305 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 15 May 2025 21:56:10 +0200 Subject: [PATCH 30/36] Full Support for Bot API 9.0 (#4756) Co-authored-by: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> --- README.rst | 4 +- .../4756.JT5nmUmGRG6qDEh5ScMn5f.toml | 51 + docs/source/inclusions/bot_methods.rst | 56 +- docs/source/telegram.acceptedgifttypes.rst | 6 + docs/source/telegram.at-tree.rst | 29 + docs/source/telegram.businessbotrights.rst | 6 + docs/source/telegram.giftinfo.rst | 7 + docs/source/telegram.inputprofilephoto.rst | 6 + .../telegram.inputprofilephotoanimated.rst | 6 + .../telegram.inputprofilephotostatic.rst | 6 + docs/source/telegram.inputstorycontent.rst | 6 + .../telegram.inputstorycontentphoto.rst | 6 + .../telegram.inputstorycontentvideo.rst | 6 + docs/source/telegram.locationaddress.rst | 6 + docs/source/telegram.ownedgift.rst | 6 + docs/source/telegram.ownedgiftregular.rst | 6 + docs/source/telegram.ownedgifts.rst | 6 + docs/source/telegram.ownedgiftunique.rst | 6 + .../telegram.paidmeessagepricechanged.rst | 6 + docs/source/telegram.payments-tree.rst | 1 + docs/source/telegram.staramount.rst | 6 + docs/source/telegram.storyarea.rst | 6 + docs/source/telegram.storyareaposition.rst | 6 + docs/source/telegram.storyareatype.rst | 6 + docs/source/telegram.storyareatypelink.rst | 6 + .../source/telegram.storyareatypelocation.rst | 6 + ...elegram.storyareatypesuggestedreaction.rst | 6 + .../telegram.storyareatypeuniquegift.rst | 6 + docs/source/telegram.storyareatypeweather.rst | 6 + docs/source/telegram.uniquegift.rst | 7 + docs/source/telegram.uniquegiftbackdrop.rst | 7 + .../telegram.uniquegiftbackdropcolors.rst | 7 + docs/source/telegram.uniquegiftinfo.rst | 7 + docs/source/telegram.uniquegiftmodel.rst | 7 + docs/source/telegram.uniquegiftsymbol.rst | 7 + telegram/__init__.py | 65 +- telegram/_bot.py | 1015 ++++++++++++++++- telegram/_business.py | 218 +++- telegram/_chat.py | 69 ++ telegram/_chatfullinfo.py | 69 +- telegram/_files/_inputstorycontent.py | 175 +++ telegram/_files/inputprofilephoto.py | 142 +++ telegram/_gifts.py | 210 ++++ telegram/_message.py | 91 ++ telegram/_ownedgift.py | 419 +++++++ telegram/_paidmessagepricechanged.py | 55 + telegram/_payment/stars/affiliateinfo.py | 12 +- telegram/_payment/stars/staramount.py | 68 ++ telegram/_payment/stars/startransactions.py | 8 +- telegram/_payment/stars/transactionpartner.py | 98 +- telegram/_storyarea.py | 438 +++++++ telegram/_uniquegift.py | 401 +++++++ telegram/_user.py | 40 + telegram/_utils/warnings_transition.py | 4 +- telegram/constants.py | 457 +++++++- telegram/ext/_extbot.py | 481 ++++++++ telegram/ext/filters.py | 41 + telegram/request/_requestparameter.py | 27 + tests/_files/test_inputprofilephoto.py | 153 +++ tests/_files/test_inputstorycontent.py | 159 +++ tests/_payment/stars/test_staramount.py | 72 ++ tests/_payment/stars/test_startransactions.py | 2 + .../_payment/stars/test_transactionpartner.py | 17 + tests/auxil/dummy_objects.py | 26 + tests/ext/test_filters.py | 15 + tests/request/test_requestparameter.py | 78 +- tests/test_bot.py | 76 +- ...t_business.py => test_business_classes.py} | 197 +++- tests/test_business_methods.py | 600 ++++++++++ tests/test_chat.py | 43 + tests/test_chatfullinfo.py | 41 + tests/test_constants.py | 1 + tests/test_enum_types.py | 1 + tests/test_gifts.py | 190 ++- tests/test_message.py | 77 ++ tests/test_official/exceptions.py | 33 + tests/test_ownedgift.py | 461 ++++++++ tests/test_paidmessagepricechanged.py | 70 ++ tests/test_storyarea.py | 508 +++++++++ tests/test_uniquegift.py | 459 ++++++++ tests/test_user.py | 30 + 81 files changed, 8154 insertions(+), 86 deletions(-) create mode 100644 changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml create mode 100644 docs/source/telegram.acceptedgifttypes.rst create mode 100644 docs/source/telegram.businessbotrights.rst create mode 100644 docs/source/telegram.giftinfo.rst create mode 100644 docs/source/telegram.inputprofilephoto.rst create mode 100644 docs/source/telegram.inputprofilephotoanimated.rst create mode 100644 docs/source/telegram.inputprofilephotostatic.rst create mode 100644 docs/source/telegram.inputstorycontent.rst create mode 100644 docs/source/telegram.inputstorycontentphoto.rst create mode 100644 docs/source/telegram.inputstorycontentvideo.rst create mode 100644 docs/source/telegram.locationaddress.rst create mode 100644 docs/source/telegram.ownedgift.rst create mode 100644 docs/source/telegram.ownedgiftregular.rst create mode 100644 docs/source/telegram.ownedgifts.rst create mode 100644 docs/source/telegram.ownedgiftunique.rst create mode 100644 docs/source/telegram.paidmeessagepricechanged.rst create mode 100644 docs/source/telegram.staramount.rst create mode 100644 docs/source/telegram.storyarea.rst create mode 100644 docs/source/telegram.storyareaposition.rst create mode 100644 docs/source/telegram.storyareatype.rst create mode 100644 docs/source/telegram.storyareatypelink.rst create mode 100644 docs/source/telegram.storyareatypelocation.rst create mode 100644 docs/source/telegram.storyareatypesuggestedreaction.rst create mode 100644 docs/source/telegram.storyareatypeuniquegift.rst create mode 100644 docs/source/telegram.storyareatypeweather.rst create mode 100644 docs/source/telegram.uniquegift.rst create mode 100644 docs/source/telegram.uniquegiftbackdrop.rst create mode 100644 docs/source/telegram.uniquegiftbackdropcolors.rst create mode 100644 docs/source/telegram.uniquegiftinfo.rst create mode 100644 docs/source/telegram.uniquegiftmodel.rst create mode 100644 docs/source/telegram.uniquegiftsymbol.rst create mode 100644 telegram/_files/_inputstorycontent.py create mode 100644 telegram/_files/inputprofilephoto.py create mode 100644 telegram/_ownedgift.py create mode 100644 telegram/_paidmessagepricechanged.py create mode 100644 telegram/_payment/stars/staramount.py create mode 100644 telegram/_storyarea.py create mode 100644 telegram/_uniquegift.py create mode 100644 tests/_files/test_inputprofilephoto.py create mode 100644 tests/_files/test_inputstorycontent.py create mode 100644 tests/_payment/stars/test_staramount.py rename tests/{test_business.py => test_business_classes.py} (63%) create mode 100644 tests/test_business_methods.py create mode 100644 tests/test_ownedgift.py create mode 100644 tests/test_paidmessagepricechanged.py create mode 100644 tests/test_storyarea.py create mode 100644 tests/test_uniquegift.py diff --git a/README.rst b/README.rst index d847fd3140c..633dc383ad7 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-8.3-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-9.0-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with Telegram API support ~~~~~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **8.3** are natively supported by this library. +All types and methods of the Telegram Bot API **9.0** 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 diff --git a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml new file mode 100644 index 00000000000..a7a6b76c9a7 --- /dev/null +++ b/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml @@ -0,0 +1,51 @@ +features = "Full Support for Bot API 9.0" +deprecations = """This release comes with several deprecations, in line with our :ref:`stability policy `. +This includes the following: + +- Deprecated ``telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT`` and ``telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT``. These members will be replaced by ``telegram.constants.NanostarLimit.MIN_AMOUNT`` and ``telegram.constants.NanostarLimit.MAX_AMOUNT``. +- Deprecated the class ``telegram.constants.StarTransactions``. Its only member ``telegram.constants.StarTransactions.NANOSTAR_VALUE`` will be replaced by ``telegram.constants.Nanostar.VALUE``. +- Bot API 9.0 deprecated ``BusinessConnection.can_reply`` in favor of ``BusinessConnection.rights`` +- Bot API 9.0 deprecated ``ChatFullInfo.can_send_gift`` in favor of ``ChatFullInfo.accepted_gift_types``. +- Bot API 9.0 introduced these new required fields to existing classes: + - ``TransactionPartnerUser.transaction_type`` + - ``ChatFullInfo.accepted_gift_types`` + + Passing these values as positional arguments is deprecated. We encourage you to use keyword arguments instead, as the the signature will be updated in a future release. + +These deprecations are backward compatible, but we strongly recommend to update your code to use the new members. +""" +[[pull_requests]] +uid = "4756" +author_uid = "Bibo-Joshi" +closes_threads = ["4754"] +[[pull_requests]] +uid = "4757" +author_uid = "Bibo-Joshi" +closes_threads = [] +[[pull_requests]] +uid = "4759" +author_uid = "Bibo-Joshi" +closes_threads = [] +[[pull_requests]] +uid = "4763" +author_uid = "aelkheir" +closes_threads = [] +[[pull_requests]] +uid = "4766" +author_uid = "Bibo-Joshi" +[[pull_requests]] +uid = "4769" +author_uid = "aelkheir" +closes_threads = [] +[[pull_requests]] +uid = "4773" +author_uid = "aelkheir" +closes_threads = [] +[[pull_requests]] +uid = "4781" +author_uid = "aelkheir" +closes_threads = [] +[[pull_requests]] +uid = "4782" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 240c258f68f..d1ff3c3ac13 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -161,8 +161,6 @@ - Used for unpinning a message * - :meth:`~telegram.Bot.unpin_all_chat_messages` - Used for unpinning all pinned chat messages - * - :meth:`~telegram.Bot.get_business_connection` - - Used for getting information about the business account. * - :meth:`~telegram.Bot.get_user_profile_photos` - Used for obtaining user's profile pictures * - :meth:`~telegram.Bot.get_chat` @@ -396,6 +394,60 @@ - Used for obtaining the bot's Telegram Stars transactions * - :meth:`~telegram.Bot.refund_star_payment` - Used for refunding a payment in Telegram Stars + * - :meth:`~telegram.Bot.gift_premium_subscription` + - Used for gifting Telegram Premium to another user. + +.. raw:: html + + +
+ +.. raw:: html + +
+ Business Related Methods + +.. list-table:: + :align: left + :widths: 1 4 + + * - :meth:`~telegram.Bot.get_business_connection` + - Used for getting information about the business account. + * - :meth:`~telegram.Bot.get_business_account_gifts` + - Used for getting gifts owned by the business account. + * - :meth:`~telegram.Bot.get_business_account_star_balance` + - Used for getting the amount of Stars owned by the business account. + * - :meth:`~telegram.Bot.read_business_message` + - Used for marking a message as read. + * - :meth:`~telegram.Bot.delete_story` + - Used for deleting business stories posted by the bot. + * - :meth:`~telegram.Bot.delete_business_messages` + - Used for deleting business messages. + * - :meth:`~telegram.Bot.remove_business_account_profile_photo` + - Used for removing the business accounts profile photo + * - :meth:`~telegram.Bot.set_business_account_name` + - Used for setting the business account name. + * - :meth:`~telegram.Bot.set_business_account_username` + - Used for setting the business account username. + * - :meth:`~telegram.Bot.set_business_account_bio` + - Used for setting the business account bio. + * - :meth:`~telegram.Bot.set_business_account_gift_settings` + - Used for setting the business account gift settings. + * - :meth:`~telegram.Bot.set_business_account_profile_photo` + - Used for setting the business accounts profile photo + * - :meth:`~telegram.Bot.post_story` + - Used for posting a story on behalf of business account. + * - :meth:`~telegram.Bot.edit_story` + - Used for editing business stories posted by the bot. + * - :meth:`~telegram.Bot.convert_gift_to_stars` + - Used for converting owned reqular gifts to stars. + * - :meth:`~telegram.Bot.upgrade_gift` + - Used for upgrading owned regular gifts to unique ones. + * - :meth:`~telegram.Bot.transfer_gift` + - Used for transferring owned unique gifts to another user. + * - :meth:`~telegram.Bot.transfer_business_account_stars` + - Used for transfering Stars from the business account balance to the bot's balance. + .. raw:: html diff --git a/docs/source/telegram.acceptedgifttypes.rst b/docs/source/telegram.acceptedgifttypes.rst new file mode 100644 index 00000000000..2926dffd338 --- /dev/null +++ b/docs/source/telegram.acceptedgifttypes.rst @@ -0,0 +1,6 @@ +AcceptedGiftTypes +================= + +.. autoclass:: telegram.AcceptedGiftTypes + :members: + :show-inheritance: diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 22abbfb3867..63da86e76de 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -4,6 +4,7 @@ Available Types .. toctree:: :titlesonly: + telegram.acceptedgifttypes telegram.animation telegram.audio telegram.birthdate @@ -19,6 +20,7 @@ Available Types telegram.botdescription telegram.botname telegram.botshortdescription + telegram.businessbotrights telegram.businessconnection telegram.businessintro telegram.businesslocation @@ -75,6 +77,7 @@ Available Types telegram.forumtopicreopened telegram.generalforumtopichidden telegram.generalforumtopicunhidden + telegram.giftinfo telegram.giveaway telegram.giveawaycompleted telegram.giveawaycreated @@ -92,13 +95,20 @@ Available Types telegram.inputpaidmedia telegram.inputpaidmediaphoto telegram.inputpaidmediavideo + telegram.inputprofilephoto + telegram.inputprofilephotoanimated + telegram.inputprofilephotostatic telegram.inputpolloption + telegram.inputstorycontent + telegram.inputstorycontentphoto + telegram.inputstorycontentvideo telegram.keyboardbutton telegram.keyboardbuttonpolltype telegram.keyboardbuttonrequestchat telegram.keyboardbuttonrequestusers telegram.linkpreviewoptions telegram.location + telegram.locationaddress telegram.loginurl telegram.maybeinaccessiblemessage telegram.menubutton @@ -116,12 +126,17 @@ Available Types telegram.messageoriginuser telegram.messagereactioncountupdated telegram.messagereactionupdated + telegram.ownedgift + telegram.ownedgiftregular + telegram.ownedgifts + telegram.ownedgiftunique telegram.paidmedia telegram.paidmediainfo telegram.paidmediaphoto telegram.paidmediapreview telegram.paidmediapurchased telegram.paidmediavideo + telegram.paidmessagepricechanged telegram.photosize telegram.poll telegram.pollanswer @@ -138,9 +153,23 @@ Available Types telegram.sentwebappmessage telegram.shareduser telegram.story + telegram.storyarea + telegram.storyareaposition + telegram.storyareatype + telegram.storyareatypelink + telegram.storyareatypelocation + telegram.storyareatypesuggestedreaction + telegram.storyareatypeuniquegift + telegram.storyareatypeweather telegram.switchinlinequerychosenchat telegram.telegramobject telegram.textquote + telegram.uniquegift + telegram.uniquegiftbackdrop + telegram.uniquegiftbackdropcolors + telegram.uniquegiftinfo + telegram.uniquegiftmodel + telegram.uniquegiftsymbol telegram.update telegram.user telegram.userchatboosts diff --git a/docs/source/telegram.businessbotrights.rst b/docs/source/telegram.businessbotrights.rst new file mode 100644 index 00000000000..d6bdab1a809 --- /dev/null +++ b/docs/source/telegram.businessbotrights.rst @@ -0,0 +1,6 @@ +BusinessBotRights +================= + +.. autoclass:: telegram.BusinessBotRights + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.giftinfo.rst b/docs/source/telegram.giftinfo.rst new file mode 100644 index 00000000000..ff5ab6ad352 --- /dev/null +++ b/docs/source/telegram.giftinfo.rst @@ -0,0 +1,7 @@ +GiftInfo +======== + +.. autoclass:: telegram.GiftInfo + :members: + :show-inheritance: + diff --git a/docs/source/telegram.inputprofilephoto.rst b/docs/source/telegram.inputprofilephoto.rst new file mode 100644 index 00000000000..723f3c92389 --- /dev/null +++ b/docs/source/telegram.inputprofilephoto.rst @@ -0,0 +1,6 @@ +InputProfilePhoto +================= + +.. autoclass:: telegram.InputProfilePhoto + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inputprofilephotoanimated.rst b/docs/source/telegram.inputprofilephotoanimated.rst new file mode 100644 index 00000000000..c192d0d8e58 --- /dev/null +++ b/docs/source/telegram.inputprofilephotoanimated.rst @@ -0,0 +1,6 @@ +InputProfilePhotoAnimated +========================= + +.. autoclass:: telegram.InputProfilePhotoAnimated + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inputprofilephotostatic.rst b/docs/source/telegram.inputprofilephotostatic.rst new file mode 100644 index 00000000000..49b498c13ba --- /dev/null +++ b/docs/source/telegram.inputprofilephotostatic.rst @@ -0,0 +1,6 @@ +InputProfilePhotoStatic +======================= + +.. autoclass:: telegram.InputProfilePhotoStatic + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inputstorycontent.rst b/docs/source/telegram.inputstorycontent.rst new file mode 100644 index 00000000000..3406e8cf253 --- /dev/null +++ b/docs/source/telegram.inputstorycontent.rst @@ -0,0 +1,6 @@ +InputStoryContent +================= + +.. autoclass:: telegram.InputStoryContent + :members: + :show-inheritance: diff --git a/docs/source/telegram.inputstorycontentphoto.rst b/docs/source/telegram.inputstorycontentphoto.rst new file mode 100644 index 00000000000..1adacb2322c --- /dev/null +++ b/docs/source/telegram.inputstorycontentphoto.rst @@ -0,0 +1,6 @@ +InputStoryContentPhoto +====================== + +.. autoclass:: telegram.InputStoryContentPhoto + :members: + :show-inheritance: diff --git a/docs/source/telegram.inputstorycontentvideo.rst b/docs/source/telegram.inputstorycontentvideo.rst new file mode 100644 index 00000000000..27550468e3b --- /dev/null +++ b/docs/source/telegram.inputstorycontentvideo.rst @@ -0,0 +1,6 @@ +InputStoryContentVideo +====================== + +.. autoclass:: telegram.InputStoryContentVideo + :members: + :show-inheritance: diff --git a/docs/source/telegram.locationaddress.rst b/docs/source/telegram.locationaddress.rst new file mode 100644 index 00000000000..f6e3874de9d --- /dev/null +++ b/docs/source/telegram.locationaddress.rst @@ -0,0 +1,6 @@ +LocationAddress +=============== + +.. autoclass:: telegram.LocationAddress + :members: + :show-inheritance: diff --git a/docs/source/telegram.ownedgift.rst b/docs/source/telegram.ownedgift.rst new file mode 100644 index 00000000000..0c726895c07 --- /dev/null +++ b/docs/source/telegram.ownedgift.rst @@ -0,0 +1,6 @@ +OwnedGift +========= + +.. autoclass:: telegram.OwnedGift + :members: + :show-inheritance: diff --git a/docs/source/telegram.ownedgiftregular.rst b/docs/source/telegram.ownedgiftregular.rst new file mode 100644 index 00000000000..eb4f3641ed6 --- /dev/null +++ b/docs/source/telegram.ownedgiftregular.rst @@ -0,0 +1,6 @@ +OwnedGiftRegular +================ + +.. autoclass:: telegram.OwnedGiftRegular + :members: + :show-inheritance: diff --git a/docs/source/telegram.ownedgifts.rst b/docs/source/telegram.ownedgifts.rst new file mode 100644 index 00000000000..71a1c51b86f --- /dev/null +++ b/docs/source/telegram.ownedgifts.rst @@ -0,0 +1,6 @@ +OwnedGifts +========== + +.. autoclass:: telegram.OwnedGifts + :members: + :show-inheritance: diff --git a/docs/source/telegram.ownedgiftunique.rst b/docs/source/telegram.ownedgiftunique.rst new file mode 100644 index 00000000000..cc114fecc49 --- /dev/null +++ b/docs/source/telegram.ownedgiftunique.rst @@ -0,0 +1,6 @@ +OwnedGiftUnique +=============== + +.. autoclass:: telegram.OwnedGiftUnique + :members: + :show-inheritance: diff --git a/docs/source/telegram.paidmeessagepricechanged.rst b/docs/source/telegram.paidmeessagepricechanged.rst new file mode 100644 index 00000000000..3d0e739c456 --- /dev/null +++ b/docs/source/telegram.paidmeessagepricechanged.rst @@ -0,0 +1,6 @@ +PaidMessagePriceChanged +======================= + +.. autoclass:: telegram.PaidMessagePriceChanged + :members: + :show-inheritance: diff --git a/docs/source/telegram.payments-tree.rst b/docs/source/telegram.payments-tree.rst index 3e6f42bdc97..94e4fec3c99 100644 --- a/docs/source/telegram.payments-tree.rst +++ b/docs/source/telegram.payments-tree.rst @@ -22,6 +22,7 @@ Your bot can accept payments from Telegram users. Please see the `introduction t telegram.shippingaddress telegram.shippingoption telegram.shippingquery + telegram.staramount telegram.startransaction telegram.startransactions telegram.successfulpayment diff --git a/docs/source/telegram.staramount.rst b/docs/source/telegram.staramount.rst new file mode 100644 index 00000000000..9d5a6e24572 --- /dev/null +++ b/docs/source/telegram.staramount.rst @@ -0,0 +1,6 @@ +StarAmount +========== + +.. autoclass:: telegram.StarAmount + :members: + :show-inheritance: diff --git a/docs/source/telegram.storyarea.rst b/docs/source/telegram.storyarea.rst new file mode 100644 index 00000000000..88c028535d6 --- /dev/null +++ b/docs/source/telegram.storyarea.rst @@ -0,0 +1,6 @@ +StoryArea +========= + +.. autoclass:: telegram.StoryArea + :members: + :show-inheritance: diff --git a/docs/source/telegram.storyareaposition.rst b/docs/source/telegram.storyareaposition.rst new file mode 100644 index 00000000000..d14aa66cb2a --- /dev/null +++ b/docs/source/telegram.storyareaposition.rst @@ -0,0 +1,6 @@ +StoryAreaPosition +================= + +.. autoclass:: telegram.StoryAreaPosition + :members: + :show-inheritance: diff --git a/docs/source/telegram.storyareatype.rst b/docs/source/telegram.storyareatype.rst new file mode 100644 index 00000000000..aa4ad3312aa --- /dev/null +++ b/docs/source/telegram.storyareatype.rst @@ -0,0 +1,6 @@ +StoryAreaType +============= + +.. autoclass:: telegram.StoryAreaType + :members: + :show-inheritance: diff --git a/docs/source/telegram.storyareatypelink.rst b/docs/source/telegram.storyareatypelink.rst new file mode 100644 index 00000000000..493eeef5da2 --- /dev/null +++ b/docs/source/telegram.storyareatypelink.rst @@ -0,0 +1,6 @@ +StoryAreaTypeLink +================= + +.. autoclass:: telegram.StoryAreaTypeLink + :members: + :show-inheritance: diff --git a/docs/source/telegram.storyareatypelocation.rst b/docs/source/telegram.storyareatypelocation.rst new file mode 100644 index 00000000000..8f09ee9bf40 --- /dev/null +++ b/docs/source/telegram.storyareatypelocation.rst @@ -0,0 +1,6 @@ +StoryAreaTypeLocation +===================== + +.. autoclass:: telegram.StoryAreaTypeLocation + :members: + :show-inheritance: diff --git a/docs/source/telegram.storyareatypesuggestedreaction.rst b/docs/source/telegram.storyareatypesuggestedreaction.rst new file mode 100644 index 00000000000..e099e992d61 --- /dev/null +++ b/docs/source/telegram.storyareatypesuggestedreaction.rst @@ -0,0 +1,6 @@ +StoryAreaTypeSuggestedReaction +============================== + +.. autoclass:: telegram.StoryAreaTypeSuggestedReaction + :members: + :show-inheritance: diff --git a/docs/source/telegram.storyareatypeuniquegift.rst b/docs/source/telegram.storyareatypeuniquegift.rst new file mode 100644 index 00000000000..c6e7fd9a119 --- /dev/null +++ b/docs/source/telegram.storyareatypeuniquegift.rst @@ -0,0 +1,6 @@ +StoryAreaTypeUniqueGift +======================= + +.. autoclass:: telegram.StoryAreaTypeUniqueGift + :members: + :show-inheritance: diff --git a/docs/source/telegram.storyareatypeweather.rst b/docs/source/telegram.storyareatypeweather.rst new file mode 100644 index 00000000000..a704e7eecfd --- /dev/null +++ b/docs/source/telegram.storyareatypeweather.rst @@ -0,0 +1,6 @@ +StoryAreaTypeWeather +==================== + +.. autoclass:: telegram.StoryAreaTypeWeather + :members: + :show-inheritance: diff --git a/docs/source/telegram.uniquegift.rst b/docs/source/telegram.uniquegift.rst new file mode 100644 index 00000000000..0d9d1a12d32 --- /dev/null +++ b/docs/source/telegram.uniquegift.rst @@ -0,0 +1,7 @@ +UniqueGift +========== + +.. autoclass:: telegram.UniqueGift + :members: + :show-inheritance: + diff --git a/docs/source/telegram.uniquegiftbackdrop.rst b/docs/source/telegram.uniquegiftbackdrop.rst new file mode 100644 index 00000000000..52264731b22 --- /dev/null +++ b/docs/source/telegram.uniquegiftbackdrop.rst @@ -0,0 +1,7 @@ +UniqueGiftBackdrop +================== + +.. autoclass:: telegram.UniqueGiftBackdrop + :members: + :show-inheritance: + diff --git a/docs/source/telegram.uniquegiftbackdropcolors.rst b/docs/source/telegram.uniquegiftbackdropcolors.rst new file mode 100644 index 00000000000..40fbf609a37 --- /dev/null +++ b/docs/source/telegram.uniquegiftbackdropcolors.rst @@ -0,0 +1,7 @@ +UniqueGiftBackdropColors +======================== + +.. autoclass:: telegram.UniqueGiftBackdropColors + :members: + :show-inheritance: + diff --git a/docs/source/telegram.uniquegiftinfo.rst b/docs/source/telegram.uniquegiftinfo.rst new file mode 100644 index 00000000000..5d8ef6402cf --- /dev/null +++ b/docs/source/telegram.uniquegiftinfo.rst @@ -0,0 +1,7 @@ +UniqueGiftInfo +============== + +.. autoclass:: telegram.UniqueGiftInfo + :members: + :show-inheritance: + diff --git a/docs/source/telegram.uniquegiftmodel.rst b/docs/source/telegram.uniquegiftmodel.rst new file mode 100644 index 00000000000..a0a95a04307 --- /dev/null +++ b/docs/source/telegram.uniquegiftmodel.rst @@ -0,0 +1,7 @@ +UniqueGiftModel +=============== + +.. autoclass:: telegram.UniqueGiftModel + :members: + :show-inheritance: + diff --git a/docs/source/telegram.uniquegiftsymbol.rst b/docs/source/telegram.uniquegiftsymbol.rst new file mode 100644 index 00000000000..8246da5cf17 --- /dev/null +++ b/docs/source/telegram.uniquegiftsymbol.rst @@ -0,0 +1,7 @@ +UniqueGiftSymbol +================ + +.. autoclass:: telegram.UniqueGiftSymbol + :members: + :show-inheritance: + diff --git a/telegram/__init__.py b/telegram/__init__.py index fe2fce247ea..0f20f0ba605 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -20,6 +20,7 @@ __author__ = "devs@python-telegram-bot.org" __all__ = ( + "AcceptedGiftTypes", "AffiliateInfo", "Animation", "Audio", @@ -46,6 +47,7 @@ "BotDescription", "BotName", "BotShortDescription", + "BusinessBotRights", "BusinessConnection", "BusinessIntro", "BusinessLocation", @@ -103,6 +105,7 @@ "GeneralForumTopicHidden", "GeneralForumTopicUnhidden", "Gift", + "GiftInfo", "Gifts", "Giveaway", "GiveawayCompleted", @@ -150,7 +153,13 @@ "InputPaidMediaPhoto", "InputPaidMediaVideo", "InputPollOption", + "InputProfilePhoto", + "InputProfilePhotoAnimated", + "InputProfilePhotoStatic", "InputSticker", + "InputStoryContent", + "InputStoryContentPhoto", + "InputStoryContentVideo", "InputTextMessageContent", "InputVenueMessageContent", "Invoice", @@ -161,6 +170,7 @@ "LabeledPrice", "LinkPreviewOptions", "Location", + "LocationAddress", "LoginUrl", "MaskPosition", "MaybeInaccessibleMessage", @@ -180,12 +190,17 @@ "MessageReactionCountUpdated", "MessageReactionUpdated", "OrderInfo", + "OwnedGift", + "OwnedGiftRegular", + "OwnedGiftUnique", + "OwnedGifts", "PaidMedia", "PaidMediaInfo", "PaidMediaPhoto", "PaidMediaPreview", "PaidMediaPurchased", "PaidMediaVideo", + "PaidMessagePriceChanged", "PassportData", "PassportElementError", "PassportElementErrorDataField", @@ -227,11 +242,20 @@ "ShippingAddress", "ShippingOption", "ShippingQuery", + "StarAmount", "StarTransaction", "StarTransactions", "Sticker", "StickerSet", "Story", + "StoryArea", + "StoryAreaPosition", + "StoryAreaType", + "StoryAreaTypeLink", + "StoryAreaTypeLocation", + "StoryAreaTypeSuggestedReaction", + "StoryAreaTypeUniqueGift", + "StoryAreaTypeWeather", "SuccessfulPayment", "SwitchInlineQueryChosenChat", "TelegramObject", @@ -244,6 +268,12 @@ "TransactionPartnerTelegramAds", "TransactionPartnerTelegramApi", "TransactionPartnerUser", + "UniqueGift", + "UniqueGiftBackdrop", + "UniqueGiftBackdropColors", + "UniqueGiftInfo", + "UniqueGiftModel", + "UniqueGiftSymbol", "Update", "User", "UserChatBoosts", @@ -272,6 +302,7 @@ "warnings", ) +from telegram._payment.stars.staramount import StarAmount from telegram._payment.stars.startransactions import StarTransaction, StarTransactions from telegram._payment.stars.transactionpartner import ( TransactionPartner, @@ -301,6 +332,7 @@ from ._botdescription import BotDescription, BotShortDescription from ._botname import BotName from ._business import ( + BusinessBotRights, BusinessConnection, BusinessIntro, BusinessLocation, @@ -352,6 +384,11 @@ from ._choseninlineresult import ChosenInlineResult from ._copytextbutton import CopyTextButton from ._dice import Dice +from ._files._inputstorycontent import ( + InputStoryContent, + InputStoryContentPhoto, + InputStoryContentVideo, +) from ._files.animation import Animation from ._files.audio import Audio from ._files.chatphoto import ChatPhoto @@ -370,6 +407,11 @@ InputPaidMediaPhoto, InputPaidMediaVideo, ) +from ._files.inputprofilephoto import ( + InputProfilePhoto, + InputProfilePhotoAnimated, + InputProfilePhotoStatic, +) from ._files.inputsticker import InputSticker from ._files.location import Location from ._files.photosize import PhotoSize @@ -391,7 +433,7 @@ from ._games.callbackgame import CallbackGame from ._games.game import Game from ._games.gamehighscore import GameHighScore -from ._gifts import Gift, Gifts +from ._gifts import AcceptedGiftTypes, Gift, GiftInfo, Gifts from ._giveaway import Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners from ._inline.inlinekeyboardbutton import InlineKeyboardButton from ._inline.inlinekeyboardmarkup import InlineKeyboardMarkup @@ -443,6 +485,7 @@ MessageOriginUser, ) from ._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated +from ._ownedgift import OwnedGift, OwnedGiftRegular, OwnedGifts, OwnedGiftUnique from ._paidmedia import ( PaidMedia, PaidMediaInfo, @@ -451,6 +494,7 @@ PaidMediaPurchased, PaidMediaVideo, ) +from ._paidmessagepricechanged import PaidMessagePriceChanged from ._passport.credentials import ( Credentials, DataCredentials, @@ -506,8 +550,27 @@ from ._sentwebappmessage import SentWebAppMessage from ._shared import ChatShared, SharedUser, UsersShared from ._story import Story +from ._storyarea import ( + LocationAddress, + StoryArea, + StoryAreaPosition, + StoryAreaType, + StoryAreaTypeLink, + StoryAreaTypeLocation, + StoryAreaTypeSuggestedReaction, + StoryAreaTypeUniqueGift, + StoryAreaTypeWeather, +) from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject +from ._uniquegift import ( + UniqueGift, + UniqueGiftBackdrop, + UniqueGiftBackdropColors, + UniqueGiftInfo, + UniqueGiftModel, + UniqueGiftSymbol, +) from ._update import Update from ._user import User from ._userprofilephotos import UserProfilePhotos diff --git a/telegram/_bot.py b/telegram/_bot.py index 49847efd3d4..43c350f1a79 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -75,17 +75,20 @@ from telegram._files.voice import Voice from telegram._forumtopic import ForumTopic from telegram._games.gamehighscore import GameHighScore -from telegram._gifts import Gift, Gifts +from telegram._gifts import AcceptedGiftTypes, Gift, Gifts from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._inline.preparedinlinemessage import PreparedInlineMessage from telegram._menubutton import MenuButton from telegram._message import Message from telegram._messageid import MessageId +from telegram._ownedgift import OwnedGifts +from telegram._payment.stars.staramount import StarAmount from telegram._payment.stars.startransactions 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._story import Story from telegram._telegramobject import TelegramObject from telegram._update import Update from telegram._user import User @@ -123,12 +126,15 @@ InputMediaDocument, InputMediaPhoto, InputMediaVideo, + InputProfilePhoto, InputSticker, + InputStoryContent, LabeledPrice, LinkPreviewOptions, MessageEntity, PassportElementError, ShippingOption, + StoryArea, ) BT = TypeVar("BT", bound="Bot") @@ -9362,6 +9368,83 @@ async def set_message_reaction( api_kwargs=api_kwargs, ) + async def gift_premium_subscription( + self, + user_id: int, + month_count: int, + star_count: int, + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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, + ) -> bool: + """ + Gifts a Telegram Premium subscription to the given user. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): Unique identifier of the target user who will receive a Telegram + Premium subscription. + month_count (:obj:`int`): Number of months the Telegram Premium subscription will be + active for the user; must be one of + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_THREE`, + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_SIX`, + or :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_TWELVE`. + star_count (:obj:`int`): Number of Telegram Stars to pay for the Telegram Premium + subscription; must be + :tg-const:`telegram.constants.PremiumSubscription.STARS_THREE_MONTHS` + for :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_THREE` months, + :tg-const:`telegram.constants.PremiumSubscription.STARS_SIX_MONTHS` + for :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_SIX` months, + and :tg-const:`telegram.constants.PremiumSubscription.STARS_TWELVE_MONTHS` + for :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_TWELVE` months. + text (:obj:`str`, optional): Text that will be shown along with the service message + about the subscription; + 0-:tg-const:`telegram.constants.PremiumSubscription.MAX_TEXT_LENGTH` characters. + text_parse_mode (:obj:`str`, optional): Mode for parsing entities. + See :class:`telegram.constants.ParseMode` and + `formatting options `__ for + more details. Entities other than :attr:`~MessageEntity.BOLD`, + :attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`, + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and + :attr:`~MessageEntity.CUSTOM_EMOJI` are ignored. + text_entities (Sequence[:class:`telegram.MessageEntity`], optional): A list of special + entities that appear in the gift text. It can be specified instead of + :paramref:`text_parse_mode`. Entities other than :attr:`~MessageEntity.BOLD`, + :attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`, + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and + :attr:`~MessageEntity.CUSTOM_EMOJI` are ignored. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + "month_count": month_count, + "star_count": star_count, + "text": text, + "text_entities": text_entities, + "text_parse_mode": text_parse_mode, + } + return await self._post( + "giftPremiumSubscription", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def get_business_connection( self, business_connection_id: str, @@ -9401,6 +9484,900 @@ async def get_business_connection( bot=self, ) + async def get_business_account_gifts( + self, + business_connection_id: str, + exclude_unsaved: Optional[bool] = None, + exclude_saved: Optional[bool] = None, + exclude_unlimited: Optional[bool] = None, + exclude_limited: Optional[bool] = None, + exclude_unique: Optional[bool] = None, + sort_by_price: Optional[bool] = None, + offset: Optional[str] = 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, + ) -> OwnedGifts: + """ + Returns the gifts received and owned by a managed business account. Requires the + :attr:`~telegram.BusinessBotRights.can_view_gifts_and_stars` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + exclude_unsaved (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that aren't + saved to the account's profile page. + exclude_saved (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that are saved + to the account's profile page. + exclude_unlimited (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that can + be purchased an unlimited number of times. + exclude_limited (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that can be + purchased a limited number of times. + exclude_unique (:obj:`bool`, optional): Pass :obj:`True` to exclude unique gifts. + sort_by_price (:obj:`bool`, optional): Pass :obj:`True` to sort results by gift price + instead of send date. Sorting is applied before pagination. + offset (:obj:`str`, optional): Offset of the first entry to return as received from + the previous request; use empty string to get the first chunk of results. + limit (:obj:`int`, optional): The maximum number of gifts to be returned; + :tg-const:`telegram.constants.BusinessLimit.MIN_GIFT_RESULTS`-\ + :tg-const:`telegram.constants.BusinessLimit.MAX_GIFT_RESULTS`. + Defaults to :tg-const:`telegram.constants.BusinessLimit.MAX_GIFT_RESULTS`. + + Returns: + :class:`telegram.OwnedGifts` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "exclude_unsaved": exclude_unsaved, + "exclude_saved": exclude_saved, + "exclude_unlimited": exclude_unlimited, + "exclude_limited": exclude_limited, + "exclude_unique": exclude_unique, + "sort_by_price": sort_by_price, + "offset": offset, + "limit": limit, + } + + return OwnedGifts.de_json( + await self._post( + "getBusinessAccountGifts", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + ) + + async def get_business_account_star_balance( + self, + business_connection_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> StarAmount: + """ + Returns the amount of Telegram Stars owned by a managed business account. Requires the + :attr:`~telegram.BusinessBotRights.can_view_gifts_and_stars` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + + Returns: + :class:`telegram.StarAmount` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"business_connection_id": business_connection_id} + return StarAmount.de_json( + await self._post( + "getBusinessAccountStarBalance", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + + async def read_business_message( + self, + business_connection_id: str, + chat_id: int, + message_id: int, + *, + 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, + ) -> bool: + """ + Marks incoming message as read on behalf of a business account. + Requires the :attr:`~telegram.BusinessBotRights.can_read_messages` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection on + behalf of which to read the message. + chat_id (:obj:`int`): Unique identifier of the chat in which the message was received. + The chat must have been active in the last + :tg-const:`~telegram.constants.BusinessLimit.\ +CHAT_ACTIVITY_TIMEOUT` seconds. + message_id (:obj:`int`): Unique identifier of the message to mark as read. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "chat_id": chat_id, + "message_id": message_id, + } + return await self._post( + "readBusinessMessage", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_business_messages( + self, + business_connection_id: str, + message_ids: Sequence[int], + *, + 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, + ) -> bool: + """ + Delete messages on behalf of a business account. Requires the + :attr:`~telegram.BusinessBotRights.can_delete_sent_messages` business bot right to + delete messages sent by the bot itself, or the + :attr:`~telegram.BusinessBotRights.can_delete_all_messages` business bot right to delete + any message. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`int` | :obj:`str`): Unique identifier of the business + connection on behalf of which to delete the messages + message_ids (Sequence[:obj:`int`]): A list of + :tg-const:`telegram.constants.BulkRequestLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.BulkRequestLimit.MAX_LIMIT` identifiers of messages + to delete. See :meth:`delete_message` for limitations on which messages can be + deleted. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "message_ids": message_ids, + } + return await self._post( + "deleteBusinessMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def post_story( + self, + business_connection_id: str, + content: "InputStoryContent", + active_period: TimePeriod, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + areas: Optional[Sequence["StoryArea"]] = None, + post_to_chat_page: Optional[bool] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> Story: + """ + Posts a story on behalf of a managed business account. Requires the + :attr:`~telegram.BusinessBotRights.can_manage_stories` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + content (:class:`telegram.InputStoryContent`): Content of the story. + active_period (:obj:`int` | :class:`datetime.timedelta`, optional): Period after which + the story is moved to the archive, in seconds; must be one of + :tg-const:`~telegram.constants.StoryLimit.ACTIVITY_SIX_HOURS`, + :tg-const:`~telegram.constants.StoryLimit.ACTIVITY_TWELVE_HOURS`, + :tg-const:`~telegram.constants.StoryLimit.ACTIVITY_ONE_DAY`, + or :tg-const:`~telegram.constants.StoryLimit.ACTIVITY_TWO_DAYS`. + caption (:obj:`str`, optional): Caption of the story, + 0-:tg-const:`~telegram.constants.StoryLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): Mode for parsing entities in the story caption. + See the constants in :class:`telegram.constants.ParseMode` for the + available modes. + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + areas (Sequence[:class:`telegram.StoryArea`], optional): Sequence of clickable areas to + be shown on the story. + + Note: + Each type of clickable area in :paramref:`areas` has its own maximum limit: + + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_LOCATION_AREAS` + of :class:`telegram.StoryAreaTypeLocation`. + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.\ +MAX_SUGGESTED_REACTION_AREAS` of :class:`telegram.StoryAreaTypeSuggestedReaction`. + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_LINK_AREAS` + of :class:`telegram.StoryAreaTypeLink`. + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_WEATHER_AREAS` + of :class:`telegram.StoryAreaTypeWeather`. + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.\ +MAX_UNIQUE_GIFT_AREAS` of :class:`telegram.StoryAreaTypeUniqueGift`. + post_to_chat_page (:class:`telegram.InputStoryContent`, optional): Pass :obj:`True` to + keep the story accessible after it expires. + protect_content (:obj:`bool`, optional): Pass :obj:`True` if the content of the story + must be protected from forwarding and screenshotting + + Returns: + :class:`Story` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "content": content, + "active_period": active_period, + "caption": caption, + "parse_mode": parse_mode, + "caption_entities": caption_entities, + "areas": areas, + "post_to_chat_page": post_to_chat_page, + "protect_content": protect_content, + } + return Story.de_json( + await self._post( + "postStory", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + ) + + async def edit_story( + self, + business_connection_id: str, + story_id: int, + content: "InputStoryContent", + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + areas: Optional[Sequence["StoryArea"]] = 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, + ) -> Story: + """ + Edits a story previously posted by the bot on behalf of a managed business account. + Requires the :attr:`~telegram.BusinessBotRights.can_manage_stories` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + story_id (:obj:`int`): Unique identifier of the story to edit. + content (:class:`telegram.InputStoryContent`): Content of the story. + caption (:obj:`str`, optional): Caption of the story, + 0-:tg-const:`~telegram.constants.StoryLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): Mode for parsing entities in the story caption. + See the constants in :class:`telegram.constants.ParseMode` for the + available modes. + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + areas (Sequence[:class:`telegram.StoryArea`], optional): Sequence of clickable areas to + be shown on the story. + + Note: + Each type of clickable area in :paramref:`areas` has its own maximum limit: + + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_LOCATION_AREAS` + of :class:`telegram.StoryAreaTypeLocation`. + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.\ +MAX_SUGGESTED_REACTION_AREAS` of :class:`telegram.StoryAreaTypeSuggestedReaction`. + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_LINK_AREAS` + of :class:`telegram.StoryAreaTypeLink`. + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_WEATHER_AREAS` + of :class:`telegram.StoryAreaTypeWeather`. + * Up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.\ +MAX_UNIQUE_GIFT_AREAS` of :class:`telegram.StoryAreaTypeUniqueGift`. + + Returns: + :class:`Story` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "story_id": story_id, + "content": content, + "caption": caption, + "parse_mode": parse_mode, + "caption_entities": caption_entities, + "areas": areas, + } + return Story.de_json( + await self._post( + "editStory", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + ) + + async def delete_story( + self, + business_connection_id: str, + story_id: int, + *, + 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, + ) -> bool: + """ + Deletes a story previously posted by the bot on behalf of a managed business account. + Requires the :attr:`~telegram.BusinessBotRights.can_manage_stories` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + story_id (:obj:`int`): Unique identifier of the story to delete. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "story_id": story_id, + } + return await self._post( + "deleteStory", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_business_account_name( + self, + business_connection_id: str, + first_name: str, + last_name: Optional[str] = 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, + ) -> bool: + """ + Changes the first and last name of a managed business account. Requires the + :attr:`~telegram.BusinessBotRights.can_edit_name` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`int` | :obj:`str`): Unique identifier of the business + connection + first_name (:obj:`str`): New first name of the business account; + :tg-const:`telegram.constants.BusinessLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.BusinessLimit.MAX_NAME_LENGTH` characters. + last_name (:obj:`str`, optional): New last name of the business account; + 0-:tg-const:`telegram.constants.BusinessLimit.MAX_NAME_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "first_name": first_name, + "last_name": last_name, + } + return await self._post( + "setBusinessAccountName", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_business_account_username( + self, + business_connection_id: str, + username: Optional[str] = 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, + ) -> bool: + """ + Changes the username of a managed business account. Requires the + :attr:`~telegram.BusinessBotRights.can_edit_username` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + username (:obj:`str`, optional): New business account username; + 0-:tg-const:`telegram.constants.BusinessLimit.MAX_USERNAME_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "username": username, + } + return await self._post( + "setBusinessAccountUsername", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_business_account_bio( + self, + business_connection_id: str, + bio: Optional[str] = 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, + ) -> bool: + """ + Changes the bio of a managed business account. Requires the + :attr:`~telegram.BusinessBotRights.can_edit_bio` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + bio (:obj:`str`, optional): The new value of the bio for the business account; + 0-:tg-const:`telegram.constants.BusinessLimit.MAX_BIO_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "bio": bio, + } + return await self._post( + "setBusinessAccountBio", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_business_account_gift_settings( + self, + business_connection_id: str, + show_gift_button: bool, + accepted_gift_types: AcceptedGiftTypes, + *, + 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, + ) -> bool: + """ + Changes the privacy settings pertaining to incoming gifts in a managed business account. + Requires the :attr:`~telegram.BusinessBotRights.can_change_gift_settings` business + bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business + connection + show_gift_button (:obj:`bool`): Pass :obj:`True`, if a button for sending a gift to the + user or by the business account must always be shown in the input field. + accepted_gift_types (:class:`telegram.AcceptedGiftTypes`): Types of gifts accepted by + the business account. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "show_gift_button": show_gift_button, + "accepted_gift_types": accepted_gift_types, + } + return await self._post( + "setBusinessAccountGiftSettings", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_business_account_profile_photo( + self, + business_connection_id: str, + photo: "InputProfilePhoto", + is_public: Optional[bool] = 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, + ) -> bool: + """ + Changes the profile photo of a managed business account. + Requires the :attr:`~telegram.BusinessBotRights.can_edit_profile_photo` business + bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + photo (:class:`telegram.InputProfilePhoto`): The new profile photo to set. + is_public (:obj:`bool`, optional): Pass :obj:`True` to set the public photo, which will + be visible even if the main photo is hidden by the business account's privacy + settings. An account can have only one public photo. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "photo": photo, + "is_public": is_public, + } + return await self._post( + "setBusinessAccountProfilePhoto", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def remove_business_account_profile_photo( + self, + business_connection_id: str, + is_public: Optional[bool] = 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, + ) -> bool: + """ + Removes the current profile photo of a managed business account. + Requires the :attr:`~telegram.BusinessBotRights.can_edit_profile_photo` business + bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + is_public (:obj:`bool`, optional): Pass :obj:`True` to remove the public photo, which + will be visible even if the main photo is hidden by the business account's privacy + settings. After the main photo is removed, the previous profile photo (if present) + becomes the main photo. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "is_public": is_public, + } + return await self._post( + "removeBusinessAccountProfilePhoto", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def convert_gift_to_stars( + self, + business_connection_id: str, + owned_gift_id: str, + *, + 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, + ) -> bool: + """ + Converts a given regular gift to Telegram Stars. Requires the + :attr:`~telegram.BusinessBotRights.can_convert_gifts_to_stars` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business + connection + owned_gift_id (:obj:`str`): Unique identifier of the regular gift that should be + converted to Telegram Stars. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "owned_gift_id": owned_gift_id, + } + return await self._post( + "convertGiftToStars", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def upgrade_gift( + self, + business_connection_id: str, + owned_gift_id: str, + keep_original_details: Optional[bool] = None, + star_count: 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, + ) -> bool: + """ + Upgrades a given regular gift to a unique gift. Requires the + :attr:`~telegram.BusinessBotRights.can_transfer_and_upgrade_gifts` business bot right. + Additionally requires the :attr:`~telegram.BusinessBotRights.can_transfer_stars` business + bot right if the upgrade is paid. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business + connection + owned_gift_id (:obj:`str`): Unique identifier of the regular gift that should be + upgraded to a unique one. + keep_original_details (:obj:`bool`, optional): Pass :obj:`True` to keep the original + gift text, sender and receiver in the upgraded gift + star_count (:obj:`int`, optional): The amount of Telegram Stars that will + be paid for the upgrade from the business account balance. If + ``gift.prepaid_upgrade_star_count > 0``, then pass ``0``, otherwise, + the :attr:`~telegram.BusinessBotRights.can_transfer_stars` + business bot right is required and :attr:`telegram.Gift.upgrade_star_count` + must be passed. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "owned_gift_id": owned_gift_id, + "keep_original_details": keep_original_details, + "star_count": star_count, + } + return await self._post( + "upgradeGift", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def transfer_gift( + self, + business_connection_id: str, + owned_gift_id: str, + new_owner_chat_id: int, + star_count: 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, + ) -> bool: + """ + Transfers an owned unique gift to another user. Requires the + :attr:`~telegram.BusinessBotRights.can_transfer_and_upgrade_gifts` business bot right. + Requires :attr:`~telegram.BusinessBotRights.can_transfer_stars` business bot right if the + transfer is paid. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business + connection + owned_gift_id (:obj:`str`): Unique identifier of the regular gift that should be + transferred. + new_owner_chat_id (:obj:`int`): Unique identifier of the chat which will + own the gift. The chat must be active in the last + :tg-const:`~telegram.constants.BusinessLimit.\ +CHAT_ACTIVITY_TIMEOUT` seconds. + star_count (:obj:`int`, optional): The amount of Telegram Stars that will be paid for + the transfer from the business account balance. If positive, then + the :attr:`~telegram.BusinessBotRights.can_transfer_stars` business bot + right is required. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "owned_gift_id": owned_gift_id, + "new_owner_chat_id": new_owner_chat_id, + "star_count": star_count, + } + return await self._post( + "transferGift", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def transfer_business_account_stars( + self, + business_connection_id: str, + star_count: int, + *, + 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, + ) -> bool: + """ + Transfers Telegram Stars from the business account balance to the bot's balance. Requires + the :attr:`~telegram.BusinessBotRights.can_transfer_stars` business bot right. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business + connection + star_count (:obj:`int`): Number of Telegram Stars to transfer; + :tg-const:`~telegram.constants.BusinessLimit.MIN_STAR_COUNT`\ +-:tg-const:`~telegram.constants.BusinessLimit.MAX_STAR_COUNT` + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "star_count": star_count, + } + return await self._post( + "transferBusinessAccountStars", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def replace_sticker_in_set( self, user_id: int, @@ -10336,8 +11313,44 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`get_user_chat_boosts`""" setMessageReaction = set_message_reaction """Alias for :meth:`set_message_reaction`""" + giftPremiumSubscription = gift_premium_subscription + """Alias for :meth:`gift_premium_subscription`""" + getBusinessAccountGifts = get_business_account_gifts + """Alias for :meth:`get_business_account_gifts`""" + getBusinessAccountStarBalance = get_business_account_star_balance + """Alias for :meth:`get_business_account_star_balance`""" getBusinessConnection = get_business_connection """Alias for :meth:`get_business_connection`""" + readBusinessMessage = read_business_message + """Alias for :meth:`read_business_message`""" + deleteBusinessMessages = delete_business_messages + """Alias for :meth:`delete_business_messages`""" + postStory = post_story + """Alias for :meth:`post_story`""" + editStory = edit_story + """Alias for :meth:`edit_story`""" + deleteStory = delete_story + """Alias for :meth:`delete_story`""" + setBusinessAccountName = set_business_account_name + """Alias for :meth:`set_business_account_name`""" + setBusinessAccountUsername = set_business_account_username + """Alias for :meth:`set_business_account_username`""" + setBusinessAccountBio = set_business_account_bio + """Alias for :meth:`set_business_account_bio`""" + setBusinessAccountGiftSettings = set_business_account_gift_settings + """Alias for :meth:`set_business_account_gift_settings`""" + setBusinessAccountProfilePhoto = set_business_account_profile_photo + """Alias for :meth:`set_business_account_profile_photo`""" + removeBusinessAccountProfilePhoto = remove_business_account_profile_photo + """Alias for :meth:`remove_business_account_profile_photo`""" + convertGiftToStars = convert_gift_to_stars + """Alias for :meth:`convert_gift_to_stars`""" + upgradeGift = upgrade_gift + """Alias for :meth:`upgrade_gift`""" + transferGift = transfer_gift + """Alias for :meth:`transfer_gift`""" + transferBusinessAccountStars = transfer_business_account_stars + """Alias for :meth:`transfer_business_account_stars`""" replaceStickerInSet = replace_sticker_in_set """Alias for :meth:`replace_sticker_in_set`""" refundStarPayment = refund_star_payment diff --git a/telegram/_business.py b/telegram/_business.py index 95607c24344..5f4b5f4e184 100644 --- a/telegram/_business.py +++ b/telegram/_business.py @@ -30,20 +30,172 @@ from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict +from telegram._utils.warnings import warn +from telegram._utils.warnings_transition import ( + build_deprecation_warning_message, + warn_about_deprecated_attr_in_property, +) +from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot +class BusinessBotRights(TelegramObject): + """ + This object represents the rights of a business bot. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if all their attributes are equal. + + .. versionadded:: NEXT.VERSION + + Args: + can_reply (:obj:`bool`, optional): True, if the bot can send and edit messages in the + private chats that had incoming messages in the last 24 hours. + can_read_messages (:obj:`bool`, optional): True, if the bot can mark incoming private + messages as read. + can_delete_sent_messages (:obj:`bool`, optional): True, if the bot can delete messages + sent by the bot. + can_delete_all_messages (:obj:`bool`, optional): True, if the bot can delete all private + messages in managed chats. + can_edit_name (:obj:`bool`, optional): True, if the bot can edit the first and last name + of the business account. + can_edit_bio (:obj:`bool`, optional): True, if the bot can edit the bio of the + business account. + can_edit_profile_photo (:obj:`bool`, optional): True, if the bot can edit the profile + photo of the business account. + can_edit_username (:obj:`bool`, optional): True, if the bot can edit the username of the + business account. + can_change_gift_settings (:obj:`bool`, optional): True, if the bot can change the privacy + settings pertaining to gifts for the business account. + can_view_gifts_and_stars (:obj:`bool`, optional): True, if the bot can view gifts and the + amount of Telegram Stars owned by the business account. + can_convert_gifts_to_stars (:obj:`bool`, optional): True, if the bot can convert regular + gifts owned by the business account to Telegram Stars. + can_transfer_and_upgrade_gifts (:obj:`bool`, optional): True, if the bot can transfer and + upgrade gifts owned by the business account. + can_transfer_stars (:obj:`bool`, optional): True, if the bot can transfer Telegram Stars + received by the business account to its own account, or use them to upgrade and + transfer gifts. + can_manage_stories (:obj:`bool`, optional): True, if the bot can post, edit and delete + stories on behalf of the business account. + + Attributes: + can_reply (:obj:`bool`): Optional. True, if the bot can send and edit messages in the + private chats that had incoming messages in the last 24 hours. + can_read_messages (:obj:`bool`): Optional. True, if the bot can mark incoming private + messages as read. + can_delete_sent_messages (:obj:`bool`): Optional. True, if the bot can delete messages + sent by the bot. + can_delete_all_messages (:obj:`bool`): Optional. True, if the bot can delete all private + messages in managed chats. + can_edit_name (:obj:`bool`): Optional. True, if the bot can edit the first and last name + of the business account. + can_edit_bio (:obj:`bool`): Optional. True, if the bot can edit the bio of the + business account. + can_edit_profile_photo (:obj:`bool`): Optional. True, if the bot can edit the profile + photo of the business account. + can_edit_username (:obj:`bool`): Optional. True, if the bot can edit the username of the + business account. + can_change_gift_settings (:obj:`bool`): Optional. True, if the bot can change the privacy + settings pertaining to gifts for the business account. + can_view_gifts_and_stars (:obj:`bool`): Optional. True, if the bot can view gifts and the + amount of Telegram Stars owned by the business account. + can_convert_gifts_to_stars (:obj:`bool`): Optional. True, if the bot can convert regular + gifts owned by the business account to Telegram Stars. + can_transfer_and_upgrade_gifts (:obj:`bool`): Optional. True, if the bot can transfer and + upgrade gifts owned by the business account. + can_transfer_stars (:obj:`bool`): Optional. True, if the bot can transfer Telegram Stars + received by the business account to its own account, or use them to upgrade and + transfer gifts. + can_manage_stories (:obj:`bool`): Optional. True, if the bot can post, edit and delete + stories on behalf of the business account. + """ + + __slots__ = ( + "can_change_gift_settings", + "can_convert_gifts_to_stars", + "can_delete_all_messages", + "can_delete_sent_messages", + "can_edit_bio", + "can_edit_name", + "can_edit_profile_photo", + "can_edit_username", + "can_manage_stories", + "can_read_messages", + "can_reply", + "can_transfer_and_upgrade_gifts", + "can_transfer_stars", + "can_view_gifts_and_stars", + ) + + def __init__( + self, + can_reply: Optional[bool] = None, + can_read_messages: Optional[bool] = None, + can_delete_sent_messages: Optional[bool] = None, + can_delete_all_messages: Optional[bool] = None, + can_edit_name: Optional[bool] = None, + can_edit_bio: Optional[bool] = None, + can_edit_profile_photo: Optional[bool] = None, + can_edit_username: Optional[bool] = None, + can_change_gift_settings: Optional[bool] = None, + can_view_gifts_and_stars: Optional[bool] = None, + can_convert_gifts_to_stars: Optional[bool] = None, + can_transfer_and_upgrade_gifts: Optional[bool] = None, + can_transfer_stars: Optional[bool] = None, + can_manage_stories: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.can_reply: Optional[bool] = can_reply + self.can_read_messages: Optional[bool] = can_read_messages + self.can_delete_sent_messages: Optional[bool] = can_delete_sent_messages + self.can_delete_all_messages: Optional[bool] = can_delete_all_messages + self.can_edit_name: Optional[bool] = can_edit_name + self.can_edit_bio: Optional[bool] = can_edit_bio + self.can_edit_profile_photo: Optional[bool] = can_edit_profile_photo + self.can_edit_username: Optional[bool] = can_edit_username + self.can_change_gift_settings: Optional[bool] = can_change_gift_settings + self.can_view_gifts_and_stars: Optional[bool] = can_view_gifts_and_stars + self.can_convert_gifts_to_stars: Optional[bool] = can_convert_gifts_to_stars + self.can_transfer_and_upgrade_gifts: Optional[bool] = can_transfer_and_upgrade_gifts + self.can_transfer_stars: Optional[bool] = can_transfer_stars + self.can_manage_stories: Optional[bool] = can_manage_stories + + self._id_attrs = ( + self.can_reply, + self.can_read_messages, + self.can_delete_sent_messages, + self.can_delete_all_messages, + self.can_edit_name, + self.can_edit_bio, + self.can_edit_profile_photo, + self.can_edit_username, + self.can_change_gift_settings, + self.can_view_gifts_and_stars, + self.can_convert_gifts_to_stars, + self.can_transfer_and_upgrade_gifts, + self.can_transfer_stars, + self.can_manage_stories, + ) + + self._freeze() + + class BusinessConnection(TelegramObject): """ Describes the connection of the bot with a business account. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`id`, :attr:`user`, :attr:`user_chat_id`, :attr:`date`, - :attr:`can_reply`, and :attr:`is_enabled` are equal. + :attr:`rights`, and :attr:`is_enabled` are equal. .. versionadded:: 21.1 + .. versionchanged:: NEXT.VERSION + Equality comparison now considers :attr:`rights` instead of :attr:`can_reply`. Args: id (:obj:`str`): Unique identifier of the business connection. @@ -51,9 +203,15 @@ class BusinessConnection(TelegramObject): user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the business connection. date (:obj:`datetime.datetime`): Date the connection was established in Unix time. - can_reply (:obj:`bool`): True, if the bot can act on behalf of the business account in - chats that were active in the last 24 hours. + can_reply (:obj:`bool`, optional): True, if the bot can act on behalf of the business + account in chats that were active in the last 24 hours. + + .. deprecated:: NEXT.VERSION + Bot API 9.0 deprecated this argument in favor of :paramref:`rights`. is_enabled (:obj:`bool`): True, if the connection is active. + rights (:class:`BusinessBotRights`, optional): Rights of the business bot. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`str`): Unique identifier of the business connection. @@ -61,16 +219,18 @@ class BusinessConnection(TelegramObject): user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the business connection. date (:obj:`datetime.datetime`): Date the connection was established in Unix time. - can_reply (:obj:`bool`): True, if the bot can act on behalf of the business account in - chats that were active in the last 24 hours. is_enabled (:obj:`bool`): True, if the connection is active. + rights (:class:`BusinessBotRights`): Optional. Rights of the business bot. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( - "can_reply", + "_can_reply", "date", "id", "is_enabled", + "rights", "user", "user_chat_id", ) @@ -81,30 +241,67 @@ def __init__( user: "User", user_chat_id: int, date: dtm.datetime, - can_reply: bool, - is_enabled: bool, + can_reply: Optional[bool] = None, + # temporarily optional to account for changed signature + # tags: deprecated NEXT.VERSION; bot api 9.0 + is_enabled: Optional[bool] = None, + rights: Optional[BusinessBotRights] = None, *, api_kwargs: Optional[JSONDict] = None, ): + if is_enabled is None: + raise TypeError("Missing required argument `is_enabled`") + + if can_reply is not None: + warn( + PTBDeprecationWarning( + version="NEXT.VERSION", + message=build_deprecation_warning_message( + deprecated_name="can_reply", + new_name="rights", + bot_api_version="9.0", + object_type="parameter", + ), + ), + stacklevel=2, + ) + super().__init__(api_kwargs=api_kwargs) self.id: str = id self.user: User = user self.user_chat_id: int = user_chat_id self.date: dtm.datetime = date - self.can_reply: bool = can_reply + self._can_reply: Optional[bool] = can_reply self.is_enabled: bool = is_enabled + self.rights: Optional[BusinessBotRights] = rights self._id_attrs = ( self.id, self.user, self.user_chat_id, self.date, - self.can_reply, + self.rights, self.is_enabled, ) self._freeze() + @property + def can_reply(self) -> Optional[bool]: + """:obj:`bool`: Optional. True, if the bot can act on behalf of the business account in + chats that were active in the last 24 hours. + + .. deprecated:: NEXT.VERSION + Bot API 9.0 deprecated this argument in favor of :attr:`rights` + """ + warn_about_deprecated_attr_in_property( + deprecated_attr_name="can_reply", + new_attr_name="rights", + bot_api_version="9.0", + ptb_version="NEXT.VERSION", + ) + return self._can_reply + @classmethod def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessConnection": """See :meth:`telegram.TelegramObject.de_json`.""" @@ -115,6 +312,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessConnec data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) data["user"] = de_json_optional(data.get("user"), User, bot) + data["rights"] = de_json_optional(data.get("rights"), BusinessBotRights, bot) return super().de_json(data=data, bot=bot) diff --git a/telegram/_chat.py b/telegram/_chat.py index fe49dc3593e..ab77b6d5a67 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -3506,6 +3506,41 @@ async def send_gift( **{"chat_id" if self.type == Chat.CHANNEL else "user_id": self.id}, ) + async def transfer_gift( + self, + business_connection_id: str, + owned_gift_id: str, + star_count: 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, + ) -> bool: + """Shortcut for:: + + await bot.transfer_gift(new_owner_chat_id=update.effective_chat.id, *args, **kwargs ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.transfer_gift`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().transfer_gift( + new_owner_chat_id=self.id, + business_connection_id=business_connection_id, + owned_gift_id=owned_gift_id, + star_count=star_count, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def verify( self, custom_description: Optional[str] = None, @@ -3568,6 +3603,40 @@ async def remove_verification( api_kwargs=api_kwargs, ) + async def read_business_message( + self, + business_connection_id: str, + message_id: int, + *, + 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, + ) -> bool: + """Shortcut for:: + + await bot.read_business_message(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.read_business_message`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().read_business_message( + chat_id=self.id, + business_connection_id=business_connection_id, + message_id=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 1ce640638e1..7d0bffe7e92 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -27,10 +27,17 @@ from telegram._chatlocation import ChatLocation from telegram._chatpermissions import ChatPermissions from telegram._files.chatphoto import ChatPhoto +from telegram._gifts import AcceptedGiftTypes from telegram._reaction import ReactionType from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict +from telegram._utils.warnings import warn +from telegram._utils.warnings_transition import ( + build_deprecation_warning_message, + warn_about_deprecated_attr_in_property, +) +from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot, BusinessIntro, BusinessLocation, BusinessOpeningHours, Message @@ -64,6 +71,10 @@ class ChatFullInfo(_ChatBase): message in the chat. .. versionadded:: 21.2 + accepted_gift_types (:class:`telegram.AcceptedGiftTypes`): Information about types of + gifts that are accepted by the chat or by the corresponding user for private chats. + + .. versionadded:: NEXT.VERSION title (:obj:`str`, optional): Title, for supergroups, channels and group chats. username (:obj:`str`, optional): Username, for private chats, supergroups and channels if available. @@ -204,6 +215,10 @@ class ChatFullInfo(_ChatBase): .. versionadded:: 21.11 + .. deprecated:: NEXT.VERSION + Bot API 9.0 introduced :paramref:`accepted_gift_types`, replacing this argument. + Hence, this argument will be removed in future versions. + Attributes: id (:obj:`int`): Unique identifier for this chat. type (:obj:`str`): Type of chat, can be either :attr:`PRIVATE`, :attr:`GROUP`, @@ -218,6 +233,10 @@ class ChatFullInfo(_ChatBase): message in the chat. .. versionadded:: 21.2 + accepted_gift_types (:class:`telegram.AcceptedGiftTypes`): Information about types of + gifts that are accepted by the chat or by the corresponding user for private chats. + + .. versionadded:: NEXT.VERSION title (:obj:`str`, optional): Title, for supergroups, channels and group chats. username (:obj:`str`, optional): Username, for private chats, supergroups and channels if available. @@ -357,16 +376,15 @@ class ChatFullInfo(_ChatBase): sent or forwarded to the channel chat. The field is available only for channel chats. .. versionadded:: 21.4 - can_send_gift (:obj:`bool`): Optional. :obj:`True`, if gifts can be sent to the chat. - - .. versionadded:: 21.11 .. _accent colors: https://core.telegram.org/bots/api#accent-colors .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups """ __slots__ = ( + "_can_send_gift", "accent_color_id", + "accepted_gift_types", "active_usernames", "available_reactions", "background_custom_emoji_id", @@ -375,7 +393,6 @@ class ChatFullInfo(_ChatBase): "business_intro", "business_location", "business_opening_hours", - "can_send_gift", "can_send_paid_media", "can_set_sticker_set", "custom_emoji_sticker_set_name", @@ -452,7 +469,10 @@ def __init__( linked_chat_id: Optional[int] = None, location: Optional[ChatLocation] = None, can_send_paid_media: Optional[bool] = None, + # tags: deprecated NEXT.VERSION; bot api 9.0 can_send_gift: Optional[bool] = None, + # temporarily optional to account for changed signature + accepted_gift_types: Optional[AcceptedGiftTypes] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -466,6 +486,22 @@ def __init__( is_forum=is_forum, api_kwargs=api_kwargs, ) + if accepted_gift_types is None: + raise TypeError("`accepted_gift_type` is a required argument since Bot API 9.0") + + if can_send_gift is not None: + warn( + PTBDeprecationWarning( + "NEXT.VERSION", + build_deprecation_warning_message( + deprecated_name="can_send_gift", + new_name="accepted_gift_types", + object_type="parameter", + bot_api_version="9.0", + ), + ), + stacklevel=2, + ) # Required and unique to this class- with self._unfrozen(): @@ -518,7 +554,27 @@ def __init__( self.business_location: Optional[BusinessLocation] = business_location self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours self.can_send_paid_media: Optional[bool] = can_send_paid_media - self.can_send_gift: Optional[bool] = can_send_gift + self._can_send_gift: Optional[bool] = can_send_gift + self.accepted_gift_types: AcceptedGiftTypes = accepted_gift_types + + @property + def can_send_gift(self) -> Optional[bool]: + """ + :obj:`bool`: Optional. :obj:`True`, if gifts can be sent to the chat. + + .. deprecated:: NEXT.VERSION + As Bot API 9.0 replaces this attribute with :attr:`accepted_gift_types`, this attribute + will be removed in future versions. + + """ + warn_about_deprecated_attr_in_property( + deprecated_attr_name="can_send_gift", + new_attr_name="accepted_gift_types", + bot_api_version="9.0", + ptb_version="NEXT.VERSION", + stacklevel=2, + ) + return self._can_send_gift @classmethod def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo": @@ -533,6 +589,9 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo": ) data["photo"] = de_json_optional(data.get("photo"), ChatPhoto, bot) + data["accepted_gift_types"] = de_json_optional( + data.get("accepted_gift_types"), AcceptedGiftTypes, bot + ) from telegram import ( # pylint: disable=import-outside-toplevel BusinessIntro, diff --git a/telegram/_files/_inputstorycontent.py b/telegram/_files/_inputstorycontent.py new file mode 100644 index 00000000000..3d9ee40e017 --- /dev/null +++ b/telegram/_files/_inputstorycontent.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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.""" + +import datetime as dtm +from typing import Final, Optional, Union + +from telegram import constants +from telegram._files.inputfile import InputFile +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.files import parse_file_input +from telegram._utils.types import FileInput, JSONDict + + +class InputStoryContent(TelegramObject): + """This object describes the content of a story to post. Currently, it can be one of: + + * :class:`telegram.InputStoryContentPhoto` + * :class:`telegram.InputStoryContentVideo` + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the content. + + Attributes: + type (:obj:`str`): Type of the content. + """ + + __slots__ = ("type",) + + PHOTO: Final[str] = constants.InputStoryContentType.PHOTO + """:const:`telegram.constants.InputStoryContentType.PHOTO`""" + VIDEO: Final[str] = constants.InputStoryContentType.VIDEO + """:const:`telegram.constants.InputStoryContentType.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.InputStoryContentType, type, type) + + self._freeze() + + @staticmethod + def _parse_file_input(file_input: FileInput) -> Union[str, InputFile]: + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + return parse_file_input(file_input, attach=True, local_mode=True) + + +class InputStoryContentPhoto(InputStoryContent): + """Describes a photo to post as a story. + + .. versionadded:: NEXT.VERSION + + Args: + photo (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): The photo to post as a story. The photo must be of the + size :tg-const:`telegram.constants.InputStoryContentLimit.PHOTO_WIDTH` + x :tg-const:`telegram.constants.InputStoryContentLimit.PHOTO_HEIGHT` and must not + exceed :tg-const:`telegram.constants.InputStoryContentLimit.PHOTOSIZE_UPLOAD` MB. + |uploadinputnopath|. + + Attributes: + type (:obj:`str`): Type of the content, must be :attr:`~telegram.InputStoryContent.PHOTO`. + photo (:class:`telegram.InputFile`): The photo to post as a story. The photo must be of the + size :tg-const:`telegram.constants.InputStoryContentLimit.PHOTO_WIDTH` + x :tg-const:`telegram.constants.InputStoryContentLimit.PHOTO_HEIGHT` and must not + exceed :tg-const:`telegram.constants.InputStoryContentLimit.PHOTOSIZE_UPLOAD` MB. + + """ + + __slots__ = ("photo",) + + def __init__( + self, + photo: FileInput, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=InputStoryContent.PHOTO, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.photo: Union[str, InputFile] = self._parse_file_input(photo) + + +class InputStoryContentVideo(InputStoryContent): + """ + Describes a video to post as a story. + + .. versionadded:: NEXT.VERSION + + Args: + video (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): The video to post as a story. The video must be of + the size :tg-const:`telegram.constants.InputStoryContentLimit.VIDEO_WIDTH` + x :tg-const:`telegram.constants.InputStoryContentLimit.VIDEO_HEIGHT`, + streamable, encoded with ``H.265`` codec, with key frames added + each second in the ``MPEG4`` format, and must not exceed + :tg-const:`telegram.constants.InputStoryContentLimit.VIDEOSIZE_UPLOAD` MB. + |uploadinputnopath|. + duration (:class:`datetime.timedelta` | :obj:`int` | :obj:`float`, optional): Precise + duration of the video in seconds; + 0-:tg-const:`telegram.constants.InputStoryContentLimit.MAX_VIDEO_DURATION` + cover_frame_timestamp (:class:`datetime.timedelta` | :obj:`int` | :obj:`float`, optional): + Timestamp in seconds of the frame that will be used as the static cover for the story. + Defaults to ``0.0``. + is_animation (:obj:`bool`, optional): Pass :obj:`True` if the video has no sound + + Attributes: + type (:obj:`str`): Type of the content, must be :attr:`~telegram.InputStoryContent.VIDEO`. + video (:class:`telegram.InputFile`): The video to post as a story. The video must be of + the size :tg-const:`telegram.constants.InputStoryContentLimit.VIDEO_WIDTH` + x :tg-const:`telegram.constants.InputStoryContentLimit.VIDEO_HEIGHT`, + streamable, encoded with ``H.265`` codec, with key frames added + each second in the ``MPEG4`` format, and must not exceed + :tg-const:`telegram.constants.InputStoryContentLimit.VIDEOSIZE_UPLOAD` MB. + duration (:class:`datetime.timedelta`): Optional. Precise duration of the video in seconds; + 0-:tg-const:`telegram.constants.InputStoryContentLimit.MAX_VIDEO_DURATION` + cover_frame_timestamp (:class:`datetime.timedelta`): Optional. Timestamp in seconds of the + frame that will be used as the static cover for the story. Defaults to ``0.0``. + is_animation (:obj:`bool`): Optional. Pass :obj:`True` if the video has no sound + """ + + __slots__ = ("cover_frame_timestamp", "duration", "is_animation", "video") + + def __init__( + self, + video: FileInput, + duration: Optional[Union[float, dtm.timedelta]] = None, + cover_frame_timestamp: Optional[Union[float, dtm.timedelta]] = None, + is_animation: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=InputStoryContent.VIDEO, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.video: Union[str, InputFile] = self._parse_file_input(video) + self.duration: Optional[dtm.timedelta] = self._parse_period_arg(duration) + self.cover_frame_timestamp: Optional[dtm.timedelta] = self._parse_period_arg( + cover_frame_timestamp + ) + self.is_animation: Optional[bool] = is_animation + + # This helper is temporarly here until we can use `argumentparsing.parse_period_arg` + # from https://github.com/python-telegram-bot/python-telegram-bot/pull/4750 + @staticmethod + def _parse_period_arg(arg: Optional[Union[float, dtm.timedelta]]) -> Optional[dtm.timedelta]: + if arg is None: + return None + if isinstance(arg, dtm.timedelta): + return arg + return dtm.timedelta(seconds=arg) diff --git a/telegram/_files/inputprofilephoto.py b/telegram/_files/inputprofilephoto.py new file mode 100644 index 00000000000..ec94b8e001e --- /dev/null +++ b/telegram/_files/inputprofilephoto.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 objects that represents a InputProfilePhoto and subclasses.""" + +import datetime as dtm +from typing import TYPE_CHECKING, Optional, Union + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.files import parse_file_input +from telegram._utils.types import FileInput, JSONDict + +if TYPE_CHECKING: + from telegram import InputFile + + +class InputProfilePhoto(TelegramObject): + """This object describes a profile photo to set. Currently, it can be one of + + * :class:`InputProfilePhotoStatic` + * :class:`InputProfilePhotoAnimated` + + .. versionadded:: NEXT.VERSION + + Args: + type (:obj:`str`): Type of the profile photo. + + Attributes: + type (:obj:`str`): Type of the profile photo. + + """ + + STATIC = constants.InputProfilePhotoType.STATIC + """:obj:`str`: :tg-const:`telegram.constants.InputProfilePhotoType.STATIC`.""" + ANIMATED = constants.InputProfilePhotoType.ANIMATED + """:obj:`str`: :tg-const:`telegram.constants.InputProfilePhotoType.ANIMATED`.""" + + __slots__ = ("type",) + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.InputProfilePhotoType, type, type) + + self._freeze() + + +class InputProfilePhotoStatic(InputProfilePhoto): + """A static profile photo in the .JPG format. + + .. versionadded:: NEXT.VERSION + + Args: + photo (:term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path`): The static profile photo. |uploadinputnopath| + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InputProfilePhotoType.STATIC`. + photo (:class:`telegram.InputFile` | :obj:`str`): The static profile photo. + + """ + + __slots__ = ("photo",) + + def __init__( + self, + photo: FileInput, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=constants.InputProfilePhotoType.STATIC, api_kwargs=api_kwargs) + with self._unfrozen(): + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + self.photo: Union[str, InputFile] = parse_file_input( + photo, attach=True, local_mode=True + ) + + +class InputProfilePhotoAnimated(InputProfilePhoto): + """An animated profile photo in the MPEG4 format. + + .. versionadded:: NEXT.VERSION + + Args: + animation (:term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path`): The animated profile photo. |uploadinputnopath| + main_frame_timestamp (:class:`datetime.timedelta` | :obj:`int` | :obj:`float`, optional): + Timestamp in seconds of the frame that will be used as the static profile photo. + Defaults to ``0.0``. + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InputProfilePhotoType.ANIMATED`. + animation (:class:`telegram.InputFile` | :obj:`str`): The animated profile photo. + main_frame_timestamp (:class:`datetime.timedelta`): Optional. Timestamp in seconds of the + frame that will be used as the static profile photo. Defaults to ``0.0``. + """ + + __slots__ = ("animation", "main_frame_timestamp") + + def __init__( + self, + animation: FileInput, + main_frame_timestamp: Union[float, dtm.timedelta, None] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=constants.InputProfilePhotoType.ANIMATED, api_kwargs=api_kwargs) + with self._unfrozen(): + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + self.animation: Union[str, InputFile] = parse_file_input( + animation, attach=True, local_mode=True + ) + + if isinstance(main_frame_timestamp, dtm.timedelta): + self.main_frame_timestamp: Optional[dtm.timedelta] = main_frame_timestamp + elif main_frame_timestamp is None: + self.main_frame_timestamp = None + else: + self.main_frame_timestamp = dtm.timedelta(seconds=main_frame_timestamp) diff --git a/telegram/_gifts.py b/telegram/_gifts.py index d068923c6df..ad17451aa19 100644 --- a/telegram/_gifts.py +++ b/telegram/_gifts.py @@ -22,8 +22,10 @@ from typing import TYPE_CHECKING, Optional from telegram._files.sticker import Sticker +from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -145,3 +147,211 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Gifts": data["gifts"] = de_list_optional(data.get("gifts"), Gift, bot) return super().de_json(data=data, bot=bot) + + +class GiftInfo(TelegramObject): + """Describes a service message about a regular gift that was sent or received. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`gift` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + gift (:class:`Gift`): Information about the gift. + owned_gift_id (:obj:`str`, optional): Unique identifier of the received gift for the bot; + only present for gifts received on behalf of business accounts + convert_star_count (:obj:`int`, optional) Number of Telegram Stars that can be claimed by + the receiver by converting the gift; omitted if conversion to Telegram Stars + is impossible + prepaid_upgrade_star_count (:obj:`int`, optional): Number of Telegram Stars that were + prepaid by the sender for the ability to upgrade the gift + can_be_upgraded (:obj:`bool`, optional): :obj:`True`, if the gift can be upgraded + to a unique gift. + text (:obj:`str`, optional): Text of the message that was added to the gift. + entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities that + appear in the text. + is_private (:obj:`bool`, optional): :obj:`True`, if the sender and gift text are + shown only to the gift receiver; otherwise, everyone will be able to see them. + + Attributes: + gift (:class:`Gift`): Information about the gift. + owned_gift_id (:obj:`str`): Optional. Unique identifier of the received gift for the bot; + only present for gifts received on behalf of business accounts + convert_star_count (:obj:`int`): Optional. Number of Telegram Stars that can be claimed by + the receiver by converting the gift; omitted if conversion to Telegram Stars + is impossible + prepaid_upgrade_star_count (:obj:`int`): Optional. Number of Telegram Stars that were + prepaid by the sender for the ability to upgrade the gift + can_be_upgraded (:obj:`bool`): Optional. :obj:`True`, if the gift can be upgraded + to a unique gift. + text (:obj:`str`): Optional. Text of the message that was added to the gift. + entities (Sequence[:class:`telegram.MessageEntity`]): Optional. Special entities that + appear in the text. + is_private (:obj:`bool`): Optional. :obj:`True`, if the sender and gift text are + shown only to the gift receiver; otherwise, everyone will be able to see them. + + """ + + __slots__ = ( + "can_be_upgraded", + "convert_star_count", + "entities", + "gift", + "is_private", + "owned_gift_id", + "prepaid_upgrade_star_count", + "text", + ) + + def __init__( + self, + gift: Gift, + owned_gift_id: Optional[str] = None, + convert_star_count: Optional[int] = None, + prepaid_upgrade_star_count: Optional[int] = None, + can_be_upgraded: Optional[bool] = None, + text: Optional[str] = None, + entities: Optional[Sequence[MessageEntity]] = None, + is_private: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.gift: Gift = gift + # Optional + self.owned_gift_id: Optional[str] = owned_gift_id + self.convert_star_count: Optional[int] = convert_star_count + self.prepaid_upgrade_star_count: Optional[int] = prepaid_upgrade_star_count + self.can_be_upgraded: Optional[bool] = can_be_upgraded + self.text: Optional[str] = text + self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities) + self.is_private: Optional[bool] = is_private + + self._id_attrs = (self.gift,) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "GiftInfo": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["gift"] = de_json_optional(data.get("gift"), Gift, bot) + data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot) + + return super().de_json(data=data, bot=bot) + + def parse_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`text` + from a given :class:`telegram.MessageEntity` of :attr:`entities`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``Message.text`` with the offset and length.) + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to :attr:`entities`. + + Returns: + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the gift info has no text. + + """ + if not self.text: + raise RuntimeError("This GiftInfo has no 'text'.") + + return parse_message_entity(self.text, entity) + + def parse_entities(self, types: Optional[list[str]] = None) -> dict[MessageEntity, str]: + """ + Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. + It contains entities from this gift info's text filtered by their ``type`` attribute as + the key, and the text that each entity belongs to as the value of the :obj:`dict`. + + Note: + This method should always be used instead of the :attr:`entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_entity` for more info. + + Args: + types (list[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``type`` attribute of an entity is contained in this list, it will be returned. + Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`. + + Returns: + dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to + the text that belongs to them, calculated based on UTF-16 codepoints. + + Raises: + RuntimeError: If the gift info has no text. + + """ + if not self.text: + raise RuntimeError("This GiftInfo has no 'text'.") + + return parse_message_entities(self.text, self.entities, types) + + +class AcceptedGiftTypes(TelegramObject): + """This object describes the types of gifts that can be gifted to a user or a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`unlimited_gifts`, :attr:`limited_gifts`, + :attr:`unique_gifts` and :attr:`premium_subscription` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + unlimited_gifts (:class:`bool`): :obj:`True`, if unlimited regular gifts are accepted. + limited_gifts (:class:`bool`): :obj:`True`, if limited regular gifts are accepted. + unique_gifts (:class:`bool`): :obj:`True`, if unique gifts or gifts that can be upgraded + to unique for free are accepted. + premium_subscription (:class:`bool`): :obj:`True`, if a Telegram Premium subscription + is accepted. + + Attributes: + unlimited_gifts (:class:`bool`): :obj:`True`, if unlimited regular gifts are accepted. + limited_gifts (:class:`bool`): :obj:`True`, if limited regular gifts are accepted. + unique_gifts (:class:`bool`): :obj:`True`, if unique gifts or gifts that can be upgraded + to unique for free are accepted. + premium_subscription (:class:`bool`): :obj:`True`, if a Telegram Premium subscription + is accepted. + + """ + + __slots__ = ( + "limited_gifts", + "premium_subscription", + "unique_gifts", + "unlimited_gifts", + ) + + def __init__( + self, + unlimited_gifts: bool, + limited_gifts: bool, + unique_gifts: bool, + premium_subscription: bool, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.unlimited_gifts: bool = unlimited_gifts + self.limited_gifts: bool = limited_gifts + self.unique_gifts: bool = unique_gifts + self.premium_subscription: bool = premium_subscription + + self._id_attrs = ( + self.unlimited_gifts, + self.limited_gifts, + self.unique_gifts, + self.premium_subscription, + ) + + self._freeze() diff --git a/telegram/_message.py b/telegram/_message.py index 646266be84f..f39e52e7851 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -49,11 +49,13 @@ GeneralForumTopicUnhidden, ) from telegram._games.game import Game +from telegram._gifts import GiftInfo from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._linkpreviewoptions import LinkPreviewOptions from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from telegram._messageentity import MessageEntity from telegram._paidmedia import PaidMediaInfo +from telegram._paidmessagepricechanged import PaidMessagePriceChanged from telegram._passport.passportdata import PassportData from telegram._payment.invoice import Invoice from telegram._payment.refundedpayment import RefundedPayment @@ -64,6 +66,7 @@ from telegram._shared import ChatShared, UsersShared from telegram._story import Story from telegram._telegramobject import TelegramObject +from telegram._uniquegift import UniqueGiftInfo from telegram._user import User from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp @@ -443,6 +446,10 @@ class Message(MaybeInaccessibleMessage): `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. + paid_star_count (:obj:`int`, optional): The number of Telegram Stars that were paid by the + sender of the message to send it + + .. versionadded:: NEXT.VERSION passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data. poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the poll. @@ -525,6 +532,14 @@ class Message(MaybeInaccessibleMessage): with the bot. .. versionadded:: 20.1 + gift (:class:`telegram.GiftInfo`, optional): Service message: a regular gift was sent + or received. + + .. versionadded:: NEXT.VERSION + unique_gift (:class:`telegram.UniqueGiftInfo`, optional): Service message: a unique gift + was sent or received + + .. versionadded:: NEXT.VERSION giveaway_created (:class:`telegram.GiveawayCreated`, optional): Service message: a scheduled giveaway was created @@ -541,6 +556,10 @@ class Message(MaybeInaccessibleMessage): giveaway without public winners was completed .. versionadded:: 20.8 + paid_message_price_changed (:class:`telegram.PaidMessagePriceChanged`, optional): Service + message: the price for paid messages has changed in the chat + + .. versionadded:: NEXT.VERSION external_reply (:class:`telegram.ExternalReplyInfo`, optional): Information about the message that is being replied to, which may come from another chat or forum topic. @@ -771,6 +790,10 @@ class Message(MaybeInaccessibleMessage): `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. + paid_star_count (:obj:`int`): Optional. The number of Telegram Stars that were paid by the + sender of the message to send it + + .. versionadded:: NEXT.VERSION passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data. Examples: @@ -853,6 +876,14 @@ class Message(MaybeInaccessibleMessage): with the bot. .. versionadded:: 20.1 + gift (:class:`telegram.GiftInfo`): Optional. Service message: a regular gift was sent + or received. + + .. versionadded:: NEXT.VERSION + unique_gift (:class:`telegram.UniqueGiftInfo`): Optional. Service message: a unique gift + was sent or received + + .. versionadded:: NEXT.VERSION giveaway_created (:class:`telegram.GiveawayCreated`): Optional. Service message: a scheduled giveaway was created @@ -869,6 +900,10 @@ class Message(MaybeInaccessibleMessage): giveaway without public winners was completed .. versionadded:: 20.8 + paid_message_price_changed (:class:`telegram.PaidMessagePriceChanged`): Optional. Service + message: the price for paid messages has changed in the chat + + .. versionadded:: NEXT.VERSION external_reply (:class:`telegram.ExternalReplyInfo`): Optional. Information about the message that is being replied to, which may come from another chat or forum topic. @@ -966,6 +1001,7 @@ class Message(MaybeInaccessibleMessage): "game", "general_forum_topic_hidden", "general_forum_topic_unhidden", + "gift", "giveaway", "giveaway_completed", "giveaway_created", @@ -989,6 +1025,8 @@ class Message(MaybeInaccessibleMessage): "new_chat_photo", "new_chat_title", "paid_media", + "paid_message_price_changed", + "paid_star_count", "passport_data", "photo", "pinned_message", @@ -1008,6 +1046,7 @@ class Message(MaybeInaccessibleMessage): "successful_payment", "supergroup_chat_created", "text", + "unique_gift", "users_shared", "venue", "via_bot", @@ -1109,6 +1148,10 @@ def __init__( show_caption_above_media: Optional[bool] = None, paid_media: Optional[PaidMediaInfo] = None, refunded_payment: Optional[RefundedPayment] = None, + gift: Optional[GiftInfo] = None, + unique_gift: Optional[UniqueGiftInfo] = None, + paid_message_price_changed: Optional[PaidMessagePriceChanged] = None, + paid_star_count: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1212,6 +1255,12 @@ def __init__( 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.gift: Optional[GiftInfo] = gift + self.unique_gift: Optional[UniqueGiftInfo] = unique_gift + self.paid_message_price_changed: Optional[PaidMessagePriceChanged] = ( + paid_message_price_changed + ) + self.paid_star_count: Optional[int] = paid_star_count self._effective_attachment = DEFAULT_NONE @@ -1346,6 +1395,11 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message": data["refunded_payment"] = de_json_optional( data.get("refunded_payment"), RefundedPayment, bot ) + data["gift"] = de_json_optional(data.get("gift"), GiftInfo, bot) + data["unique_gift"] = de_json_optional(data.get("unique_gift"), UniqueGiftInfo, bot) + data["paid_message_price_changed"] = de_json_optional( + data.get("paid_message_price_changed"), PaidMessagePriceChanged, bot + ) # Unfortunately, this needs to be here due to cyclic imports from telegram._giveaway import ( # pylint: disable=import-outside-toplevel @@ -4479,6 +4533,43 @@ async def set_reaction( api_kwargs=api_kwargs, ) + async def read_business_message( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.read_business_message( + 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.read_business_message`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + """ + return await self.get_bot().read_business_message( + chat_id=self.chat_id, + message_id=self.message_id, + business_connection_id=self.business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def parse_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. diff --git a/telegram/_ownedgift.py b/telegram/_ownedgift.py new file mode 100644 index 00000000000..6481eb33de3 --- /dev/null +++ b/telegram/_ownedgift.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 owned gifts.""" + +import datetime as dtm +from collections.abc import Sequence +from typing import TYPE_CHECKING, Final, Optional + +from telegram import constants +from telegram._gifts import Gift +from telegram._messageentity import MessageEntity +from telegram._telegramobject import TelegramObject +from telegram._uniquegift import UniqueGift +from telegram._user import User +from telegram._utils import enum +from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.entities import parse_message_entities, parse_message_entity +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class OwnedGift(TelegramObject): + """This object describes a gift received and owned by a user or a chat. Currently, it + can be one of: + + * :class:`telegram.OwnedGiftRegular` + * :class:`telegram.OwnedGiftUnique` + + 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 owned gift. + + Attributes: + type (:obj:`str`): Type of the owned gift. + """ + + __slots__ = ("type",) + + REGULAR: Final[str] = constants.OwnedGiftType.REGULAR + """:const:`telegram.constants.OwnedGiftType.REGULAR`""" + UNIQUE: Final[str] = constants.OwnedGiftType.UNIQUE + """:const:`telegram.constants.OwnedGiftType.UNIQUE`""" + + 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.OwnedGiftType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGift": + """Converts JSON data to the appropriate :class:`OwnedGift` 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) + + _class_mapping: dict[str, type[OwnedGift]] = { + cls.REGULAR: OwnedGiftRegular, + cls.UNIQUE: OwnedGiftUnique, + } + + if cls is OwnedGift 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 OwnedGifts(TelegramObject): + """Contains the list of gifts received and owned by a user or a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`total_count` and :attr:`gifts` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + total_count (:obj:`int`): The total number of gifts owned by the user or the chat. + gifts (Sequence[:class:`telegram.OwnedGift`]): The list of gifts. + next_offset (:obj:`str`, optional): Offset for the next request. If empty, + then there are no more results. + + Attributes: + total_count (:obj:`int`): The total number of gifts owned by the user or the chat. + gifts (Sequence[:class:`telegram.OwnedGift`]): The list of gifts. + next_offset (:obj:`str`): Optional. Offset for the next request. If empty, + then there are no more results. + """ + + __slots__ = ( + "gifts", + "next_offset", + "total_count", + ) + + def __init__( + self, + total_count: int, + gifts: Sequence[OwnedGift], + next_offset: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.total_count: int = total_count + self.gifts: tuple[OwnedGift, ...] = parse_sequence_arg(gifts) + self.next_offset: Optional[str] = next_offset + + self._id_attrs = (self.total_count, self.gifts) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGifts": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["gifts"] = de_list_optional(data.get("gifts"), OwnedGift, bot) + return super().de_json(data=data, bot=bot) + + +class OwnedGiftRegular(OwnedGift): + """Describes a regular gift owned by a user or a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`gift` and :attr:`send_date` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + gift (:class:`telegram.Gift`): Information about the regular gift. + owned_gift_id (:obj:`str`, optional): Unique identifier of the gift for the bot; for + gifts received on behalf of business accounts only. + sender_user (:class:`telegram.User`, optional): Sender of the gift if it is a known user. + send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`. + |datetime_localization|. + text (:obj:`str`, optional): Text of the message that was added to the gift. + entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities that + appear in the text. + is_private (:obj:`bool`, optional): :obj:`True`, if the sender and gift text are shown + only to the gift receiver; otherwise, everyone will be able to see them. + is_saved (:obj:`bool`, optional): :obj:`True`, if the gift is displayed on the account's + profile page; for gifts received on behalf of business accounts only. + can_be_upgraded (:obj:`bool`, optional): :obj:`True`, if the gift can be upgraded to a + unique gift; for gifts received on behalf of business accounts only. + was_refunded (:obj:`bool`, optional): :obj:`True`, if the gift was refunded and isn't + available anymore. + convert_star_count (:obj:`int`, optional): Number of Telegram Stars that can be + claimed by the receiver instead of the gift; omitted if the gift cannot be converted + to Telegram Stars. + prepaid_upgrade_star_count (:obj:`int`, optional): Number of Telegram Stars that were + paid by the sender for the ability to upgrade the gift. + + Attributes: + type (:obj:`str`): Type of the gift, always :attr:`~telegram.OwnedGift.REGULAR`. + gift (:class:`telegram.Gift`): Information about the regular gift. + owned_gift_id (:obj:`str`): Optional. Unique identifier of the gift for the bot; for + gifts received on behalf of business accounts only. + sender_user (:class:`telegram.User`): Optional. Sender of the gift if it is a known user. + send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`. + |datetime_localization|. + text (:obj:`str`): Optional. Text of the message that was added to the gift. + entities (Sequence[:class:`telegram.MessageEntity`]): Optional. Special entities that + appear in the text. + is_private (:obj:`bool`): Optional. :obj:`True`, if the sender and gift text are shown + only to the gift receiver; otherwise, everyone will be able to see them. + is_saved (:obj:`bool`): Optional. :obj:`True`, if the gift is displayed on the account's + profile page; for gifts received on behalf of business accounts only. + can_be_upgraded (:obj:`bool`): Optional. :obj:`True`, if the gift can be upgraded to a + unique gift; for gifts received on behalf of business accounts only. + was_refunded (:obj:`bool`): Optional. :obj:`True`, if the gift was refunded and isn't + available anymore. + convert_star_count (:obj:`int`): Optional. Number of Telegram Stars that can be + claimed by the receiver instead of the gift; omitted if the gift cannot be converted + to Telegram Stars. + prepaid_upgrade_star_count (:obj:`int`): Optional. Number of Telegram Stars that were + paid by the sender for the ability to upgrade the gift. + + """ + + __slots__ = ( + "can_be_upgraded", + "convert_star_count", + "entities", + "gift", + "is_private", + "is_saved", + "owned_gift_id", + "prepaid_upgrade_star_count", + "send_date", + "sender_user", + "text", + "was_refunded", + ) + + def __init__( + self, + gift: Gift, + send_date: dtm.datetime, + owned_gift_id: Optional[str] = None, + sender_user: Optional[User] = None, + text: Optional[str] = None, + entities: Optional[Sequence[MessageEntity]] = None, + is_private: Optional[bool] = None, + is_saved: Optional[bool] = None, + can_be_upgraded: Optional[bool] = None, + was_refunded: Optional[bool] = None, + convert_star_count: Optional[int] = None, + prepaid_upgrade_star_count: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=OwnedGift.REGULAR, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.gift: Gift = gift + self.send_date: dtm.datetime = send_date + self.owned_gift_id: Optional[str] = owned_gift_id + self.sender_user: Optional[User] = sender_user + self.text: Optional[str] = text + self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities) + self.is_private: Optional[bool] = is_private + self.is_saved: Optional[bool] = is_saved + self.can_be_upgraded: Optional[bool] = can_be_upgraded + self.was_refunded: Optional[bool] = was_refunded + self.convert_star_count: Optional[int] = convert_star_count + self.prepaid_upgrade_star_count: Optional[int] = prepaid_upgrade_star_count + + self._id_attrs = (self.type, self.gift, self.send_date) + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGiftRegular": + """See :meth:`telegram.OwnedGift.de_json`.""" + data = cls._parse_data(data) + + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) + data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot) + data["gift"] = de_json_optional(data.get("gift"), Gift, bot) + data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot) + + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + def parse_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`text` + from a given :class:`telegram.MessageEntity` of :attr:`entities`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``OwnedGiftRegular.text`` with the offset and length.) + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to :attr:`entities`. + + Returns: + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the owned gift has no text. + + """ + if not self.text: + raise RuntimeError("This OwnedGiftRegular has no 'text'.") + + return parse_message_entity(self.text, entity) + + def parse_entities(self, types: Optional[list[str]] = None) -> dict[MessageEntity, str]: + """ + Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. + It contains entities from this owned gift's text filtered by their ``type`` attribute as + the key, and the text that each entity belongs to as the value of the :obj:`dict`. + + Note: + This method should always be used instead of the :attr:`entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_entity` for more info. + + Args: + types (list[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``type`` attribute of an entity is contained in this list, it will be returned. + Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`. + + Returns: + dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to + the text that belongs to them, calculated based on UTF-16 codepoints. + + Raises: + RuntimeError: If the owned gift has no text. + + """ + if not self.text: + raise RuntimeError("This OwnedGiftRegular has no 'text'.") + + return parse_message_entities(self.text, self.entities, types) + + +class OwnedGiftUnique(OwnedGift): + """ + Describes a unique gift received and owned by a user or a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`gift` and :attr:`send_date` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + gift (:class:`telegram.UniqueGift`): Information about the unique gift. + owned_gift_id (:obj:`str`, optional): Unique identifier of the received gift for the + bot; for gifts received on behalf of business accounts only. + sender_user (:class:`telegram.User`, optional): Sender of the gift if it is a known user. + send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`. + |datetime_localization|. + is_saved (:obj:`bool`, optional): :obj:`True`, if the gift is displayed on the account's + profile page; for gifts received on behalf of business accounts only. + can_be_transferred (:obj:`bool`, optional): :obj:`True`, if the gift can be transferred to + another owner; for gifts received on behalf of business accounts only. + transfer_star_count (:obj:`int`, optional): Number of Telegram Stars that must be paid + to transfer the gift; omitted if the bot cannot transfer the gift. + + Attributes: + type (:obj:`str`): Type of the owned gift, always :tg-const:`~telegram.OwnedGift.UNIQUE`. + gift (:class:`telegram.UniqueGift`): Information about the unique gift. + owned_gift_id (:obj:`str`): Optional. Unique identifier of the received gift for the + bot; for gifts received on behalf of business accounts only. + sender_user (:class:`telegram.User`): Optional. Sender of the gift if it is a known user. + send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`. + |datetime_localization|. + is_saved (:obj:`bool`): Optional. :obj:`True`, if the gift is displayed on the account's + profile page; for gifts received on behalf of business accounts only. + can_be_transferred (:obj:`bool`): Optional. :obj:`True`, if the gift can be transferred to + another owner; for gifts received on behalf of business accounts only. + transfer_star_count (:obj:`int`): Optional. Number of Telegram Stars that must be paid + to transfer the gift; omitted if the bot cannot transfer the gift. + """ + + __slots__ = ( + "can_be_transferred", + "gift", + "is_saved", + "owned_gift_id", + "send_date", + "sender_user", + "transfer_star_count", + ) + + def __init__( + self, + gift: UniqueGift, + send_date: dtm.datetime, + owned_gift_id: Optional[str] = None, + sender_user: Optional[User] = None, + is_saved: Optional[bool] = None, + can_be_transferred: Optional[bool] = None, + transfer_star_count: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=OwnedGift.UNIQUE, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.gift: UniqueGift = gift + self.send_date: dtm.datetime = send_date + self.owned_gift_id: Optional[str] = owned_gift_id + self.sender_user: Optional[User] = sender_user + self.is_saved: Optional[bool] = is_saved + self.can_be_transferred: Optional[bool] = can_be_transferred + self.transfer_star_count: Optional[int] = transfer_star_count + + self._id_attrs = (self.type, self.gift, self.send_date) + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGiftUnique": + """See :meth:`telegram.OwnedGift.de_json`.""" + data = cls._parse_data(data) + + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) + data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot) + data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot) + + return super().de_json(data=data, bot=bot) # type: ignore[return-value] diff --git a/telegram/_paidmessagepricechanged.py b/telegram/_paidmessagepricechanged.py new file mode 100644 index 00000000000..f31d7293b40 --- /dev/null +++ b/telegram/_paidmessagepricechanged.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 describes a price change of a paid message.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class PaidMessagePriceChanged(TelegramObject): + """Describes a service message about a change in the price of paid messages within a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`paid_message_star_count` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + paid_message_star_count (:obj:`int`): The new number of Telegram Stars that must be paid by + non-administrator users of the supergroup chat for each sent message + + Attributes: + paid_message_star_count (:obj:`int`): The new number of Telegram Stars that must be paid by + non-administrator users of the supergroup chat for each sent message + """ + + __slots__ = ("paid_message_star_count",) + + def __init__( + self, + paid_message_star_count: int, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.paid_message_star_count: int = paid_message_star_count + + self._id_attrs = (self.paid_message_star_count,) + self._freeze() diff --git a/telegram/_payment/stars/affiliateinfo.py b/telegram/_payment/stars/affiliateinfo.py index 80349290b44..64fd7224e23 100644 --- a/telegram/_payment/stars/affiliateinfo.py +++ b/telegram/_payment/stars/affiliateinfo.py @@ -48,10 +48,10 @@ class AffiliateInfo(TelegramObject): amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the transaction, rounded to 0; can be negative for refunds nanostar_amount (:obj:`int`, optional): The number of - :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram + :tg-const:`~telegram.constants.Nanostar.VALUE` shares of Telegram Stars received by the affiliate; from - :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to - :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`; + :tg-const:`~telegram.constants.NanostarLimit.MIN_AMOUNT` to + :tg-const:`~telegram.constants.NanostarLimit.MAX_AMOUNT`; can be negative for refunds Attributes: @@ -64,10 +64,10 @@ class AffiliateInfo(TelegramObject): amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the transaction, rounded to 0; can be negative for refunds nanostar_amount (:obj:`int`): Optional. The number of - :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram + :tg-const:`~telegram.constants.Nanostar.VALUE` shares of Telegram Stars received by the affiliate; from - :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to - :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`; + :tg-const:`~telegram.constants.NanostarLimit.MIN_AMOUNT` to + :tg-const:`~telegram.constants.NanostarLimit.MAX_AMOUNT`; can be negative for refunds """ diff --git a/telegram/_payment/stars/staramount.py b/telegram/_payment/stars/staramount.py new file mode 100644 index 00000000000..a8d61b2a118 --- /dev/null +++ b/telegram/_payment/stars/staramount.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 an object that represents a Telegram StarAmount.""" + + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class StarAmount(TelegramObject): + """Describes an amount of Telegram Stars. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`amount` and :attr:`nanostar_amount` are equal. + + Args: + amount (:obj:`int`): Integer amount of Telegram Stars, rounded to ``0``; can be negative. + nanostar_amount (:obj:`int`, optional): The number of + :tg-const:`telegram.constants.Nanostar.VALUE` shares of Telegram + Stars; from :tg-const:`telegram.constants.NanostarLimit.MIN_AMOUNT` + to :tg-const:`telegram.constants.NanostarLimit.MAX_AMOUNT`; can be + negative if and only if :attr:`amount` is non-positive. + + Attributes: + amount (:obj:`int`): Integer amount of Telegram Stars, rounded to ``0``; can be negative. + nanostar_amount (:obj:`int`): Optional. The number of + :tg-const:`telegram.constants.Nanostar.VALUE` shares of Telegram + Stars; from :tg-const:`telegram.constants.NanostarLimit.MIN_AMOUNT` + to :tg-const:`telegram.constants.NanostarLimit.MAX_AMOUNT`; can be + negative if and only if :attr:`amount` is non-positive. + + """ + + __slots__ = ("amount", "nanostar_amount") + + def __init__( + self, + amount: int, + nanostar_amount: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.amount: int = amount + self.nanostar_amount: Optional[int] = nanostar_amount + + self._id_attrs = (self.amount, self.nanostar_amount) + + self._freeze() diff --git a/telegram/_payment/stars/startransactions.py b/telegram/_payment/stars/startransactions.py index 7ac1ef7e338..09f314985ff 100644 --- a/telegram/_payment/stars/startransactions.py +++ b/telegram/_payment/stars/startransactions.py @@ -52,9 +52,9 @@ class StarTransaction(TelegramObject): successful incoming payments from users. amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction. nanostar_amount (:obj:`int`, optional): The number of - :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram + :tg-const:`~telegram.constants.Nanostar.VALUE` shares of Telegram Stars transferred by the transaction; from 0 to - :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT` + :tg-const:`~telegram.constants.NanostarLimit.MAX_AMOUNT` .. versionadded:: 21.9 date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object. @@ -72,9 +72,9 @@ class StarTransaction(TelegramObject): successful incoming payments from users. amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction. nanostar_amount (:obj:`int`): Optional. The number of - :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram + :tg-const:`~telegram.constants.Nanostar.VALUE` shares of Telegram Stars transferred by the transaction; from 0 to - :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT` + :tg-const:`~telegram.constants.NanostarLimit.MAX_AMOUNT` .. versionadded:: 21.9 date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object. diff --git a/telegram/_payment/stars/transactionpartner.py b/telegram/_payment/stars/transactionpartner.py index 811947581ee..cf086f6bff9 100644 --- a/telegram/_payment/stars/transactionpartner.py +++ b/telegram/_payment/stars/transactionpartner.py @@ -281,55 +281,115 @@ 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. + considered equal, if their :attr:`user` and :attr:`transaction_type` are equal. .. versionadded:: 21.4 + .. versionchanged:: NEXT.VERSION + Equality comparison now includes the new required argument :paramref:`transaction_type`, + introduced in Bot API 9.0. + Args: + transaction_type (:obj:`str`): Type of the transaction, currently one of + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` for payments via + invoices, :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + for payments for paid media, + :tg-const:`telegram.constants.TransactionPartnerUser.GIFT_PURCHASE` for gifts sent by + the bot, :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` + for Telegram Premium subscriptions gifted by the bot, + :tg-const:`telegram.constants.TransactionPartnerUser.BUSINESS_ACCOUNT_TRANSFER` for + direct transfers from managed business accounts. + + .. versionadded:: NEXT.VERSION user (:class:`telegram.User`): Information about the user. affiliate (:class:`telegram.AffiliateInfo`, optional): Information about the affiliate that - received a commission via this transaction + received a commission via this transaction. Can be available only for + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` + and :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + transactions. .. versionadded:: 21.9 - invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. + invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. Can be available + only for :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` + transactions. subscription_period (:class:`datetime.timedelta`, optional): The duration of the paid - subscription + subscription. Can be available only for + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` transactions. .. versionadded:: 21.8 paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid - media bought by the user. + media bought by the user. for + :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + transactions only. .. versionadded:: 21.5 - paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload. + paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload. Can be + available only for + :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` transactions. .. versionadded:: 21.6 - gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot + gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot; for + :tg-const:`telegram.constants.TransactionPartnerUser.GIFT_PURCHASE` transactions only. .. versionadded:: 21.8 + premium_subscription_duration (:obj:`int`, optional): Number of months the gifted Telegram + Premium subscription will be active for; for + :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` + transactions only. + + .. versionadded:: NEXT.VERSION Attributes: type (:obj:`str`): The type of the transaction partner, always :tg-const:`telegram.TransactionPartner.USER`. + transaction_type (:obj:`str`): Type of the transaction, currently one of + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` for payments via + invoices, :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + for payments for paid media, + :tg-const:`telegram.constants.TransactionPartnerUser.GIFT_PURCHASE` for gifts sent by + the bot, :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` + for Telegram Premium subscriptions gifted by the bot, + :tg-const:`telegram.constants.TransactionPartnerUser.BUSINESS_ACCOUNT_TRANSFER` for + direct transfers from managed business accounts. + + .. versionadded:: NEXT.VERSION user (:class:`telegram.User`): Information about the user. affiliate (:class:`telegram.AffiliateInfo`): Optional. Information about the affiliate that - received a commission via this transaction + received a commission via this transaction. Can be available only for + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` + and :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + transactions. .. versionadded:: 21.9 - invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. + invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. Can be available + only for :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` + transactions. subscription_period (:class:`datetime.timedelta`): Optional. The duration of the paid - subscription + subscription. Can be available only for + :tg-const:`telegram.constants.TransactionPartnerUser.INVOICE_PAYMENT` transactions. .. versionadded:: 21.8 paid_media (tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid - media bought by the user. + media bought by the user. for + :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` + transactions only. .. versionadded:: 21.5 - paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload. + paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload. Can be + available only for + :tg-const:`telegram.constants.TransactionPartnerUser.PAID_MEDIA_PAYMENT` transactions. .. versionadded:: 21.6 - gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot + gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot; for + :tg-const:`telegram.constants.TransactionPartnerUser.GIFT_PURCHASE` transactions only. .. versionadded:: 21.8 + premium_subscription_duration (:obj:`int`): Optional. Number of months the gifted Telegram + Premium subscription will be active for; for + :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` + transactions only. + + .. versionadded:: NEXT.VERSION """ @@ -339,7 +399,9 @@ class TransactionPartnerUser(TransactionPartner): "invoice_payload", "paid_media", "paid_media_payload", + "premium_subscription_duration", "subscription_period", + "transaction_type", "user", ) @@ -352,11 +414,18 @@ def __init__( subscription_period: Optional[dtm.timedelta] = None, gift: Optional[Gift] = None, affiliate: Optional[AffiliateInfo] = None, + premium_subscription_duration: Optional[int] = None, + # temporarily optional to account for changed signature + transaction_type: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) + # tags: deprecated NEXT.VERSION, bot api 9.0 + if transaction_type is None: + raise TypeError("`transaction_type` is a required argument since Bot API 9.0") + with self._unfrozen(): self.user: User = user self.affiliate: Optional[AffiliateInfo] = affiliate @@ -365,10 +434,13 @@ def __init__( self.paid_media_payload: Optional[str] = paid_media_payload self.subscription_period: Optional[dtm.timedelta] = subscription_period self.gift: Optional[Gift] = gift + self.premium_subscription_duration: Optional[int] = premium_subscription_duration + self.transaction_type: str = transaction_type self._id_attrs = ( self.type, self.user, + self.transaction_type, ) @classmethod diff --git a/telegram/_storyarea.py b/telegram/_storyarea.py new file mode 100644 index 00000000000..1b72587fdd9 --- /dev/null +++ b/telegram/_storyarea.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 story areas.""" + +from typing import Final, Optional + +from telegram import constants +from telegram._reaction import ReactionType +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.types import JSONDict + + +class StoryAreaPosition(TelegramObject): + """Describes the position of a clickable area within a story. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if all of their attributes are equal. + + .. versionadded:: NEXT.VERSION + + Args: + x_percentage (:obj:`float`): The abscissa of the area's center, as a percentage of the + media width. + y_percentage (:obj:`float`): The ordinate of the area's center, as a percentage of the + media height. + width_percentage (:obj:`float`): The width of the area's rectangle, as a percentage of the + media width. + height_percentage (:obj:`float`): The height of the area's rectangle, as a percentage of + the media height. + rotation_angle (:obj:`float`): The clockwise rotation angle of the rectangle, in degrees; + 0-:tg-const:`~telegram.constants.StoryAreaPositionLimit.MAX_ROTATION_ANGLE`. + corner_radius_percentage (:obj:`float`): The radius of the rectangle corner rounding, as a + percentage of the media width. + + Attributes: + x_percentage (:obj:`float`): The abscissa of the area's center, as a percentage of the + media width. + y_percentage (:obj:`float`): The ordinate of the area's center, as a percentage of the + media height. + width_percentage (:obj:`float`): The width of the area's rectangle, as a percentage of the + media width. + height_percentage (:obj:`float`): The height of the area's rectangle, as a percentage of + the media height. + rotation_angle (:obj:`float`): The clockwise rotation angle of the rectangle, in degrees; + 0-:tg-const:`~telegram.constants.StoryAreaPositionLimit.MAX_ROTATION_ANGLE`. + corner_radius_percentage (:obj:`float`): The radius of the rectangle corner rounding, as a + percentage of the media width. + + """ + + __slots__ = ( + "corner_radius_percentage", + "height_percentage", + "rotation_angle", + "width_percentage", + "x_percentage", + "y_percentage", + ) + + def __init__( + self, + x_percentage: float, + y_percentage: float, + width_percentage: float, + height_percentage: float, + rotation_angle: float, + corner_radius_percentage: float, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.x_percentage: float = x_percentage + self.y_percentage: float = y_percentage + self.width_percentage: float = width_percentage + self.height_percentage: float = height_percentage + self.rotation_angle: float = rotation_angle + self.corner_radius_percentage: float = corner_radius_percentage + + self._id_attrs = ( + self.x_percentage, + self.y_percentage, + self.width_percentage, + self.height_percentage, + self.rotation_angle, + self.corner_radius_percentage, + ) + self._freeze() + + +class LocationAddress(TelegramObject): + """Describes the physical address of a location. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`country_code`, :attr:`state`, :attr:`city` and :attr:`street` + are equal. + + .. versionadded:: NEXT.VERSION + + Args: + country_code (:obj:`str`): The two-letter ``ISO 3166-1 alpha-2`` country code of the + country where the location is located. + state (:obj:`str`, optional): State of the location. + city (:obj:`str`, optional): City of the location. + street (:obj:`str`, optional): Street address of the location. + + Attributes: + country_code (:obj:`str`): The two-letter ``ISO 3166-1 alpha-2`` country code of the + country where the location is located. + state (:obj:`str`): Optional. State of the location. + city (:obj:`str`): Optional. City of the location. + street (:obj:`str`): Optional. Street address of the location. + + """ + + __slots__ = ("city", "country_code", "state", "street") + + def __init__( + self, + country_code: str, + state: Optional[str] = None, + city: Optional[str] = None, + street: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.country_code: str = country_code + self.state: Optional[str] = state + self.city: Optional[str] = city + self.street: Optional[str] = street + + self._id_attrs = (self.country_code, self.state, self.city, self.street) + self._freeze() + + +class StoryAreaType(TelegramObject): + """Describes the type of a clickable area on a story. Currently, it can be one of: + + * :class:`telegram.StoryAreaTypeLocation` + * :class:`telegram.StoryAreaTypeSuggestedReaction` + * :class:`telegram.StoryAreaTypeLink` + * :class:`telegram.StoryAreaTypeWeather` + * :class:`telegram.StoryAreaTypeUniqueGift` + + 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 area. + + Attributes: + type (:obj:`str`): Type of the area. + + """ + + __slots__ = ("type",) + + LOCATION: Final[str] = constants.StoryAreaTypeType.LOCATION + """:const:`telegram.constants.StoryAreaTypeType.LOCATION`""" + SUGGESTED_REACTION: Final[str] = constants.StoryAreaTypeType.SUGGESTED_REACTION + """:const:`telegram.constants.StoryAreaTypeType.SUGGESTED_REACTION`""" + LINK: Final[str] = constants.StoryAreaTypeType.LINK + """:const:`telegram.constants.StoryAreaTypeType.LINK`""" + WEATHER: Final[str] = constants.StoryAreaTypeType.WEATHER + """:const:`telegram.constants.StoryAreaTypeType.WEATHER`""" + UNIQUE_GIFT: Final[str] = constants.StoryAreaTypeType.UNIQUE_GIFT + """:const:`telegram.constants.StoryAreaTypeType.UNIQUE_GIFT`""" + + 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.StoryAreaTypeType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + +class StoryAreaTypeLocation(StoryAreaType): + """Describes a story area pointing to a location. Currently, a story can have up to + :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_LOCATION_AREAS` location areas. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`latitude` and :attr:`longitude` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + latitude (:obj:`float`): Location latitude in degrees. + longitude (:obj:`float`): Location longitude in degrees. + address (:class:`telegram.LocationAddress`, optional): Address of the location. + + Attributes: + type (:obj:`str`): Type of the area, always :attr:`~telegram.StoryAreaType.LOCATION`. + latitude (:obj:`float`): Location latitude in degrees. + longitude (:obj:`float`): Location longitude in degrees. + address (:class:`telegram.LocationAddress`): Optional. Address of the location. + + """ + + __slots__ = ("address", "latitude", "longitude") + + def __init__( + self, + latitude: float, + longitude: float, + address: Optional[LocationAddress] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=StoryAreaType.LOCATION, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.latitude: float = latitude + self.longitude: float = longitude + self.address: Optional[LocationAddress] = address + + self._id_attrs = (self.type, self.latitude, self.longitude) + + +class StoryAreaTypeSuggestedReaction(StoryAreaType): + """ + Describes a story area pointing to a suggested reaction. Currently, a story can have up to + :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_SUGGESTED_REACTION_AREAS` + suggested reaction areas. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`reaction_type`, :attr:`is_dark` and :attr:`is_flipped` + are equal. + + .. versionadded:: NEXT.VERSION + + Args: + reaction_type (:class:`ReactionType`): Type of the reaction. + is_dark (:obj:`bool`, optional): Pass :obj:`True` if the reaction area has a dark + background. + is_flipped (:obj:`bool`, optional): Pass :obj:`True` if reaction area corner is flipped. + + Attributes: + type (:obj:`str`): Type of the area, always + :tg-const:`~telegram.StoryAreaType.SUGGESTED_REACTION`. + reaction_type (:class:`ReactionType`): Type of the reaction. + is_dark (:obj:`bool`): Optional. Pass :obj:`True` if the reaction area has a dark + background. + is_flipped (:obj:`bool`): Optional. Pass :obj:`True` if reaction area corner is flipped. + + """ + + __slots__ = ("is_dark", "is_flipped", "reaction_type") + + def __init__( + self, + reaction_type: ReactionType, + is_dark: Optional[bool] = None, + is_flipped: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=StoryAreaType.SUGGESTED_REACTION, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.reaction_type: ReactionType = reaction_type + self.is_dark: Optional[bool] = is_dark + self.is_flipped: Optional[bool] = is_flipped + + self._id_attrs = (self.type, self.reaction_type, self.is_dark, self.is_flipped) + + +class StoryAreaTypeLink(StoryAreaType): + """Describes a story area pointing to an ``HTTP`` or ``tg://`` link. Currently, a story can + have up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_LINK_AREAS` link areas. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`url` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + url (:obj:`str`): ``HTTP`` or ``tg://`` URL to be opened when the area is clicked. + + Attributes: + type (:obj:`str`): Type of the area, always :attr:`~telegram.StoryAreaType.LINK`. + url (:obj:`str`): ``HTTP`` or ``tg://`` URL to be opened when the area is clicked. + + """ + + __slots__ = ("url",) + + def __init__( + self, + url: str, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=StoryAreaType.LINK, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.url: str = url + + self._id_attrs = (self.type, self.url) + + +class StoryAreaTypeWeather(StoryAreaType): + """ + Describes a story area containing weather information. Currently, a story can have up to + :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_WEATHER_AREAS` weather areas. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`temperature`, :attr:`emoji` and + :attr:`background_color` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + temperature (:obj:`float`): Temperature, in degree Celsius. + emoji (:obj:`str`): Emoji representing the weather. + background_color (:obj:`int`): A color of the area background in the ``ARGB`` format. + + Attributes: + type (:obj:`str`): Type of the area, always + :tg-const:`~telegram.StoryAreaType.WEATHER`. + temperature (:obj:`float`): Temperature, in degree Celsius. + emoji (:obj:`str`): Emoji representing the weather. + background_color (:obj:`int`): A color of the area background in the ``ARGB`` format. + + """ + + __slots__ = ("background_color", "emoji", "temperature") + + def __init__( + self, + temperature: float, + emoji: str, + background_color: int, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=StoryAreaType.WEATHER, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.temperature: float = temperature + self.emoji: str = emoji + self.background_color: int = background_color + + self._id_attrs = (self.type, self.temperature, self.emoji, self.background_color) + + +class StoryAreaTypeUniqueGift(StoryAreaType): + """ + Describes a story area pointing to a unique gift. Currently, a story can have at most + :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_UNIQUE_GIFT_AREAS` unique gift area. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + name (:obj:`str`): Unique name of the gift. + + Attributes: + type (:obj:`str`): Type of the area, always + :tg-const:`~telegram.StoryAreaType.UNIQUE_GIFT`. + name (:obj:`str`): Unique name of the gift. + + """ + + __slots__ = ("name",) + + def __init__( + self, + name: str, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=StoryAreaType.UNIQUE_GIFT, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.name: str = name + + self._id_attrs = (self.type, self.name) + + +class StoryArea(TelegramObject): + """Describes a clickable area on a story media. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`position` and :attr:`type` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + position (:class:`telegram.StoryAreaPosition`): Position of the area. + type (:class:`telegram.StoryAreaType`): Type of the area. + + Attributes: + position (:class:`telegram.StoryAreaPosition`): Position of the area. + type (:class:`telegram.StoryAreaType`): Type of the area. + + """ + + __slots__ = ("position", "type") + + def __init__( + self, + position: StoryAreaPosition, + type: StoryAreaType, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.position: StoryAreaPosition = position + self.type: StoryAreaType = type + self._id_attrs = (self.position, self.type) + + self._freeze() diff --git a/telegram/_uniquegift.py b/telegram/_uniquegift.py new file mode 100644 index 00000000000..61926552f3f --- /dev/null +++ b/telegram/_uniquegift.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python +# +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 classes related to unique gifs.""" +from typing import TYPE_CHECKING, Final, Optional + +from telegram import constants +from telegram._files.sticker import Sticker +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.argumentparsing import de_json_optional +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class UniqueGiftModel(TelegramObject): + """This object describes the model of a unique gift. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`name`, :attr:`sticker` and :attr:`rarity_per_mille` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + name (:obj:`str`): Name of the model. + sticker (:class:`telegram.Sticker`): The sticker that represents the unique gift. + rarity_per_mille (:obj:`int`): The number of unique gifts that receive this + model for every ``1000`` gifts upgraded. + + Attributes: + name (:obj:`str`): Name of the model. + sticker (:class:`telegram.Sticker`): The sticker that represents the unique gift. + rarity_per_mille (:obj:`int`): The number of unique gifts that receive this + model for every ``1000`` gifts upgraded. + + """ + + __slots__ = ( + "name", + "rarity_per_mille", + "sticker", + ) + + def __init__( + self, + name: str, + sticker: Sticker, + rarity_per_mille: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.name: str = name + self.sticker: Sticker = sticker + self.rarity_per_mille: int = rarity_per_mille + + self._id_attrs = (self.name, self.sticker, self.rarity_per_mille) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UniqueGiftModel": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) + + return super().de_json(data=data, bot=bot) + + +class UniqueGiftSymbol(TelegramObject): + """This object describes the symbol shown on the pattern of a unique gift. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`name`, :attr:`sticker` and :attr:`rarity_per_mille` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + name (:obj:`str`): Name of the symbol. + sticker (:class:`telegram.Sticker`): The sticker that represents the unique gift. + rarity_per_mille (:obj:`int`): The number of unique gifts that receive this + model for every ``1000`` gifts upgraded. + + Attributes: + name (:obj:`str`): Name of the symbol. + sticker (:class:`telegram.Sticker`): The sticker that represents the unique gift. + rarity_per_mille (:obj:`int`): The number of unique gifts that receive this + model for every ``1000`` gifts upgraded. + + """ + + __slots__ = ( + "name", + "rarity_per_mille", + "sticker", + ) + + def __init__( + self, + name: str, + sticker: Sticker, + rarity_per_mille: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.name: str = name + self.sticker: Sticker = sticker + self.rarity_per_mille: int = rarity_per_mille + + self._id_attrs = (self.name, self.sticker, self.rarity_per_mille) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UniqueGiftSymbol": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) + + return super().de_json(data=data, bot=bot) + + +class UniqueGiftBackdropColors(TelegramObject): + """This object describes the colors of the backdrop of a unique gift. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`center_color`, :attr:`edge_color`, :attr:`symbol_color`, + and :attr:`text_color` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + center_color (:obj:`int`): The color in the center of the backdrop in RGB format. + edge_color (:obj:`int`): The color on the edges of the backdrop in RGB format. + symbol_color (:obj:`int`): The color to be applied to the symbol in RGB format. + text_color (:obj:`int`): The color for the text on the backdrop in RGB format. + + Attributes: + center_color (:obj:`int`): The color in the center of the backdrop in RGB format. + edge_color (:obj:`int`): The color on the edges of the backdrop in RGB format. + symbol_color (:obj:`int`): The color to be applied to the symbol in RGB format. + text_color (:obj:`int`): The color for the text on the backdrop in RGB format. + + """ + + __slots__ = ( + "center_color", + "edge_color", + "symbol_color", + "text_color", + ) + + def __init__( + self, + center_color: int, + edge_color: int, + symbol_color: int, + text_color: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.center_color: int = center_color + self.edge_color: int = edge_color + self.symbol_color: int = symbol_color + self.text_color: int = text_color + + self._id_attrs = (self.center_color, self.edge_color, self.symbol_color, self.text_color) + + self._freeze() + + +class UniqueGiftBackdrop(TelegramObject): + """This object describes the backdrop of a unique gift. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`name`, :attr:`colors`, and :attr:`rarity_per_mille` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + name (:obj:`str`): Name of the backdrop. + colors (:class:`telegram.UniqueGiftBackdropColors`): Colors of the backdrop. + rarity_per_mille (:obj:`int`): The number of unique gifts that receive this backdrop + for every ``1000`` gifts upgraded. + + Attributes: + name (:obj:`str`): Name of the backdrop. + colors (:class:`telegram.UniqueGiftBackdropColors`): Colors of the backdrop. + rarity_per_mille (:obj:`int`): The number of unique gifts that receive this backdrop + for every ``1000`` gifts upgraded. + + """ + + __slots__ = ( + "colors", + "name", + "rarity_per_mille", + ) + + def __init__( + self, + name: str, + colors: UniqueGiftBackdropColors, + rarity_per_mille: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.name: str = name + self.colors: UniqueGiftBackdropColors = colors + self.rarity_per_mille: int = rarity_per_mille + + self._id_attrs = (self.name, self.colors, self.rarity_per_mille) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UniqueGiftBackdrop": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["colors"] = de_json_optional(data.get("colors"), UniqueGiftBackdropColors, bot) + + return super().de_json(data=data, bot=bot) + + +class UniqueGift(TelegramObject): + """This object describes a unique gift that was upgraded from a regular gift. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`base_name`, :attr:`name`, :attr:`number`, :class:`model`, + :attr:`symbol`, and :attr:`backdrop` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + base_name (:obj:`str`): Human-readable name of the regular gift from which this unique + gift was upgraded. + name (:obj:`str`): Unique name of the gift. This name can be used + in ``https://t.me/nft/...`` links and story areas. + number (:obj:`int`): Unique number of the upgraded gift among gifts upgraded from the + same regular gift. + model (:class:`UniqueGiftModel`): Model of the gift. + symbol (:class:`UniqueGiftSymbol`): Symbol of the gift. + backdrop (:class:`UniqueGiftBackdrop`): Backdrop of the gift. + + Attributes: + base_name (:obj:`str`): Human-readable name of the regular gift from which this unique + gift was upgraded. + name (:obj:`str`): Unique name of the gift. This name can be used + in ``https://t.me/nft/...`` links and story areas. + number (:obj:`int`): Unique number of the upgraded gift among gifts upgraded from the + same regular gift. + model (:class:`telegram.UniqueGiftModel`): Model of the gift. + symbol (:class:`telegram.UniqueGiftSymbol`): Symbol of the gift. + backdrop (:class:`telegram.UniqueGiftBackdrop`): Backdrop of the gift. + + """ + + __slots__ = ( + "backdrop", + "base_name", + "model", + "name", + "number", + "symbol", + ) + + def __init__( + self, + base_name: str, + name: str, + number: int, + model: UniqueGiftModel, + symbol: UniqueGiftSymbol, + backdrop: UniqueGiftBackdrop, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.base_name: str = base_name + self.name: str = name + self.number: int = number + self.model: UniqueGiftModel = model + self.symbol: UniqueGiftSymbol = symbol + self.backdrop: UniqueGiftBackdrop = backdrop + + self._id_attrs = ( + self.base_name, + self.name, + self.number, + self.model, + self.symbol, + self.backdrop, + ) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UniqueGift": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["model"] = de_json_optional(data.get("model"), UniqueGiftModel, bot) + data["symbol"] = de_json_optional(data.get("symbol"), UniqueGiftSymbol, bot) + data["backdrop"] = de_json_optional(data.get("backdrop"), UniqueGiftBackdrop, bot) + + return super().de_json(data=data, bot=bot) + + +class UniqueGiftInfo(TelegramObject): + """Describes a service message about a unique gift that was sent or received. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`gift`, and :attr:`origin` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + gift (:class:`UniqueGift`): Information about the gift. + origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE` + or :attr:`TRANSFER`. + owned_gift_id (:obj:`str`, optional) Unique identifier of the received gift for the + bot; only present for gifts received on behalf of business accounts. + transfer_star_count (:obj:`int`, optional): Number of Telegram Stars that must be paid + to transfer the gift; omitted if the bot cannot transfer the gift. + + Attributes: + gift (:class:`UniqueGift`): Information about the gift. + origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE` + or :attr:`TRANSFER`. + owned_gift_id (:obj:`str`) Optional. Unique identifier of the received gift for the + bot; only present for gifts received on behalf of business accounts. + transfer_star_count (:obj:`int`): Optional. Number of Telegram Stars that must be paid + to transfer the gift; omitted if the bot cannot transfer the gift. + + """ + + UPGRADE: Final[str] = constants.UniqueGiftInfoOrigin.UPGRADE + """:const:`telegram.constants.UniqueGiftInfoOrigin.UPGRADE`""" + TRANSFER: Final[str] = constants.UniqueGiftInfoOrigin.TRANSFER + """:const:`telegram.constants.UniqueGiftInfoOrigin.TRANSFER`""" + + __slots__ = ( + "gift", + "origin", + "owned_gift_id", + "transfer_star_count", + ) + + def __init__( + self, + gift: UniqueGift, + origin: str, + owned_gift_id: Optional[str] = None, + transfer_star_count: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.gift: UniqueGift = gift + self.origin: str = enum.get_member(constants.UniqueGiftInfoOrigin, origin, origin) + # Optional + self.owned_gift_id: Optional[str] = owned_gift_id + self.transfer_star_count: Optional[int] = transfer_star_count + + self._id_attrs = (self.gift, self.origin) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UniqueGiftInfo": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot) + + return super().de_json(data=data, bot=bot) diff --git a/telegram/_user.py b/telegram/_user.py index 640a3573acc..63cb9625046 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -1698,6 +1698,46 @@ async def send_gift( api_kwargs=api_kwargs, ) + async def gift_premium_subscription( + self, + month_count: int, + star_count: int, + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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, + ) -> bool: + """Shortcut for:: + + await bot.gift_premium_subscription(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.gift_premium_subscription`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().gift_premium_subscription( + user_id=self.id, + month_count=month_count, + star_count=star_count, + text=text, + text_parse_mode=text_parse_mode, + text_entities=text_entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def send_copy( self, from_chat_id: Union[str, int], diff --git a/telegram/_utils/warnings_transition.py b/telegram/_utils/warnings_transition.py index cd9fecd7562..7aca62c2ac6 100644 --- a/telegram/_utils/warnings_transition.py +++ b/telegram/_utils/warnings_transition.py @@ -35,12 +35,12 @@ def build_deprecation_warning_message( object_type: str, bot_api_version: str, ) -> str: - """Builds a warning message for the transition in API when an object is renamed. + """Builds a warning message for the transition in API when an object is renamed/replaced. Returns a warning message that can be used in `warn` function. """ return ( - f"The {object_type} '{deprecated_name}' was renamed to '{new_name}' in Bot API " + f"The {object_type} '{deprecated_name}' was replaced by '{new_name}' in Bot API " f"{bot_api_version}. We recommend using '{new_name}' instead of " f"'{deprecated_name}'." ) diff --git a/telegram/constants.py b/telegram/constants.py index e9de34abb25..78873a8da19 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -46,6 +46,7 @@ "BotDescriptionLimit", "BotNameLimit", "BulkRequestLimit", + "BusinessLimit", "CallbackQueryLimit", "ChatAction", "ChatBoostSources", @@ -74,6 +75,9 @@ "InlineQueryResultsButtonLimit", "InputMediaType", "InputPaidMediaType", + "InputProfilePhotoType", + "InputStoryContentLimit", + "InputStoryContentType", "InvoiceLimit", "KeyboardButtonRequestUsersLimit", "LocationLimit", @@ -85,11 +89,15 @@ "MessageLimit", "MessageOriginType", "MessageType", + "Nanostar", + "NanostarLimit", + "OwnedGiftType", "PaidMediaType", "ParseMode", "PollLimit", "PollType", "PollingLimit", + "PremiumSubscription", "ProfileAccentColor", "ReactionEmoji", "ReactionType", @@ -101,7 +109,13 @@ "StickerLimit", "StickerSetLimit", "StickerType", + "StoryAreaPositionLimit", + "StoryAreaTypeLimit", + "StoryAreaTypeType", + "StoryLimit", "TransactionPartnerType", + "TransactionPartnerUser", + "UniqueGiftInfoOrigin", "UpdateType", "UserProfilePhotosLimit", "VerifyLimit", @@ -155,7 +169,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=3) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=0) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -702,6 +716,63 @@ class BulkRequestLimit(IntEnum): """:obj:`int`: Maximum number of messages required for bulk actions.""" +class BusinessLimit(IntEnum): + """This enum contains limitations related to handling business accounts. The enum members + of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + CHAT_ACTIVITY_TIMEOUT = int(dtm.timedelta(hours=24).total_seconds()) + """:obj:`int`: Time in seconds in which the chat must have been active for. Relevant for + :paramref:`~telegram.Bot.read_business_message.chat_id` + of :meth:`~telegram.Bot.read_business_message` and + :paramref:`~telegram.Bot.transfer_gift.new_owner_chat_id` + of :meth:`~telegram.Bot.transfer_gift`. + """ + MIN_NAME_LENGTH = 1 + """:obj:`int`: Minimum length of the name of a business account. Relevant only for + :paramref:`~telegram.Bot.set_business_account_name.first_name` of + :meth:`telegram.Bot.set_business_account_name`. + """ + MAX_NAME_LENGTH = 64 + """:obj:`int`: Maximum length of the name of a business account. Relevant for the parameters + of :meth:`telegram.Bot.set_business_account_name`. + """ + MAX_USERNAME_LENGTH = 32 + """::obj:`int`: Maximum length of the username of a business account. Relevant for + :paramref:`~telegram.Bot.set_business_account_username.username` of + :meth:`telegram.Bot.set_business_account_username`. + """ + MAX_BIO_LENGTH = 140 + """:obj:`int`: Maximum length of the bio of a business account. Relevant for + :paramref:`~telegram.Bot.set_business_account_bio.bio` of + :meth:`telegram.Bot.set_business_account_bio`. + """ + MIN_GIFT_RESULTS = 1 + """:obj:`int`: Minimum number of gifts to be returned. Relevant for + :paramref:`~telegram.Bot.get_business_account_gifts.limit` of + :meth:`telegram.Bot.get_business_account_gifts`. + """ + MAX_GIFT_RESULTS = 100 + """:obj:`int`: Maximum number of gifts to be returned. Relevant for + :paramref:`~telegram.Bot.get_business_account_gifts.limit` of + :meth:`telegram.Bot.get_business_account_gifts`. + """ + MIN_STAR_COUNT = 1 + """:obj:`int`: Minimum number of Telegram Stars to be transfered. Relevant for + :paramref:`~telegram.Bot.transfer_business_account_stars.star_count` of + :meth:`telegram.Bot.transfer_business_account_stars`. + """ + MAX_STAR_COUNT = 10000 + """:obj:`int`: Maximum number of Telegram Stars to be transfered. Relevant for + :paramref:`~telegram.Bot.transfer_business_account_stars.star_count` of + :meth:`telegram.Bot.transfer_business_account_stars`. + """ + + class CallbackQueryLimit(IntEnum): """This enum contains limitations for :class:`telegram.CallbackQuery`/ :meth:`telegram.Bot.answer_callback_query`. The enum members of this enumeration are instances @@ -887,8 +958,12 @@ class ChatSubscriptionLimit(IntEnum): """:obj:`int`: The number of seconds the subscription will be active.""" MIN_PRICE = 1 """:obj:`int`: Amount of stars a user pays, minimum amount the subscription can be set to.""" - MAX_PRICE = 2500 - """:obj:`int`: Amount of stars a user pays, maximum amount the subscription can be set to.""" + MAX_PRICE = 10000 + """:obj:`int`: Amount of stars a user pays, maximum amount the subscription can be set to. + + .. versionchanged:: NEXT.VERSION + Bot API 9.0 changed the value to 10000. + """ class BackgroundTypeLimit(IntEnum): @@ -1369,6 +1444,83 @@ class InputPaidMediaType(StringEnum): """:obj:`str`: Type of :class:`telegram.InputMediaVideo`.""" +class InputProfilePhotoType(StringEnum): + """This enum contains the available types of :class:`telegram.InputProfilePhoto`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + STATIC = "static" + """:obj:`str`: Type of :class:`telegram.InputProfilePhotoStatic`.""" + ANIMATED = "animated" + """:obj:`str`: Type of :class:`telegram.InputProfilePhotoAnimated`.""" + + +class InputStoryContentLimit(StringEnum): + """This enum contains limitations for :class:`telegram.InputStoryContentPhoto`/ + :class:`telegram.InputStoryContentVideo`. The enum members of this enumeration are instances + of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + PHOTOSIZE_UPLOAD = FileSizeLimit.PHOTOSIZE_UPLOAD # (10MB) + """:obj:`int`: Maximum file size of the photo to be passed to + :paramref:`~telegram.InputStoryContentPhoto.photo` parameter of + :class:`telegram.InputStoryContentPhoto` in Bytes. + """ + PHOTO_WIDTH = 1080 + """:obj:`int`: Horizontal resolution of the photo to be passed to + :paramref:`~telegram.InputStoryContentPhoto.photo` parameter of + :class:`telegram.InputStoryContentPhoto`. + """ + PHOTO_HEIGHT = 1920 + """:obj:`int`: Vertical resolution of the video to be passed to + :paramref:`~telegram.InputStoryContentPhoto.photo` parameter of + :class:`telegram.InputStoryContentPhoto`. + """ + VIDEOSIZE_UPLOAD = int(30e6) # (30MB) + """:obj:`int`: Maximum file size of the video to be passed to + :paramref:`~telegram.InputStoryContentVideo.video` parameter of + :class:`telegram.InputStoryContentVideo` in Bytes. + """ + VIDEO_WIDTH = 720 + """:obj:`int`: Horizontal resolution of the video to be passed to + :paramref:`~telegram.InputStoryContentVideo.video` parameter of + :class:`telegram.InputStoryContentVideo`. + """ + VIDEO_HEIGHT = 1080 + """:obj:`int`: Vertical resolution of the video to be passed to + :paramref:`~telegram.InputStoryContentVideo.video` parameter of + :class:`telegram.InputStoryContentVideo`. + """ + MAX_VIDEO_DURATION = int(dtm.timedelta(seconds=60).total_seconds()) + """:obj:`int`: Maximum duration of the video to be passed to + :paramref:`~telegram.InputStoryContentVideo.duration` parameter of + :class:`telegram.InputStoryContentVideo`. + """ + + +class InputStoryContentType(StringEnum): + """This enum contains the available types of :class:`telegram.InputStoryContent`. 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.InputStoryContentPhoto`.""" + VIDEO = "video" + """:obj:`str`: Type of :class:`telegram.InputStoryContentVideo`.""" + + 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 @@ -1949,6 +2101,11 @@ class MessageType(StringEnum): .. versionadded:: 20.8 """ + GIFT = "gift" + """:obj:`str`: Messages with :attr:`telegram.Message.gift`. + + .. versionadded:: NEXT.VERSION + """ GIVEAWAY = "giveaway" """:obj:`str`: Messages with :attr:`telegram.Message.giveaway`. @@ -1992,6 +2149,11 @@ class MessageType(StringEnum): .. versionadded:: 21.4 """ + PAID_MESSAGE_PRICE_CHANGED = "paid_message_price_changed" + """:obj:`str`: Messages with :attr:`telegram.Message.paid_message_price_changed`. + + .. versionadded:: Next.VERSION + """ PASSPORT_DATA = "passport_data" """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" PHOTO = "photo" @@ -2032,6 +2194,11 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.successful_payment`.""" TEXT = "text" """:obj:`str`: Messages with :attr:`telegram.Message.text`.""" + UNIQUE_GIFT = "unique_gift" + """:obj:`str`: Messages with :attr:`telegram.Message.unique_gift`. + + .. versionadded:: NEXT.VERSION + """ USERS_SHARED = "users_shared" """:obj:`str`: Messages with :attr:`telegram.Message.users_shared`. @@ -2065,6 +2232,69 @@ class MessageType(StringEnum): """ +class Nanostar(FloatEnum): + """This enum contains constants for ``nanostar_amount`` parameter of + :class:`telegram.StarAmount`, :class:`telegram.StarTransaction` + and :class:`telegram.AffiliateInfo`. + The enum members of this enumeration are instances of :class:`float` and can be treated as + such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + VALUE = 1 / 1000000000 + """:obj:`float`: The value of one nanostar as used in + :paramref:`telegram.StarTransaction.nanostar_amount` + parameter of :class:`telegram.StarTransaction`, + :paramref:`telegram.StarAmount.nanostar_amount` parameter of :class:`telegram.StarAmount` + and :paramref:`telegram.AffiliateInfo.nanostar_amount` + parameter of :class:`telegram.AffiliateInfo` + """ + + +class NanostarLimit(IntEnum): + """This enum contains limitations for ``nanostar_amount`` parameter of + :class:`telegram.AffiliateInfo`, :class:`telegram.StarTransaction` + and :class:`telegram.StarAmount`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MIN_AMOUNT = -999999999 + """:obj:`int`: Minimum value allowed for :paramref:`~telegram.AffiliateInfo.nanostar_amount` + parameter of :class:`telegram.AffiliateInfo` + and :paramref:`~telegram.StarAmount.nanostar_amount` + parameter of :class:`telegram.StarAmount`. + """ + MAX_AMOUNT = 999999999 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.StarTransaction.nanostar_amount` + parameter of :class:`telegram.StarTransaction`, + :paramref:`~telegram.AffiliateInfo.nanostar_amount` parameter of + :class:`telegram.AffiliateInfo` and :paramref:`~telegram.StarAmount.nanostar_amount` + parameter of :class:`telegram.StarAmount`. + """ + + +class OwnedGiftType(StringEnum): + """This enum contains the available types of :class:`telegram.OwnedGift`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + REGULAR = "regular" + """:obj:`str`: a regular owned gift.""" + UNIQUE = "unique" + """:obj:`str`: a unique owned gift.""" + + class PaidMediaType(StringEnum): """ This enum contains the available types of :class:`telegram.PaidMedia`. The enum @@ -2102,6 +2332,58 @@ class PollingLimit(IntEnum): """ +class PremiumSubscription(IntEnum): + """This enum contains limitations for :meth:`~telegram.Bot.gift_premium_subscription`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MAX_TEXT_LENGTH = 128 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.gift_premium_subscription.text` + parameter of :meth:`~telegram.Bot.gift_premium_subscription`. + """ + MONTH_COUNT_THREE = 3 + """:obj:`int`: Possible value for + :paramref:`~telegram.Bot.gift_premium_subscription.month_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`; number of months the Premium + subscription will be active for. + """ + MONTH_COUNT_SIX = 6 + """:obj:`int`: Possible value for + :paramref:`~telegram.Bot.gift_premium_subscription.month_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`; number of months the Premium + subscription will be active for. + """ + MONTH_COUNT_TWELVE = 12 + """:obj:`int`: Possible value for + :paramref:`~telegram.Bot.gift_premium_subscription.month_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`; number of months the Premium + subscription will be active for. + """ + STARS_THREE_MONTHS = 1000 + """:obj:`int`: Number of Telegram Stars to pay for a Premium subscription of + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_THREE` months period. + Relevant for :paramref:`~telegram.Bot.gift_premium_subscription.star_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`. + """ + STARS_SIX_MONTHS = 1500 + """:obj:`int`: Number of Telegram Stars to pay for a Premium subscription of + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_SIX` months period. + Relevant for :paramref:`~telegram.Bot.gift_premium_subscription.star_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`. + """ + STARS_TWELVE_MONTHS = 2500 + """:obj:`int`: Number of Telegram Stars to pay for a Premium subscription of + :tg-const:`telegram.constants.PremiumSubscription.MONTH_COUNT_TWELVE` months period. + Relevant for :paramref:`~telegram.Bot.gift_premium_subscription.star_count` parameter + of :meth:`~telegram.Bot.gift_premium_subscription`. + """ + + class ProfileAccentColor(Enum): """This enum contains the available accent colors for :class:`telegram.ChatFullInfo.profile_accent_color_id`. @@ -2455,19 +2737,27 @@ class RevenueWithdrawalStateType(StringEnum): """:obj:`str`: A withdrawal failed and the transaction was refunded.""" +# tags: deprecated NEXT.VERSION, bot api 9.0 class StarTransactions(FloatEnum): """This enum contains constants for :class:`telegram.StarTransaction`. The enum members of this enumeration are instances of :class:`float` and can be treated as such. .. versionadded:: 21.9 + + .. deprecated:: NEXT.VERSION + This class will be removed as its only member :attr:`NANOSTAR_VALUE` will be replaced + by :attr:`telegram.constants.Nanostar.VALUE`. """ __slots__ = () - NANOSTAR_VALUE = 1 / 1000000000 + NANOSTAR_VALUE = Nanostar.VALUE """:obj:`float`: The value of one nanostar as used in :attr:`telegram.StarTransaction.nanostar_amount`. + + .. deprecated:: NEXT.VERSION + This member will be replaced by :attr:`telegram.constants.Nanostar.VALUE`. """ @@ -2489,19 +2779,27 @@ class StarTransactionsLimit(IntEnum): """:obj:`int`: Maximum value allowed for the :paramref:`~telegram.Bot.get_star_transactions.limit` parameter of :meth:`telegram.Bot.get_star_transactions`.""" - NANOSTAR_MIN_AMOUNT = -999999999 + # tags: deprecated NEXT.VERSION, bot api 9.0 + NANOSTAR_MIN_AMOUNT = NanostarLimit.MIN_AMOUNT """:obj:`int`: Minimum value allowed for :paramref:`~telegram.AffiliateInfo.nanostar_amount` parameter of :class:`telegram.AffiliateInfo`. .. versionadded:: 21.9 + + .. deprecated:: NEXT.VERSION + This member will be replaced by :attr:`telegram.constants.NanostarLimit.MIN_AMOUNT`. """ - NANOSTAR_MAX_AMOUNT = 999999999 + # tags: deprecated NEXT.VERSION, bot api 9.0 + NANOSTAR_MAX_AMOUNT = NanostarLimit.MAX_AMOUNT """:obj:`int`: Maximum value allowed for :paramref:`~telegram.StarTransaction.nanostar_amount` parameter of :class:`telegram.StarTransaction` and :paramref:`~telegram.AffiliateInfo.nanostar_amount` parameter of :class:`telegram.AffiliateInfo`. .. versionadded:: 21.9 + + .. deprecated:: NEXT.VERSION + This member will be replaced by :attr:`telegram.constants.NanostarLimit.MAX_AMOUNT`. """ @@ -2638,6 +2936,98 @@ class StickerType(StringEnum): """:obj:`str`: Custom emoji sticker.""" +class StoryAreaPositionLimit(IntEnum): + """This enum contains limitations for :class:`telegram.StoryAreaPosition`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MAX_ROTATION_ANGLE = 360 + """:obj:`int`: Maximum value allowed for: + :paramref:`~telegram.StoryAreaPosition.rotation_angle` parameter of + :class:`telegram.StoryAreaPosition` + """ + + +class StoryAreaTypeLimit(IntEnum): + """This enum contains limitations for subclasses of :class:`telegram.StoryAreaType`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MAX_LOCATION_AREAS = 10 + """:obj:`int`: Maximum number of location areas that a story can have. + """ + MAX_SUGGESTED_REACTION_AREAS = 5 + """:obj:`int`: Maximum number of suggested reaction areas that a story can have. + """ + MAX_LINK_AREAS = 3 + """:obj:`int`: Maximum number of link areas that a story can have. + """ + MAX_WEATHER_AREAS = 3 + """:obj:`int`: Maximum number of weather areas that a story can have. + """ + MAX_UNIQUE_GIFT_AREAS = 1 + """:obj:`int`: Maximum number of unique gift areas that a story can have. + """ + + +class StoryAreaTypeType(StringEnum): + """This enum contains the available types of :class:`telegram.StoryAreaType`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + LOCATION = "location" + """:obj:`str`: Type of :class:`telegram.StoryAreaTypeLocation`.""" + SUGGESTED_REACTION = "suggested_reaction" + """:obj:`str`: Type of :class:`telegram.StoryAreaTypeSuggestedReaction`.""" + LINK = "link" + """:obj:`str`: Type of :class:`telegram.StoryAreaTypeLink`.""" + WEATHER = "weather" + """:obj:`str`: Type of :class:`telegram.StoryAreaTypeWeather`.""" + UNIQUE_GIFT = "unique_gift" + """:obj:`str`: Type of :class:`telegram.StoryAreaTypeUniqueGift`.""" + + +class StoryLimit(StringEnum): + """This enum contains limitations for :meth:`~telegram.Bot.post_story` and + :meth:`~telegram.Bot.edit_story`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + CAPTION_LENGTH = 2048 + """:obj:`int`: Maximum number of characters in :paramref:`telegram.Bot.post_story.caption` + parameter of :meth:`telegram.Bot.post_story` and :paramref:`telegram.Bot.edit_story.caption` of + :meth:`telegram.Bot.edit_story`. + """ + ACTIVITY_SIX_HOURS = 6 * 3600 + """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.caption`` parameter of + :meth:`telegram.Bot.post_story`.""" + ACTIVITY_TWELVE_HOURS = 12 * 3600 + """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.caption`` parameter of + :meth:`telegram.Bot.post_story`.""" + ACTIVITY_ONE_DAY = 86400 + """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.caption`` parameter of + :meth:`telegram.Bot.post_story`.""" + ACTIVITY_TWO_DAYS = 2 * 86400 + """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.caption`` parameter of + :meth:`telegram.Bot.post_story`.""" + + 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. @@ -2673,6 +3063,38 @@ class TransactionPartnerType(StringEnum): """:obj:`str`: Transaction with a user.""" +class TransactionPartnerUser(StringEnum): + """This enum contains constants for :class:`telegram.TransactionPartnerUser`. + The enum members of this enumeration are instances of :class:`str` and can be treated as + such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + INVOICE_PAYMENT = "invoice_payment" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + PAID_MEDIA_PAYMENT = "paid_media_payment" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + GIFT_PURCHASE = "gift_purchase" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + PREMIUM_PURCHASE = "premium_purchase" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + BUSINESS_ACCOUNT_TRANSFER = "business_account_transfer" + """:obj:`str`: Possible value for + :paramref:`telegram.TransactionPartnerUser.transaction_type`. + """ + + 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. @@ -2775,6 +3197,21 @@ class PollType(StringEnum): """:obj:`str`: quiz polls.""" +class UniqueGiftInfoOrigin(StringEnum): + """This enum contains the available origins for :class:`telegram.UniqueGiftInfo`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + UPGRADE = "upgrade" + """:obj:`str` gift upgraded""" + TRANSFER = "transfer" + """:obj:`str` gift transfered""" + + class UpdateType(StringEnum): """This enum contains the available types of :class:`telegram.Update`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. @@ -2946,12 +3383,14 @@ class InvoiceLimit(IntEnum): .. versionadded:: 21.6 """ - MAX_STAR_COUNT = 2500 + MAX_STAR_COUNT = 10000 """:obj:`int`: Maximum amount of starts that must be paid to buy access to a paid media passed as :paramref:`~telegram.Bot.send_paid_media.star_count` parameter of :meth:`telegram.Bot.send_paid_media`. .. versionadded:: 21.6 + .. versionchanged:: NEXT.VERSION + Bot API 9.0 changed the value to 10000. """ SUBSCRIPTION_PERIOD = dtm.timedelta(days=30).total_seconds() """:obj:`int`: The period of time for which the subscription is active before @@ -2960,11 +3399,13 @@ class InvoiceLimit(IntEnum): .. versionadded:: 21.8 """ - SUBSCRIPTION_MAX_PRICE = 2500 + SUBSCRIPTION_MAX_PRICE = 10000 """:obj:`int`: The maximum price of a subscription created wtih :meth:`telegram.Bot.create_invoice_link`. .. versionadded:: 21.9 + .. versionchanged:: NEXT.VERSION + Bot API 9.0 changed the value to 10000. """ diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 15b969fd85d..7afadaa89fa 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -36,6 +36,7 @@ from uuid import uuid4 from telegram import ( + AcceptedGiftTypes, Animation, Audio, Bot, @@ -64,21 +65,25 @@ InputMedia, InputPaidMedia, InputPollOption, + InputProfilePhoto, LinkPreviewOptions, Location, MaskPosition, MenuButton, Message, MessageId, + OwnedGifts, PhotoSize, Poll, PreparedInlineMessage, ReactionType, ReplyParameters, SentWebAppMessage, + StarAmount, StarTransactions, Sticker, StickerSet, + Story, TelegramObject, Update, User, @@ -116,10 +121,12 @@ InputMediaPhoto, InputMediaVideo, InputSticker, + InputStoryContent, LabeledPrice, MessageEntity, PassportElementError, ShippingOption, + StoryArea, ) from telegram.ext import BaseRateLimiter, Defaults @@ -4246,6 +4253,36 @@ async def set_message_reaction( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def gift_premium_subscription( + self, + user_id: int, + month_count: int, + star_count: int, + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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, + ) -> bool: + return await super().gift_premium_subscription( + user_id=user_id, + month_count=month_count, + star_count=star_count, + text=text, + text_parse_mode=text_parse_mode, + text_entities=text_entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def get_business_connection( self, business_connection_id: str, @@ -4266,6 +4303,432 @@ async def get_business_connection( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_business_account_gifts( + self, + business_connection_id: str, + exclude_unsaved: Optional[bool] = None, + exclude_saved: Optional[bool] = None, + exclude_unlimited: Optional[bool] = None, + exclude_limited: Optional[bool] = None, + exclude_unique: Optional[bool] = None, + sort_by_price: Optional[bool] = None, + offset: Optional[str] = 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, + ) -> OwnedGifts: + return await super().get_business_account_gifts( + business_connection_id=business_connection_id, + exclude_unsaved=exclude_unsaved, + exclude_saved=exclude_saved, + exclude_unlimited=exclude_unlimited, + exclude_limited=exclude_limited, + exclude_unique=exclude_unique, + sort_by_price=sort_by_price, + 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), + ) + + async def get_business_account_star_balance( + self, + business_connection_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> StarAmount: + return await super().get_business_account_star_balance( + business_connection_id=business_connection_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), + ) + + async def read_business_message( + self, + business_connection_id: str, + chat_id: int, + message_id: int, + *, + 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, + ) -> bool: + return await super().read_business_message( + business_connection_id=business_connection_id, + chat_id=chat_id, + message_id=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), + ) + + async def delete_business_messages( + self, + business_connection_id: str, + message_ids: Sequence[int], + *, + 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, + ) -> bool: + return await super().delete_business_messages( + business_connection_id=business_connection_id, + message_ids=message_ids, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def post_story( + self, + business_connection_id: str, + content: "InputStoryContent", + active_period: TimePeriod, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + areas: Optional[Sequence["StoryArea"]] = None, + post_to_chat_page: Optional[bool] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> Story: + return await super().post_story( + business_connection_id=business_connection_id, + content=content, + active_period=active_period, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + areas=areas, + post_to_chat_page=post_to_chat_page, + protect_content=protect_content, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def edit_story( + self, + business_connection_id: str, + story_id: int, + content: "InputStoryContent", + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + areas: Optional[Sequence["StoryArea"]] = 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, + ) -> Story: + return await super().edit_story( + business_connection_id=business_connection_id, + story_id=story_id, + content=content, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + areas=areas, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def delete_story( + self, + business_connection_id: str, + story_id: int, + *, + 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, + ) -> bool: + return await super().delete_story( + business_connection_id=business_connection_id, + story_id=story_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), + ) + + async def set_business_account_name( + self, + business_connection_id: str, + first_name: str, + last_name: Optional[str] = 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, + ) -> bool: + return await super().set_business_account_name( + business_connection_id=business_connection_id, + first_name=first_name, + last_name=last_name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def set_business_account_username( + self, + business_connection_id: str, + username: Optional[str] = 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, + ) -> bool: + return await super().set_business_account_username( + business_connection_id=business_connection_id, + username=username, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def set_business_account_bio( + self, + business_connection_id: str, + bio: Optional[str] = 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, + ) -> bool: + return await super().set_business_account_bio( + business_connection_id=business_connection_id, + bio=bio, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def set_business_account_gift_settings( + self, + business_connection_id: str, + show_gift_button: bool, + accepted_gift_types: AcceptedGiftTypes, + *, + 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, + ) -> bool: + return await super().set_business_account_gift_settings( + business_connection_id=business_connection_id, + show_gift_button=show_gift_button, + accepted_gift_types=accepted_gift_types, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def set_business_account_profile_photo( + self, + business_connection_id: str, + photo: "InputProfilePhoto", + is_public: Optional[bool] = 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, + ) -> bool: + return await super().set_business_account_profile_photo( + business_connection_id=business_connection_id, + photo=photo, + is_public=is_public, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def remove_business_account_profile_photo( + self, + business_connection_id: str, + is_public: Optional[bool] = 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, + ) -> bool: + return await super().remove_business_account_profile_photo( + business_connection_id=business_connection_id, + is_public=is_public, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def convert_gift_to_stars( + self, + business_connection_id: str, + owned_gift_id: str, + *, + 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, + ) -> bool: + return await super().convert_gift_to_stars( + business_connection_id=business_connection_id, + owned_gift_id=owned_gift_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), + ) + + async def upgrade_gift( + self, + business_connection_id: str, + owned_gift_id: str, + keep_original_details: Optional[bool] = None, + star_count: 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, + ) -> bool: + return await super().upgrade_gift( + business_connection_id=business_connection_id, + owned_gift_id=owned_gift_id, + keep_original_details=keep_original_details, + star_count=star_count, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def transfer_gift( + self, + business_connection_id: str, + owned_gift_id: str, + new_owner_chat_id: int, + star_count: 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, + ) -> bool: + return await super().transfer_gift( + business_connection_id=business_connection_id, + owned_gift_id=owned_gift_id, + new_owner_chat_id=new_owner_chat_id, + star_count=star_count, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def transfer_business_account_stars( + self, + business_connection_id: str, + star_count: int, + *, + 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, + ) -> bool: + return await super().transfer_business_account_stars( + business_connection_id=business_connection_id, + star_count=star_count, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def replace_sticker_in_set( self, user_id: int, @@ -4715,7 +5178,25 @@ async def remove_user_verification( unpinAllGeneralForumTopicMessages = unpin_all_general_forum_topic_messages getUserChatBoosts = get_user_chat_boosts setMessageReaction = set_message_reaction + giftPremiumSubscription = gift_premium_subscription getBusinessConnection = get_business_connection + getBusinessAccountGifts = get_business_account_gifts + getBusinessAccountStarBalance = get_business_account_star_balance + readBusinessMessage = read_business_message + deleteBusinessMessages = delete_business_messages + postStory = post_story + editStory = edit_story + deleteStory = delete_story + setBusinessAccountName = set_business_account_name + setBusinessAccountUsername = set_business_account_username + setBusinessAccountBio = set_business_account_bio + setBusinessAccountGiftSettings = set_business_account_gift_settings + setBusinessAccountProfilePhoto = set_business_account_profile_photo + removeBusinessAccountProfilePhoto = remove_business_account_profile_photo + convertGiftToStars = convert_gift_to_stars + upgradeGift = upgrade_gift + transferGift = transfer_gift + transferBusinessAccountStars = transfer_business_account_stars replaceStickerInSet = replace_sticker_in_set refundStarPayment = refund_star_payment getStarTransactions = get_star_transactions diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index a1e8030f7b4..323c0e0f646 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1926,6 +1926,7 @@ def filter(self, update: Update) -> bool: or StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) or StatusUpdate.GENERAL_FORUM_TOPIC_HIDDEN.check_update(update) or StatusUpdate.GENERAL_FORUM_TOPIC_UNHIDDEN.check_update(update) + or StatusUpdate.GIFT.check_update(update) or StatusUpdate.GIVEAWAY_COMPLETED.check_update(update) or StatusUpdate.GIVEAWAY_CREATED.check_update(update) or StatusUpdate.LEFT_CHAT_MEMBER.check_update(update) @@ -1934,9 +1935,11 @@ def filter(self, update: Update) -> bool: or StatusUpdate.NEW_CHAT_MEMBERS.check_update(update) or StatusUpdate.NEW_CHAT_PHOTO.check_update(update) or StatusUpdate.NEW_CHAT_TITLE.check_update(update) + or StatusUpdate.PAID_MESSAGE_PRICE_CHANGED.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.UNIQUE_GIFT.check_update(update) or StatusUpdate.USERS_SHARED.check_update(update) or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update) or StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update) @@ -2079,6 +2082,18 @@ def filter(self, message: Message) -> bool: .. versionadded:: 20.0 """ + class _Gift(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.gift) + + GIFT = _Gift(name="filters.StatusUpdate.GIFT") + """Messages that contain :attr:`telegram.Message.gift`. + + .. versionadded:: NEXT.VERSION + """ + class _GiveawayCreated(MessageFilter): __slots__ = () @@ -2162,6 +2177,20 @@ def filter(self, message: Message) -> bool: NEW_CHAT_TITLE = _NewChatTitle(name="filters.StatusUpdate.NEW_CHAT_TITLE") """Messages that contain :attr:`telegram.Message.new_chat_title`.""" + class _PaidMessagePriceChanged(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.paid_message_price_changed) + + PAID_MESSAGE_PRICE_CHANGED = _PaidMessagePriceChanged( + name="filters.StatusUpdate.PAID_MESSAGE_PRICE_CHANGED" + ) + """Messages that contain :attr:`telegram.Message.paid_message_price_changed`. + + .. versionadded:: NEXT.VERSION + """ + class _PinnedMessage(MessageFilter): __slots__ = () @@ -2193,6 +2222,18 @@ def filter(self, message: Message) -> bool: .. versionadded:: 21.4 """ + class _UniqueGift(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.unique_gift) + + UNIQUE_GIFT = _UniqueGift(name="filters.StatusUpdate.UNIQUE_GIFT") + """Messages that contain :attr:`telegram.Message.unique_gift`. + + .. versionadded:: NEXT.VERSION + """ + class _UsersShared(MessageFilter): __slots__ = () diff --git a/telegram/request/_requestparameter.py b/telegram/request/_requestparameter.py index 89796713772..f0664c7943d 100644 --- a/telegram/request/_requestparameter.py +++ b/telegram/request/_requestparameter.py @@ -23,8 +23,10 @@ from dataclasses import dataclass from typing import Optional, final +from telegram._files._inputstorycontent import InputStoryContent from telegram._files.inputfile import InputFile from telegram._files.inputmedia import InputMedia, InputPaidMedia +from telegram._files.inputprofilephoto import InputProfilePhoto, InputProfilePhotoStatic from telegram._files.inputsticker import InputSticker from telegram._telegramobject import TelegramObject from telegram._utils.datetime import to_timestamp @@ -148,6 +150,31 @@ def _value_and_input_files_from_input( # pylint: disable=too-many-return-statem return data, [value.media, thumbnail] return data, [value.media] + + if isinstance(value, InputProfilePhoto): + attr = "photo" if isinstance(value, InputProfilePhotoStatic) else "animation" + if not isinstance(media := getattr(value, attr), InputFile): + # We don't have to upload anything + return value.to_dict(), [] + + # We call to_dict and change the returned dict instead of overriding + # value.photo in case the same value is reused for another request + data = value.to_dict() + data[attr] = media.attach_uri + return data, [media] + + if isinstance(value, InputStoryContent): + attr = value.type + if not isinstance(media := getattr(value, attr), InputFile): + # We don't have to upload anything + return value.to_dict(), [] + + # We call to_dict and change the returned dict instead of overriding + # value.photo in case the same value is reused for another request + data = value.to_dict() + data[attr] = media.attach_uri + return data, [media] + if isinstance(value, InputSticker) and isinstance(value.sticker, InputFile): # We call to_dict and change the returned dict instead of overriding # value.sticker in case the same value is reused for another request diff --git a/tests/_files/test_inputprofilephoto.py b/tests/_files/test_inputprofilephoto.py new file mode 100644 index 00000000000..363bf5a9fd2 --- /dev/null +++ b/tests/_files/test_inputprofilephoto.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 as dtm + +import pytest + +from telegram import ( + InputFile, + InputProfilePhoto, + InputProfilePhotoAnimated, + InputProfilePhotoStatic, +) +from telegram.constants import InputProfilePhotoType +from tests.auxil.files import data_file +from tests.auxil.slots import mro_slots + + +class TestInputProfilePhotoWithoutRequest: + + def test_type_enum_conversion(self): + instance = InputProfilePhoto(type="static") + assert isinstance(instance.type, InputProfilePhotoType) + assert instance.type is InputProfilePhotoType.STATIC + + instance = InputProfilePhoto(type="animated") + assert isinstance(instance.type, InputProfilePhotoType) + assert instance.type is InputProfilePhotoType.ANIMATED + + instance = InputProfilePhoto(type="unknown") + assert isinstance(instance.type, str) + assert instance.type == "unknown" + + +@pytest.fixture(scope="module") +def input_profile_photo_static(): + return InputProfilePhotoStatic(photo=InputProfilePhotoStaticTestBase.photo.read_bytes()) + + +class InputProfilePhotoStaticTestBase: + type_ = "static" + photo = data_file("telegram.jpg") + + +class TestInputProfilePhotoStaticWithoutRequest(InputProfilePhotoStaticTestBase): + def test_slot_behaviour(self, input_profile_photo_static): + inst = input_profile_photo_static + 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_profile_photo_static): + inst = input_profile_photo_static + assert inst.type == self.type_ + assert isinstance(inst.photo, InputFile) + + def test_to_dict(self, input_profile_photo_static): + inst = input_profile_photo_static + data = inst.to_dict() + assert data["type"] == self.type_ + assert data["photo"] == inst.photo + + def test_with_local_file(self): + inst = InputProfilePhotoStatic(photo=data_file("telegram.jpg")) + assert inst.photo == data_file("telegram.jpg").as_uri() + + def test_type_enum_conversion(self, input_profile_photo_static): + assert input_profile_photo_static.type is InputProfilePhotoType.STATIC + + +@pytest.fixture(scope="module") +def input_profile_photo_animated(): + return InputProfilePhotoAnimated( + animation=InputProfilePhotoAnimatedTestBase.animation.read_bytes(), + main_frame_timestamp=InputProfilePhotoAnimatedTestBase.main_frame_timestamp, + ) + + +class InputProfilePhotoAnimatedTestBase: + type_ = "animated" + animation = data_file("telegram2.mp4") + main_frame_timestamp = dtm.timedelta(seconds=42, milliseconds=43) + + +class TestInputProfilePhotoAnimatedWithoutRequest(InputProfilePhotoAnimatedTestBase): + def test_slot_behaviour(self, input_profile_photo_animated): + inst = input_profile_photo_animated + 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_profile_photo_animated): + inst = input_profile_photo_animated + assert inst.type == self.type_ + assert isinstance(inst.animation, InputFile) + assert inst.main_frame_timestamp == self.main_frame_timestamp + + def test_to_dict(self, input_profile_photo_animated): + inst = input_profile_photo_animated + data = inst.to_dict() + assert data["type"] == self.type_ + assert data["animation"] == inst.animation + assert data["main_frame_timestamp"] == self.main_frame_timestamp.total_seconds() + + def test_with_local_file(self): + inst = InputProfilePhotoAnimated( + animation=data_file("telegram2.mp4"), + main_frame_timestamp=self.main_frame_timestamp, + ) + assert inst.animation == data_file("telegram2.mp4").as_uri() + + def test_type_enum_conversion(self, input_profile_photo_animated): + assert input_profile_photo_animated.type is InputProfilePhotoType.ANIMATED + + @pytest.mark.parametrize( + "timestamp", + [ + dtm.timedelta(days=2), + dtm.timedelta(seconds=2 * 24 * 60 * 60), + 2 * 24 * 60 * 60, + float(2 * 24 * 60 * 60), + ], + ) + def test_main_frame_timestamp_conversion(self, timestamp): + inst = InputProfilePhotoAnimated( + animation=self.animation, + main_frame_timestamp=timestamp, + ) + assert isinstance(inst.main_frame_timestamp, dtm.timedelta) + assert inst.main_frame_timestamp == dtm.timedelta(days=2) + + assert ( + InputProfilePhotoAnimated( + animation=self.animation, + main_frame_timestamp=None, + ).main_frame_timestamp + is None + ) diff --git a/tests/_files/test_inputstorycontent.py b/tests/_files/test_inputstorycontent.py new file mode 100644 index 00000000000..9e826409584 --- /dev/null +++ b/tests/_files/test_inputstorycontent.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import datetime as dtm + +import pytest + +from telegram import InputFile, InputStoryContent, InputStoryContentPhoto, InputStoryContentVideo +from telegram.constants import InputStoryContentType +from tests.auxil.files import data_file +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def input_story_content(): + return InputStoryContent( + type=InputStoryContentTestBase.type, + ) + + +class InputStoryContentTestBase: + type = InputStoryContent.PHOTO + + +class TestInputStoryContent(InputStoryContentTestBase): + def test_slot_behaviour(self, input_story_content): + inst = input_story_content + 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_type_enum_conversion(self): + assert type(InputStoryContent(type="video").type) is InputStoryContentType + assert InputStoryContent(type="unknown").type == "unknown" + + +@pytest.fixture(scope="module") +def input_story_content_photo(): + return InputStoryContentPhoto(photo=InputStoryContentPhotoTestBase.photo.read_bytes()) + + +class InputStoryContentPhotoTestBase: + type = InputStoryContentType.PHOTO + photo = data_file("telegram.jpg") + + +class TestInputStoryContentPhotoWithoutRequest(InputStoryContentPhotoTestBase): + + def test_slot_behaviour(self, input_story_content_photo): + inst = input_story_content_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_story_content_photo): + inst = input_story_content_photo + assert inst.type is self.type + assert isinstance(inst.photo, InputFile) + + def test_to_dict(self, input_story_content_photo): + inst = input_story_content_photo + json_dict = inst.to_dict() + assert json_dict["type"] is self.type + assert json_dict["photo"] == inst.photo + + def test_with_photo_file(self, photo_file): + inst = InputStoryContentPhoto(photo=photo_file) + assert inst.type is self.type + assert isinstance(inst.photo, InputFile) + + def test_with_local_files(self): + inst = InputStoryContentPhoto(photo=data_file("telegram.jpg")) + assert inst.photo == data_file("telegram.jpg").as_uri() + + +@pytest.fixture(scope="module") +def input_story_content_video(): + return InputStoryContentVideo( + video=InputStoryContentVideoTestBase.video.read_bytes(), + duration=InputStoryContentVideoTestBase.duration, + cover_frame_timestamp=InputStoryContentVideoTestBase.cover_frame_timestamp, + is_animation=InputStoryContentVideoTestBase.is_animation, + ) + + +class InputStoryContentVideoTestBase: + type = InputStoryContentType.VIDEO + video = data_file("telegram.mp4") + duration = dtm.timedelta(seconds=30) + cover_frame_timestamp = dtm.timedelta(seconds=15) + is_animation = False + + +class TestInputMediaVideoWithoutRequest(InputStoryContentVideoTestBase): + def test_slot_behaviour(self, input_story_content_video): + inst = input_story_content_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_story_content_video): + inst = input_story_content_video + assert inst.type is self.type + assert isinstance(inst.video, InputFile) + assert inst.duration == self.duration + assert inst.cover_frame_timestamp == self.cover_frame_timestamp + assert inst.is_animation is self.is_animation + + def test_to_dict(self, input_story_content_video): + inst = input_story_content_video + json_dict = inst.to_dict() + assert json_dict["type"] is self.type + assert json_dict["video"] == inst.video + assert json_dict["duration"] == self.duration.total_seconds() + assert json_dict["cover_frame_timestamp"] == self.cover_frame_timestamp.total_seconds() + assert json_dict["is_animation"] is self.is_animation + + def test_with_video_file(self, video_file): + inst = InputStoryContentVideo(video=video_file) + assert inst.type is self.type + assert isinstance(inst.video, InputFile) + + def test_with_local_files(self): + inst = InputStoryContentVideo(video=data_file("telegram.mp4")) + assert inst.video == data_file("telegram.mp4").as_uri() + + @pytest.mark.parametrize("timestamp", [dtm.timedelta(seconds=60), 60, float(60)]) + @pytest.mark.parametrize("field", ["duration", "cover_frame_timestamp"]) + def test_time_period_arg_conversion(self, field, timestamp): + inst = InputStoryContentVideo( + video=self.video, + **{field: timestamp}, + ) + value = getattr(inst, field) + assert isinstance(value, dtm.timedelta) + assert value == dtm.timedelta(seconds=60) + + inst = InputStoryContentVideo( + video=self.video, + **{field: None}, + ) + value = getattr(inst, field) + assert value is None diff --git a/tests/_payment/stars/test_staramount.py b/tests/_payment/stars/test_staramount.py new file mode 100644 index 00000000000..f0438910b00 --- /dev/null +++ b/tests/_payment/stars/test_staramount.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 StarAmount +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def star_amount(): + return StarAmount( + amount=StarTransactionTestBase.amount, + nanostar_amount=StarTransactionTestBase.nanostar_amount, + ) + + +class StarTransactionTestBase: + amount = 100 + nanostar_amount = 356 + + +class TestStarAmountWithoutRequest(StarTransactionTestBase): + def test_slot_behaviour(self, star_amount): + inst = star_amount + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "amount": self.amount, + "nanostar_amount": self.nanostar_amount, + } + st = StarAmount.de_json(json_dict, offline_bot) + assert st.api_kwargs == {} + assert st.amount == self.amount + assert st.nanostar_amount == self.nanostar_amount + + def test_to_dict(self, star_amount): + expected_dict = { + "amount": self.amount, + "nanostar_amount": self.nanostar_amount, + } + assert star_amount.to_dict() == expected_dict + + def test_equality(self, star_amount): + a = star_amount + b = StarAmount(amount=self.amount, nanostar_amount=self.nanostar_amount) + c = StarAmount(amount=99, nanostar_amount=99) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) diff --git a/tests/_payment/stars/test_startransactions.py b/tests/_payment/stars/test_startransactions.py index 0878e8cbede..f90361e2b99 100644 --- a/tests/_payment/stars/test_startransactions.py +++ b/tests/_payment/stars/test_startransactions.py @@ -63,6 +63,7 @@ class StarTransactionTestBase: nanostar_amount = 365 date = to_timestamp(dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)) source = TransactionPartnerUser( + transaction_type="premium_purchase", user=User( id=2, is_bot=False, @@ -144,6 +145,7 @@ def test_equality(self): amount=3, date=to_timestamp(dtm.datetime.utcnow()), source=TransactionPartnerUser( + transaction_type="other_type", user=User( id=3, is_bot=False, diff --git a/tests/_payment/stars/test_transactionpartner.py b/tests/_payment/stars/test_transactionpartner.py index 3f795b93ca2..f89568901a6 100644 --- a/tests/_payment/stars/test_transactionpartner.py +++ b/tests/_payment/stars/test_transactionpartner.py @@ -63,6 +63,7 @@ class TransactionPartnerTestBase: first_name="user", last_name="user", ) + transaction_type = "premium_purchase" invoice_payload = "invoice_payload" paid_media = ( PaidMediaVideo( @@ -101,6 +102,7 @@ class TransactionPartnerTestBase: id=3, type=Chat.CHANNEL, ) + premium_subscription_duration = 3 class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase): @@ -137,6 +139,7 @@ def test_subclass(self, offline_bot, tp_type, subclass): "type": tp_type, "commission_per_mille": self.commission_per_mille, "user": self.user.to_dict(), + "transaction_type": self.transaction_type, "request_count": self.request_count, } tp = TransactionPartner.de_json(json_dict, offline_bot) @@ -268,11 +271,13 @@ def test_equality(self, transaction_partner_fragment): @pytest.fixture def transaction_partner_user(): return TransactionPartnerUser( + transaction_type=TransactionPartnerTestBase.transaction_type, user=TransactionPartnerTestBase.user, invoice_payload=TransactionPartnerTestBase.invoice_payload, paid_media=TransactionPartnerTestBase.paid_media, paid_media_payload=TransactionPartnerTestBase.paid_media_payload, subscription_period=TransactionPartnerTestBase.subscription_period, + premium_subscription_duration=TransactionPartnerTestBase.premium_subscription_duration, ) @@ -288,36 +293,48 @@ def test_slot_behaviour(self, transaction_partner_user): def test_de_json(self, offline_bot): json_dict = { "user": self.user.to_dict(), + "transaction_type": self.transaction_type, "invoice_payload": self.invoice_payload, "paid_media": [pm.to_dict() for pm in self.paid_media], "paid_media_payload": self.paid_media_payload, "subscription_period": self.subscription_period.total_seconds(), + "premium_subscription_duration": self.premium_subscription_duration, } tp = TransactionPartnerUser.de_json(json_dict, offline_bot) assert tp.api_kwargs == {} assert tp.type == "user" assert tp.user == self.user + assert tp.transaction_type == self.transaction_type assert tp.invoice_payload == self.invoice_payload assert tp.paid_media == self.paid_media assert tp.paid_media_payload == self.paid_media_payload assert tp.subscription_period == self.subscription_period + assert tp.premium_subscription_duration == self.premium_subscription_duration def test_to_dict(self, transaction_partner_user): json_dict = transaction_partner_user.to_dict() assert json_dict["type"] == self.type + assert json_dict["transaction_type"] == self.transaction_type assert json_dict["user"] == self.user.to_dict() assert json_dict["invoice_payload"] == self.invoice_payload assert json_dict["paid_media"] == [pm.to_dict() for pm in self.paid_media] assert json_dict["paid_media_payload"] == self.paid_media_payload assert json_dict["subscription_period"] == self.subscription_period.total_seconds() + assert json_dict["premium_subscription_duration"] == self.premium_subscription_duration + + def test_transaction_type_is_required_argument(self): + with pytest.raises(TypeError, match="`transaction_type` is a required argument"): + TransactionPartnerUser(user=self.user) def test_equality(self, transaction_partner_user): a = transaction_partner_user b = TransactionPartnerUser( user=self.user, + transaction_type=self.transaction_type, ) c = TransactionPartnerUser( user=User(id=1, is_bot=False, first_name="user", last_name="user"), + transaction_type=self.transaction_type, ) d = User(id=1, is_bot=False, first_name="user", last_name="user") diff --git a/tests/auxil/dummy_objects.py b/tests/auxil/dummy_objects.py index 7e504f0db78..9abce52fa23 100644 --- a/tests/auxil/dummy_objects.py +++ b/tests/auxil/dummy_objects.py @@ -3,6 +3,7 @@ from typing import Union from telegram import ( + AcceptedGiftTypes, BotCommand, BotDescription, BotName, @@ -22,14 +23,18 @@ Gifts, MenuButton, MessageId, + OwnedGiftRegular, + OwnedGifts, Poll, PollOption, PreparedInlineMessage, SentWebAppMessage, + StarAmount, StarTransaction, StarTransactions, Sticker, StickerSet, + Story, TelegramObject, Update, User, @@ -74,6 +79,9 @@ type="dummy_type", accent_color_id=1, max_reaction_count=1, + accepted_gift_types=AcceptedGiftTypes( + unlimited_gifts=True, limited_gifts=True, unique_gifts=True, premium_subscription=True + ), ), "ChatInviteLink": ChatInviteLink( "dummy_invite_link", @@ -91,6 +99,22 @@ "MenuButton": MenuButton(type="dummy_type"), "Message": make_message("dummy_text"), "MessageId": MessageId(123456), + "OwnedGifts": OwnedGifts( + total_count=1, + gifts=[ + OwnedGiftRegular( + gift=Gift( + id="id1", + sticker=Sticker( + "file_id", "file_unique_id", 512, 512, False, False, "regular" + ), + star_count=5, + ), + send_date=_DUMMY_DATE, + owned_gift_id="some_id_1", + ) + ], + ), "Poll": Poll( id="dummy_id", question="dummy_question", @@ -103,6 +127,7 @@ ), "PreparedInlineMessage": PreparedInlineMessage(id="dummy_id", expiration_date=_DUMMY_DATE), "SentWebAppMessage": SentWebAppMessage(inline_message_id="dummy_inline_message_id"), + "StarAmount": StarAmount(amount=100, nanostar_amount=356), "StarTransactions": StarTransactions( transactions=[StarTransaction(id="dummy_id", amount=1, date=_DUMMY_DATE)] ), @@ -113,6 +138,7 @@ stickers=[_DUMMY_STICKER], sticker_type="dummy_type", ), + "Story": Story(chat=Chat(123, "prive"), id=123), "str": "dummy_string", "Update": Update(update_id=123456), "User": _DUMMY_USER, diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index b8ef90935ed..ae125c98a40 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -1098,6 +1098,21 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.REFUNDED_PAYMENT.check_update(update) update.message.refunded_payment = None + update.message.gift = "gift" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.GIFT.check_update(update) + update.message.gift = None + + update.message.unique_gift = "unique_gift" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.UNIQUE_GIFT.check_update(update) + update.message.unique_gift = None + + update.message.paid_message_price_changed = "paid_message_price_changed" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.PAID_MESSAGE_PRICE_CHANGED.check_update(update) + update.message.paid_message_price_changed = None + def test_filters_forwarded(self, update, message_origin_user): assert filters.FORWARDED.check_update(update) update.message.forward_origin = MessageOriginHiddenUser(dtm.datetime.utcnow(), 1) diff --git a/tests/request/test_requestparameter.py b/tests/request/test_requestparameter.py index 9082a58eae2..7e521b01229 100644 --- a/tests/request/test_requestparameter.py +++ b/tests/request/test_requestparameter.py @@ -21,7 +21,17 @@ import pytest -from telegram import InputFile, InputMediaPhoto, InputMediaVideo, InputSticker, MessageEntity +from telegram import ( + InputFile, + InputMediaPhoto, + InputMediaVideo, + InputProfilePhotoAnimated, + InputProfilePhotoStatic, + InputSticker, + InputStoryContentPhoto, + InputStoryContentVideo, + MessageEntity, +) from telegram.constants import ChatType from telegram.request._requestparameter import RequestParameter from tests.auxil.files import data_file @@ -176,6 +186,42 @@ def test_from_input_inputmedia_without_attach(self): assert request_parameter.value == {"type": "video"} assert request_parameter.input_files == [input_media.media, input_media.thumbnail] + def test_from_input_profile_photo_static(self): + input_profile_photo = InputProfilePhotoStatic(data_file("telegram.jpg").read_bytes()) + expected = input_profile_photo.to_dict() + expected.update({"photo": input_profile_photo.photo.attach_uri}) + request_parameter = RequestParameter.from_input("key", input_profile_photo) + assert request_parameter.value == expected + assert request_parameter.input_files == [input_profile_photo.photo] + + def test_from_input_profile_photo_animated(self): + input_profile_photo = InputProfilePhotoAnimated( + data_file("telegram2.mp4").read_bytes(), + main_frame_timestamp=dtm.timedelta(seconds=42, milliseconds=43), + ) + expected = input_profile_photo.to_dict() + expected.update({"animation": input_profile_photo.animation.attach_uri}) + request_parameter = RequestParameter.from_input("key", input_profile_photo) + assert request_parameter.value == expected + assert request_parameter.input_files == [input_profile_photo.animation] + + @pytest.mark.parametrize( + ("cls", "args"), + [ + (InputProfilePhotoStatic, (data_file("telegram.jpg"),)), + ( + InputProfilePhotoAnimated, + (data_file("telegram2.mp4"), dtm.timedelta(seconds=42, milliseconds=43)), + ), + ], + ) + def test_from_input_profile_photo_local_files(self, cls, args): + input_profile_photo = cls(*args) + expected = input_profile_photo.to_dict() + requested = RequestParameter.from_input("key", input_profile_photo) + assert requested.value == expected + assert requested.input_files is None + def test_from_input_inputsticker(self): input_sticker = InputSticker(data_file("telegram.png").read_bytes(), ["emoji"], "static") expected = input_sticker.to_dict() @@ -184,6 +230,36 @@ def test_from_input_inputsticker(self): assert request_parameter.value == expected assert request_parameter.input_files == [input_sticker.sticker] + def test_from_input_story_content_photo(self): + input_story_content_photo = InputStoryContentPhoto(data_file("telegram.jpg").read_bytes()) + expected = input_story_content_photo.to_dict() + expected.update({"photo": input_story_content_photo.photo.attach_uri}) + request_parameter = RequestParameter.from_input("key", input_story_content_photo) + assert request_parameter.value == expected + assert request_parameter.input_files == [input_story_content_photo.photo] + + def test_from_input_story_content_video(self): + input_story_content_video = InputStoryContentVideo(data_file("telegram2.mp4").read_bytes()) + expected = input_story_content_video.to_dict() + expected.update({"video": input_story_content_video.video.attach_uri}) + request_parameter = RequestParameter.from_input("key", input_story_content_video) + assert request_parameter.value == expected + assert request_parameter.input_files == [input_story_content_video.video] + + @pytest.mark.parametrize( + ("cls", "arg"), + [ + (InputStoryContentPhoto, data_file("telegram.jpg")), + (InputStoryContentVideo, data_file("telegram2.mp4")), + ], + ) + def test_from_input_story_content_local_files(self, cls, arg): + input_story_content = cls(arg) + expected = input_story_content.to_dict() + requested = RequestParameter.from_input("key", input_story_content) + assert requested.value == expected + assert requested.input_files is None + def test_from_input_str_and_bytes(self): input_str = "test_input" request_parameter = RequestParameter.from_input("input", input_str) diff --git a/tests/test_bot.py b/tests/test_bot.py index 22ffc9accc7..16c878dd29c 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -38,7 +38,6 @@ BotDescription, BotName, BotShortDescription, - BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, @@ -2373,26 +2372,6 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): monkeypatch.setattr(offline_bot.request, "post", make_assertion) assert await offline_bot.send_message(2, "text", allow_paid_broadcast=42) - async def test_get_business_connection(self, offline_bot, monkeypatch): - bci = "42" - user = User(1, "first", False) - user_chat_id = 1 - date = dtm.datetime.utcnow() - can_reply = True - is_enabled = True - bc = BusinessConnection(bci, user, user_chat_id, date, can_reply, is_enabled).to_json() - - async def do_request(*args, **kwargs): - data = kwargs.get("request_data") - obj = data.parameters.get("business_connection_id") - if obj == bci: - return 200, f'{{"ok": true, "result": {bc}}}'.encode() - return 400, b'{"ok": false, "result": []}' - - monkeypatch.setattr(offline_bot.request, "do_request", do_request) - obj = await offline_bot.get_business_connection(business_connection_id=bci) - assert isinstance(obj, BusinessConnection) - async def test_send_chat_action_all_args(self, bot, chat_id, monkeypatch): async def make_assertion(*args, **_): kwargs = args[1] @@ -2406,6 +2385,61 @@ async def make_assertion(*args, **_): monkeypatch.setattr(bot, "_post", make_assertion) assert await bot.send_chat_action(chat_id, "action", 1, 3) + async def test_gift_premium_subscription_all_args(self, bot, monkeypatch): + # can't make actual request so we just test that the correct data is passed + async def make_assertion(*args, **_): + kwargs = args[1] + return ( + kwargs.get("user_id") == 12 + and kwargs.get("month_count") == 3 + and kwargs.get("star_count") == 1000 + and kwargs.get("text") == "test text" + and kwargs.get("text_parse_mode") == "Markdown" + and kwargs.get("text_entities") + == [ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 11), + ] + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + assert await bot.gift_premium_subscription( + user_id=12, + month_count=3, + star_count=1000, + text="test text", + text_parse_mode="Markdown", + text_entities=[ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 11), + ], + ) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, "Markdown"), ("HTML", "HTML"), (None, None)], + ) + async def test_gift_premium_subscription_default_parse_mode( + self, default_bot, monkeypatch, passed_value, expected_value + ): + # can't make actual request so we just test that the correct data is passed + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("text_parse_mode") == expected_value + return True + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "user_id": 123, + "month_count": 3, + "star_count": 1000, + "text": "text", + } + if passed_value is not DEFAULT_NONE: + kwargs["text_parse_mode"] = passed_value + + assert await default_bot.gift_premium_subscription(**kwargs) + async def test_refund_star_payment(self, offline_bot, monkeypatch): # can't make actual request so we just test that the correct data is passed async def make_assertion(url, request_data: RequestData, *args, **kwargs): diff --git a/tests/test_business.py b/tests/test_business_classes.py similarity index 63% rename from tests/test_business.py rename to tests/test_business_classes.py index d8e976a9144..aabf60064c6 100644 --- a/tests/test_business.py +++ b/tests/test_business_classes.py @@ -32,7 +32,9 @@ Sticker, User, ) +from telegram._business import BusinessBotRights from telegram._utils.datetime import UTC, to_timestamp +from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @@ -41,7 +43,20 @@ class BusinessTestBase: user = User(123, "test_user", False) user_chat_id = 123 date = dtm.datetime.now(tz=UTC).replace(microsecond=0) + can_change_gift_settings = True + can_convert_gifts_to_stars = True + can_delete_all_messages = True + can_delete_sent_messages = True + can_edit_bio = True + can_edit_name = True + can_edit_profile_photo = True + can_edit_username = True + can_manage_stories = True + can_read_messages = True can_reply = True + can_transfer_and_upgrade_gifts = True + can_transfer_stars = True + can_view_gifts_and_stars = True is_enabled = True message_ids = (123, 321) business_connection_id = "123" @@ -60,7 +75,27 @@ class BusinessTestBase: @pytest.fixture(scope="module") -def business_connection(): +def business_bot_rights(): + return BusinessBotRights( + can_change_gift_settings=BusinessTestBase.can_change_gift_settings, + can_convert_gifts_to_stars=BusinessTestBase.can_convert_gifts_to_stars, + can_delete_all_messages=BusinessTestBase.can_delete_all_messages, + can_delete_sent_messages=BusinessTestBase.can_delete_sent_messages, + can_edit_bio=BusinessTestBase.can_edit_bio, + can_edit_name=BusinessTestBase.can_edit_name, + can_edit_profile_photo=BusinessTestBase.can_edit_profile_photo, + can_edit_username=BusinessTestBase.can_edit_username, + can_manage_stories=BusinessTestBase.can_manage_stories, + can_read_messages=BusinessTestBase.can_read_messages, + can_reply=BusinessTestBase.can_reply, + can_transfer_and_upgrade_gifts=BusinessTestBase.can_transfer_and_upgrade_gifts, + can_transfer_stars=BusinessTestBase.can_transfer_stars, + can_view_gifts_and_stars=BusinessTestBase.can_view_gifts_and_stars, + ) + + +@pytest.fixture(scope="module") +def business_connection(business_bot_rights): return BusinessConnection( BusinessTestBase.id_, BusinessTestBase.user, @@ -68,6 +103,7 @@ def business_connection(): BusinessTestBase.date, BusinessTestBase.can_reply, BusinessTestBase.is_enabled, + rights=business_bot_rights, ) @@ -113,6 +149,90 @@ def business_opening_hours(): ) +class TestBusinessBotRightsWithoutRequest(BusinessTestBase): + def test_slot_behaviour(self, business_bot_rights): + inst = business_bot_rights + 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_to_dict(self, business_bot_rights): + rights_dict = business_bot_rights.to_dict() + + assert isinstance(rights_dict, dict) + assert rights_dict["can_reply"] is self.can_reply + assert rights_dict["can_read_messages"] is self.can_read_messages + assert rights_dict["can_delete_sent_messages"] is self.can_delete_sent_messages + assert rights_dict["can_delete_all_messages"] is self.can_delete_all_messages + assert rights_dict["can_edit_name"] is self.can_edit_name + assert rights_dict["can_edit_bio"] is self.can_edit_bio + assert rights_dict["can_edit_profile_photo"] is self.can_edit_profile_photo + assert rights_dict["can_edit_username"] is self.can_edit_username + assert rights_dict["can_change_gift_settings"] is self.can_change_gift_settings + assert rights_dict["can_view_gifts_and_stars"] is self.can_view_gifts_and_stars + assert rights_dict["can_convert_gifts_to_stars"] is self.can_convert_gifts_to_stars + assert rights_dict["can_transfer_and_upgrade_gifts"] is self.can_transfer_and_upgrade_gifts + assert rights_dict["can_transfer_stars"] is self.can_transfer_stars + assert rights_dict["can_manage_stories"] is self.can_manage_stories + + def test_de_json(self): + json_dict = { + "can_reply": self.can_reply, + "can_read_messages": self.can_read_messages, + "can_delete_sent_messages": self.can_delete_sent_messages, + "can_delete_all_messages": self.can_delete_all_messages, + "can_edit_name": self.can_edit_name, + "can_edit_bio": self.can_edit_bio, + "can_edit_profile_photo": self.can_edit_profile_photo, + "can_edit_username": self.can_edit_username, + "can_change_gift_settings": self.can_change_gift_settings, + "can_view_gifts_and_stars": self.can_view_gifts_and_stars, + "can_convert_gifts_to_stars": self.can_convert_gifts_to_stars, + "can_transfer_and_upgrade_gifts": self.can_transfer_and_upgrade_gifts, + "can_transfer_stars": self.can_transfer_stars, + "can_manage_stories": self.can_manage_stories, + } + + rights = BusinessBotRights.de_json(json_dict, None) + assert rights.can_reply is self.can_reply + assert rights.can_read_messages is self.can_read_messages + assert rights.can_delete_sent_messages is self.can_delete_sent_messages + assert rights.can_delete_all_messages is self.can_delete_all_messages + assert rights.can_edit_name is self.can_edit_name + assert rights.can_edit_bio is self.can_edit_bio + assert rights.can_edit_profile_photo is self.can_edit_profile_photo + assert rights.can_edit_username is self.can_edit_username + assert rights.can_change_gift_settings is self.can_change_gift_settings + assert rights.can_view_gifts_and_stars is self.can_view_gifts_and_stars + assert rights.can_convert_gifts_to_stars is self.can_convert_gifts_to_stars + assert rights.can_transfer_and_upgrade_gifts is self.can_transfer_and_upgrade_gifts + assert rights.can_transfer_stars is self.can_transfer_stars + assert rights.can_manage_stories is self.can_manage_stories + assert rights.api_kwargs == {} + assert isinstance(rights, BusinessBotRights) + + def test_equality(self): + rights1 = BusinessBotRights( + can_reply=self.can_reply, + ) + + rights2 = BusinessBotRights( + can_reply=True, + ) + + rights3 = BusinessBotRights( + can_reply=True, + can_read_messages=self.can_read_messages, + ) + + assert rights1 == rights2 + assert hash(rights1) == hash(rights2) + assert rights1 is not rights2 + + assert rights1 != rights3 + assert hash(rights1) != hash(rights3) + + class TestBusinessConnectionWithoutRequest(BusinessTestBase): def test_slots(self, business_connection): bc = business_connection @@ -120,7 +240,7 @@ def test_slots(self, business_connection): assert getattr(bc, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(bc)) == len(set(mro_slots(bc))), "duplicate slot" - def test_de_json(self): + def test_de_json(self, business_bot_rights): json_dict = { "id": self.id_, "user": self.user.to_dict(), @@ -128,6 +248,7 @@ def test_de_json(self): "date": to_timestamp(self.date), "can_reply": self.can_reply, "is_enabled": self.is_enabled, + "rights": business_bot_rights.to_dict(), } bc = BusinessConnection.de_json(json_dict, None) assert bc.id == self.id_ @@ -136,10 +257,11 @@ def test_de_json(self): assert bc.date == self.date assert bc.can_reply == self.can_reply assert bc.is_enabled == self.is_enabled + assert bc.rights == business_bot_rights assert bc.api_kwargs == {} assert isinstance(bc, BusinessConnection) - def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): + def test_de_json_localization(self, offline_bot, raw_bot, tz_bot, business_bot_rights): json_dict = { "id": self.id_, "user": self.user.to_dict(), @@ -147,6 +269,7 @@ def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): "date": to_timestamp(self.date), "can_reply": self.can_reply, "is_enabled": self.is_enabled, + "rights": business_bot_rights.to_dict(), } chat_bot = BusinessConnection.de_json(json_dict, offline_bot) chat_bot_raw = BusinessConnection.de_json(json_dict, raw_bot) @@ -160,25 +283,52 @@ def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): assert chat_bot_raw.date.tzinfo == UTC assert date_offset_tz == date_offset - def test_to_dict(self, business_connection): + def test_to_dict(self, business_connection, business_bot_rights): bc_dict = business_connection.to_dict() assert isinstance(bc_dict, dict) assert bc_dict["id"] == self.id_ assert bc_dict["user"] == self.user.to_dict() assert bc_dict["user_chat_id"] == self.user_chat_id assert bc_dict["date"] == to_timestamp(self.date) - assert bc_dict["can_reply"] == self.can_reply assert bc_dict["is_enabled"] == self.is_enabled + assert bc_dict["rights"] == business_bot_rights.to_dict() - def test_equality(self): + def test_equality(self, business_bot_rights): bc1 = BusinessConnection( - self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + self.id_, + self.user, + self.user_chat_id, + self.date, + self.can_reply, + self.is_enabled, + rights=business_bot_rights, ) bc2 = BusinessConnection( - self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + self.id_, + self.user, + self.user_chat_id, + self.date, + self.can_reply, + self.is_enabled, + rights=business_bot_rights, ) bc3 = BusinessConnection( - "321", self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + "321", + self.user, + self.user_chat_id, + self.date, + self.can_reply, + self.is_enabled, + rights=business_bot_rights, + ) + bc4 = BusinessConnection( + self.id_, + self.user, + self.user_chat_id, + self.date, + self.can_reply, + self.is_enabled, + rights=BusinessBotRights(), ) assert bc1 == bc2 @@ -187,6 +337,35 @@ def test_equality(self): assert bc1 != bc3 assert hash(bc1) != hash(bc3) + assert bc1 != bc4 + assert hash(bc1) != hash(bc4) + + def test_can_reply_argument_property_deprecation(self, business_connection): + with pytest.warns(PTBDeprecationWarning, match=r"9\.0.*can\_reply") as record: + assert BusinessConnection( + id=self.id_, + user=self.user, + user_chat_id=self.user_chat_id, + date=self.date, + can_reply=True, + is_enabled=self.is_enabled, + ) + + assert record[0].category == PTBDeprecationWarning + assert record[0].filename == __file__, "wrong stacklevel!" + + with pytest.warns(PTBDeprecationWarning, match=r"9\.0.*can\_reply") as record: + assert business_connection.can_reply is self.can_reply + + assert record[0].category == PTBDeprecationWarning + assert record[0].filename == __file__, "wrong stacklevel!" + + def test_is_enabled_remains_required(self): + with pytest.raises(TypeError): + BusinessConnection( + id=self.id_, user=self.user, user_chat_id=self.user_chat_id, date=self.date + ) + class TestBusinessMessagesDeleted(BusinessTestBase): def test_slots(self, business_messages_deleted): diff --git a/tests/test_business_methods.py b/tests/test_business_methods.py new file mode 100644 index 00000000000..13017eca8e6 --- /dev/null +++ b/tests/test_business_methods.py @@ -0,0 +1,600 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 as dtm + +import pytest + +from telegram import ( + BusinessConnection, + Chat, + InputProfilePhotoStatic, + InputStoryContentPhoto, + MessageEntity, + StarAmount, + Story, + StoryAreaTypeLink, + StoryAreaTypeUniqueGift, + User, +) +from telegram._files.sticker import Sticker +from telegram._gifts import AcceptedGiftTypes, Gift +from telegram._ownedgift import OwnedGiftRegular, OwnedGifts +from telegram._utils.datetime import UTC +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram.constants import InputProfilePhotoType, InputStoryContentType +from tests.auxil.files import data_file + + +class BusinessMethodsTestBase: + bci = "42" + + +class TestBusinessMethodsWithoutRequest(BusinessMethodsTestBase): + async def test_get_business_connection(self, offline_bot, monkeypatch): + user = User(1, "first", False) + user_chat_id = 1 + date = dtm.datetime.utcnow() + can_reply = True + is_enabled = True + bc = BusinessConnection( + self.bci, user, user_chat_id, date, can_reply, is_enabled + ).to_json() + + async def do_request(*args, **kwargs): + data = kwargs.get("request_data") + obj = data.parameters.get("business_connection_id") + if obj == self.bci: + return 200, f'{{"ok": true, "result": {bc}}}'.encode() + return 400, b'{"ok": false, "result": []}' + + monkeypatch.setattr(offline_bot.request, "do_request", do_request) + obj = await offline_bot.get_business_connection(business_connection_id=self.bci) + assert isinstance(obj, BusinessConnection) + + @pytest.mark.parametrize("bool_param", [True, False, None]) + async def test_get_business_account_gifts(self, offline_bot, monkeypatch, bool_param): + offset = 50 + limit = 50 + owned_gifts = OwnedGifts( + total_count=1, + gifts=[ + OwnedGiftRegular( + gift=Gift( + id="id1", + sticker=Sticker( + "file_id", "file_unique_id", 512, 512, False, False, "regular" + ), + star_count=5, + ), + send_date=dtm.datetime.now(tz=UTC).replace(microsecond=0), + owned_gift_id="some_id_1", + ) + ], + ).to_json() + + async def do_request_and_make_assertions(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("exclude_unsaved") is bool_param + assert data.get("exclude_saved") is bool_param + assert data.get("exclude_unlimited") is bool_param + assert data.get("exclude_limited") is bool_param + assert data.get("exclude_unique") is bool_param + assert data.get("sort_by_price") is bool_param + assert data.get("offset") == offset + assert data.get("limit") == limit + + return 200, f'{{"ok": true, "result": {owned_gifts}}}'.encode() + + monkeypatch.setattr(offline_bot.request, "do_request", do_request_and_make_assertions) + obj = await offline_bot.get_business_account_gifts( + business_connection_id=self.bci, + exclude_unsaved=bool_param, + exclude_saved=bool_param, + exclude_unlimited=bool_param, + exclude_limited=bool_param, + exclude_unique=bool_param, + sort_by_price=bool_param, + offset=offset, + limit=limit, + ) + assert isinstance(obj, OwnedGifts) + + async def test_get_business_account_star_balance(self, offline_bot, monkeypatch): + star_amount_json = StarAmount(amount=100, nanostar_amount=356).to_json() + + async def do_request(*args, **kwargs): + data = kwargs.get("request_data") + obj = data.parameters.get("business_connection_id") + if obj == self.bci: + return 200, f'{{"ok": true, "result": {star_amount_json}}}'.encode() + return 400, b'{"ok": false, "result": []}' + + monkeypatch.setattr(offline_bot.request, "do_request", do_request) + obj = await offline_bot.get_business_account_star_balance(business_connection_id=self.bci) + assert isinstance(obj, StarAmount) + + async def test_read_business_message(self, offline_bot, monkeypatch): + chat_id = 43 + message_id = 44 + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("chat_id") == chat_id + assert data.get("message_id") == message_id + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.read_business_message( + business_connection_id=self.bci, chat_id=chat_id, message_id=message_id + ) + + async def test_delete_business_messages(self, offline_bot, monkeypatch): + message_ids = [1, 2, 3] + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("message_ids") == message_ids + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.delete_business_messages( + business_connection_id=self.bci, message_ids=message_ids + ) + + @pytest.mark.parametrize("last_name", [None, "last_name"]) + async def test_set_business_account_name(self, offline_bot, monkeypatch, last_name): + first_name = "Test Business Account" + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("first_name") == first_name + assert data.get("last_name") == last_name + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.set_business_account_name( + business_connection_id=self.bci, first_name=first_name, last_name=last_name + ) + + @pytest.mark.parametrize("username", ["username", None]) + async def test_set_business_account_username(self, offline_bot, monkeypatch, username): + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("username") == username + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.set_business_account_username( + business_connection_id=self.bci, username=username + ) + + @pytest.mark.parametrize("bio", ["bio", None]) + async def test_set_business_account_bio(self, offline_bot, monkeypatch, bio): + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("bio") == bio + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.set_business_account_bio(business_connection_id=self.bci, bio=bio) + + async def test_set_business_account_gift_settings(self, offline_bot, monkeypatch): + show_gift_button = True + accepted_gift_types = AcceptedGiftTypes(True, True, True, True) + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").json_parameters + assert data.get("business_connection_id") == self.bci + assert data.get("show_gift_button") == "true" + assert data.get("accepted_gift_types") == accepted_gift_types.to_json() + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.set_business_account_gift_settings( + business_connection_id=self.bci, + show_gift_button=show_gift_button, + accepted_gift_types=accepted_gift_types, + ) + + async def test_convert_gift_to_stars(self, offline_bot, monkeypatch): + owned_gift_id = "some_id" + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("owned_gift_id") == owned_gift_id + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.convert_gift_to_stars( + business_connection_id=self.bci, + owned_gift_id=owned_gift_id, + ) + + @pytest.mark.parametrize("keep_original_details", [True, None]) + @pytest.mark.parametrize("star_count", [100, None]) + async def test_upgrade_gift(self, offline_bot, monkeypatch, keep_original_details, star_count): + owned_gift_id = "some_id" + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("owned_gift_id") == owned_gift_id + assert data.get("keep_original_details") is keep_original_details + assert data.get("star_count") == star_count + + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.upgrade_gift( + business_connection_id=self.bci, + owned_gift_id=owned_gift_id, + keep_original_details=keep_original_details, + star_count=star_count, + ) + + @pytest.mark.parametrize("star_count", [100, None]) + async def test_transfer_gift(self, offline_bot, monkeypatch, star_count): + owned_gift_id = "some_id" + new_owner_chat_id = 123 + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("owned_gift_id") == owned_gift_id + assert data.get("new_owner_chat_id") == new_owner_chat_id + assert data.get("star_count") == star_count + + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.transfer_gift( + business_connection_id=self.bci, + owned_gift_id=owned_gift_id, + new_owner_chat_id=new_owner_chat_id, + star_count=star_count, + ) + + async def test_transfer_business_account_stars(self, offline_bot, monkeypatch): + star_count = 100 + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("star_count") == star_count + + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.transfer_business_account_stars( + business_connection_id=self.bci, + star_count=star_count, + ) + + @pytest.mark.parametrize("is_public", [True, False, None, DEFAULT_NONE]) + async def test_set_business_account_profile_photo(self, offline_bot, monkeypatch, is_public): + async def make_assertion(*args, **kwargs): + request_data = kwargs.get("request_data") + params = request_data.parameters + assert params.get("business_connection_id") == self.bci + if is_public is DEFAULT_NONE: + assert "is_public" not in params + else: + assert params.get("is_public") == is_public + + assert (photo_dict := params.get("photo")).get("type") == InputProfilePhotoType.STATIC + assert (photo_attach := photo_dict["photo"]).startswith("attach://") + assert isinstance( + request_data.multipart_data.get(photo_attach.removeprefix("attach://")), tuple + ) + + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "photo": InputProfilePhotoStatic( + photo=data_file("telegram.jpg").read_bytes(), + ), + } + if is_public is not DEFAULT_NONE: + kwargs["is_public"] = is_public + + assert await offline_bot.set_business_account_profile_photo(**kwargs) + + async def test_set_business_account_profile_photo_local_file(self, offline_bot, monkeypatch): + async def make_assertion(*args, **kwargs): + request_data = kwargs.get("request_data") + params = request_data.parameters + assert params.get("business_connection_id") == self.bci + + assert (photo_dict := params.get("photo")).get("type") == InputProfilePhotoType.STATIC + assert photo_dict["photo"] == data_file("telegram.jpg").as_uri() + assert not request_data.multipart_data + + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "photo": InputProfilePhotoStatic( + photo=data_file("telegram.jpg"), + ), + } + + assert await offline_bot.set_business_account_profile_photo(**kwargs) + + @pytest.mark.parametrize("is_public", [True, False, None, DEFAULT_NONE]) + async def test_remove_business_account_profile_photo( + self, offline_bot, monkeypatch, is_public + ): + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + if is_public is DEFAULT_NONE: + assert "is_public" not in data + else: + assert data.get("is_public") == is_public + + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + kwargs = {"business_connection_id": self.bci} + if is_public is not DEFAULT_NONE: + kwargs["is_public"] = is_public + + assert await offline_bot.remove_business_account_profile_photo(**kwargs) + + @pytest.mark.parametrize("active_period", [dtm.timedelta(seconds=30), 30]) + async def test_post_story_all_args(self, offline_bot, monkeypatch, active_period): + content = InputStoryContentPhoto(photo=data_file("telegram.jpg").read_bytes()) + caption = "test caption" + caption_entities = [ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 11), + ] + parse_mode = "Markdown" + areas = [StoryAreaTypeLink("http_url"), StoryAreaTypeUniqueGift("unique_gift_name")] + post_to_chat_page = True + protect_content = True + json_story = Story(chat=Chat(123, "private"), id=123).to_json() + + async def do_request_and_make_assertions(*args, **kwargs): + request_data = kwargs.get("request_data") + params = kwargs.get("request_data").parameters + assert params.get("business_connection_id") == self.bci + assert params.get("active_period") == 30 + assert params.get("caption") == caption + assert params.get("caption_entities") == [e.to_dict() for e in caption_entities] + assert params.get("parse_mode") == parse_mode + assert params.get("areas") == [area.to_dict() for area in areas] + assert params.get("post_to_chat_page") is post_to_chat_page + assert params.get("protect_content") is protect_content + + assert (content_dict := params.get("content")).get( + "type" + ) == InputStoryContentType.PHOTO + assert (photo_attach := content_dict["photo"]).startswith("attach://") + assert isinstance( + request_data.multipart_data.get(photo_attach.removeprefix("attach://")), tuple + ) + + return 200, f'{{"ok": true, "result": {json_story}}}'.encode() + + monkeypatch.setattr(offline_bot.request, "do_request", do_request_and_make_assertions) + obj = await offline_bot.post_story( + business_connection_id=self.bci, + content=content, + active_period=active_period, + caption=caption, + caption_entities=caption_entities, + parse_mode=parse_mode, + areas=areas, + post_to_chat_page=post_to_chat_page, + protect_content=protect_content, + ) + assert isinstance(obj, Story) + + @pytest.mark.parametrize("active_period", [dtm.timedelta(seconds=30), 30]) + async def test_post_story_local_file(self, offline_bot, monkeypatch, active_period): + json_story = Story(chat=Chat(123, "private"), id=123).to_json() + + async def make_assertion(*args, **kwargs): + request_data = kwargs.get("request_data") + params = request_data.parameters + assert params.get("business_connection_id") == self.bci + + assert (content_dict := params.get("content")).get( + "type" + ) == InputStoryContentType.PHOTO + assert content_dict["photo"] == data_file("telegram.jpg").as_uri() + assert not request_data.multipart_data + + return 200, f'{{"ok": true, "result": {json_story}}}'.encode() + + monkeypatch.setattr(offline_bot.request, "do_request", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "content": InputStoryContentPhoto( + photo=data_file("telegram.jpg"), + ), + "active_period": active_period, + } + + assert await offline_bot.post_story(**kwargs) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, "Markdown"), ("HTML", "HTML"), (None, None)], + ) + async def test_post_story_default_parse_mode( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("parse_mode") == expected_value + return Story(chat=Chat(123, "private"), id=123).to_dict() + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "content": InputStoryContentPhoto(photo=data_file("telegram.jpg").read_bytes()), + "active_period": dtm.timedelta(seconds=20), + "caption": "caption", + } + if passed_value is not DEFAULT_NONE: + kwargs["parse_mode"] = passed_value + + await default_bot.post_story(**kwargs) + + @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, True), (False, False), (None, None)], + ) + async def test_post_story_default_protect_content( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("protect_content") == expected_value + return Story(chat=Chat(123, "private"), id=123).to_dict() + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "content": InputStoryContentPhoto(bytes("photo", encoding="utf-8")), + "active_period": dtm.timedelta(seconds=20), + } + if passed_value is not DEFAULT_NONE: + kwargs["protect_content"] = passed_value + + await default_bot.post_story(**kwargs) + + async def test_edit_story_all_args(self, offline_bot, monkeypatch): + story_id = 1234 + content = InputStoryContentPhoto(photo=data_file("telegram.jpg").read_bytes()) + caption = "test caption" + caption_entities = [ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 11), + ] + parse_mode = "Markdown" + areas = [StoryAreaTypeLink("http_url"), StoryAreaTypeUniqueGift("unique_gift_name")] + json_story = Story(chat=Chat(123, "private"), id=123).to_json() + + async def do_request_and_make_assertions(*args, **kwargs): + request_data = kwargs.get("request_data") + params = kwargs.get("request_data").parameters + assert params.get("business_connection_id") == self.bci + assert params.get("story_id") == story_id + assert params.get("caption") == caption + assert params.get("caption_entities") == [e.to_dict() for e in caption_entities] + assert params.get("parse_mode") == parse_mode + assert params.get("areas") == [area.to_dict() for area in areas] + + assert (content_dict := params.get("content")).get( + "type" + ) == InputStoryContentType.PHOTO + assert (photo_attach := content_dict["photo"]).startswith("attach://") + assert isinstance( + request_data.multipart_data.get(photo_attach.removeprefix("attach://")), tuple + ) + + return 200, f'{{"ok": true, "result": {json_story}}}'.encode() + + monkeypatch.setattr(offline_bot.request, "do_request", do_request_and_make_assertions) + obj = await offline_bot.edit_story( + business_connection_id=self.bci, + story_id=story_id, + content=content, + caption=caption, + caption_entities=caption_entities, + parse_mode=parse_mode, + areas=areas, + ) + assert isinstance(obj, Story) + + async def test_edit_story_local_file(self, offline_bot, monkeypatch): + json_story = Story(chat=Chat(123, "private"), id=123).to_json() + + async def make_assertion(*args, **kwargs): + request_data = kwargs.get("request_data") + params = request_data.parameters + assert params.get("business_connection_id") == self.bci + + assert (content_dict := params.get("content")).get( + "type" + ) == InputStoryContentType.PHOTO + assert content_dict["photo"] == data_file("telegram.jpg").as_uri() + assert not request_data.multipart_data + + return 200, f'{{"ok": true, "result": {json_story}}}'.encode() + + monkeypatch.setattr(offline_bot.request, "do_request", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "story_id": 1234, + "content": InputStoryContentPhoto( + photo=data_file("telegram.jpg"), + ), + } + + assert await offline_bot.edit_story(**kwargs) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, "Markdown"), ("HTML", "HTML"), (None, None)], + ) + async def test_edit_story_default_parse_mode( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("parse_mode") == expected_value + return Story(chat=Chat(123, "private"), id=123).to_dict() + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "story_id": 1234, + "content": InputStoryContentPhoto(photo=data_file("telegram.jpg").read_bytes()), + "caption": "caption", + } + if passed_value is not DEFAULT_NONE: + kwargs["parse_mode"] = passed_value + + await default_bot.edit_story(**kwargs) + + async def test_delete_story(self, offline_bot, monkeypatch): + story_id = 123 + + async def make_assertion(*args, **kwargs): + data = kwargs.get("request_data").parameters + assert data.get("business_connection_id") == self.bci + assert data.get("story_id") == story_id + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.delete_story(business_connection_id=self.bci, story_id=story_id) diff --git a/tests/test_chat.py b/tests/test_chat.py index f53a0fdd2fe..8e901fb91bf 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1353,6 +1353,28 @@ async def make_assertion_channel(*_, **kwargs): text_entities="text_entities", ) + @pytest.mark.parametrize("star_count", [100, None]) + async def test_instance_method_transfer_gift(self, monkeypatch, chat, star_count): + async def make_assertion(*_, **kwargs): + return ( + kwargs["new_owner_chat_id"] == chat.id + and kwargs["owned_gift_id"] == "owned_gift_id" + and kwargs["star_count"] == star_count + ) + + assert check_shortcut_signature( + Chat.transfer_gift, Bot.transfer_gift, ["new_owner_chat_id"], [] + ) + assert await check_shortcut_call(chat.transfer_gift, chat.get_bot(), "transfer_gift") + assert await check_defaults_handling(chat.transfer_gift, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "transfer_gift", make_assertion) + assert await chat.transfer_gift( + owned_gift_id="owned_gift_id", + star_count=star_count, + business_connection_id="business_connection_id", + ) + async def test_instance_method_verify_chat(self, monkeypatch, chat): async def make_assertion(*_, **kwargs): return ( @@ -1384,6 +1406,27 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "remove_chat_verification", make_assertion) assert await chat.remove_verification() + async def test_instance_method_read_business_message(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["business_connection_id"] == "business_connection_id" + and kwargs["message_id"] == "message_id" + ) + + assert check_shortcut_signature( + Chat.read_business_message, Bot.read_business_message, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.read_business_message, chat.get_bot(), "read_business_message" + ) + assert await check_defaults_handling(chat.read_business_message, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "read_business_message", make_assertion) + assert await chat.read_business_message( + message_id="message_id", business_connection_id="business_connection_id" + ) + 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 11373567c9f..dff26aa7398 100644 --- a/tests/test_chatfullinfo.py +++ b/tests/test_chatfullinfo.py @@ -34,8 +34,10 @@ ReactionTypeCustomEmoji, ReactionTypeEmoji, ) +from telegram._gifts import AcceptedGiftTypes from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import ReactionEmoji +from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @@ -46,6 +48,7 @@ def chat_full_info(bot): type=ChatFullInfoTestBase.type_, accent_color_id=ChatFullInfoTestBase.accent_color_id, max_reaction_count=ChatFullInfoTestBase.max_reaction_count, + accepted_gift_types=ChatFullInfoTestBase.accepted_gift_types, title=ChatFullInfoTestBase.title, username=ChatFullInfoTestBase.username, sticker_set_name=ChatFullInfoTestBase.sticker_set_name, @@ -140,6 +143,8 @@ class ChatFullInfoTestBase: first_name = "first_name" last_name = "last_name" can_send_paid_media = True + can_send_gift = True + accepted_gift_types = AcceptedGiftTypes(True, True, True, True) class TestChatFullInfoWithoutRequest(ChatFullInfoTestBase): @@ -158,6 +163,8 @@ def test_de_json(self, offline_bot): "accent_color_id": self.accent_color_id, "max_reaction_count": self.max_reaction_count, "username": self.username, + "accepted_gift_types": self.accepted_gift_types.to_dict(), + "can_send_gift": self.can_send_gift, "sticker_set_name": self.sticker_set_name, "can_set_sticker_set": self.can_set_sticker_set, "permissions": self.permissions.to_dict(), @@ -195,10 +202,12 @@ def test_de_json(self, offline_bot): "can_send_paid_media": self.can_send_paid_media, } cfi = ChatFullInfo.de_json(json_dict, offline_bot) + assert cfi.api_kwargs == {} assert cfi.id == self.id_ assert cfi.title == self.title assert cfi.type == self.type_ assert cfi.username == self.username + assert cfi.accepted_gift_types == self.accepted_gift_types assert cfi.sticker_set_name == self.sticker_set_name assert cfi.can_set_sticker_set == self.can_set_sticker_set assert cfi.permissions == self.permissions @@ -245,6 +254,7 @@ def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): "type": self.type_, "accent_color_id": self.accent_color_id, "max_reaction_count": self.max_reaction_count, + "accepted_gift_types": self.accepted_gift_types.to_dict(), "emoji_status_expiration_date": to_timestamp(self.emoji_status_expiration_date), } cfi_bot = ChatFullInfo.de_json(json_dict, offline_bot) @@ -312,15 +322,46 @@ def test_to_dict(self, chat_full_info): 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["accepted_gift_types"] == cfi.accepted_gift_types.to_dict() assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count + def test_accepted_gift_types_is_required_argument(self): + with pytest.raises(TypeError, match="`accepted_gift_type` is a required argument"): + ChatFullInfo( + id=123, + type=Chat.PRIVATE, + accent_color_id=1, + max_reaction_count=2, + can_send_gift=True, + ) + + def test_can_send_gift_deprecation_warning(self): + with pytest.warns( + PTBDeprecationWarning, + match="'can_send_gift' was replaced by 'accepted_gift_types' in Bot API 9.0", + ): + chat_full_info = ChatFullInfo( + id=123, + type=Chat.PRIVATE, + accent_color_id=1, + max_reaction_count=2, + accepted_gift_types=self.accepted_gift_types, + can_send_gift=self.can_send_gift, + ) + with pytest.warns( + PTBDeprecationWarning, + match="Bot API 9.0 renamed the attribute 'can_send_gift' to 'accepted_gift_types'", + ): + chat_full_info.can_send_gift + def test_always_tuples_attributes(self): cfi = ChatFullInfo( id=123, type=Chat.PRIVATE, accent_color_id=1, max_reaction_count=2, + accepted_gift_types=self.accepted_gift_types, ) assert isinstance(cfi.active_usernames, tuple) assert cfi.active_usernames == () diff --git a/tests/test_constants.py b/tests/test_constants.py index 3cd9e56e7ab..b97cc4f8eac 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -203,6 +203,7 @@ def is_type_attribute(name: str) -> bool: "via_bot", "is_from_offline", "show_caption_above_media", + "paid_star_count", } @pytest.mark.parametrize( diff --git a/tests/test_enum_types.py b/tests/test_enum_types.py index ef3729061bf..36a823eda46 100644 --- a/tests/test_enum_types.py +++ b/tests/test_enum_types.py @@ -32,6 +32,7 @@ exclude_patterns = { re.compile(re.escape("self.type: ReactionType = type")), re.compile(re.escape("self.type: BackgroundType = type")), + re.compile(re.escape("self.type: StoryAreaType = type")), } diff --git a/tests/test_gifts.py b/tests/test_gifts.py index 3b3ef52cb39..2b676a6ee89 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -20,7 +20,8 @@ import pytest -from telegram import BotCommand, Gift, Gifts, MessageEntity, Sticker +from telegram import BotCommand, Gift, GiftInfo, Gifts, MessageEntity, Sticker +from telegram._gifts import AcceptedGiftTypes from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.request import RequestData from tests.auxil.slots import mro_slots @@ -291,3 +292,190 @@ class TestGiftsWithRequest(GiftTestBase): async def test_get_available_gifts(self, bot, chat_id): # We don't control the available gifts, so we can not make any better assertions assert isinstance(await bot.get_available_gifts(), Gifts) + + +@pytest.fixture +def gift_info(): + return GiftInfo( + gift=GiftInfoTestBase.gift, + owned_gift_id=GiftInfoTestBase.owned_gift_id, + convert_star_count=GiftInfoTestBase.convert_star_count, + prepaid_upgrade_star_count=GiftInfoTestBase.prepaid_upgrade_star_count, + can_be_upgraded=GiftInfoTestBase.can_be_upgraded, + text=GiftInfoTestBase.text, + entities=GiftInfoTestBase.entities, + is_private=GiftInfoTestBase.is_private, + ) + + +class GiftInfoTestBase: + gift = Gift( + id="some_id", + sticker=Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular"), + star_count=5, + total_count=10, + remaining_count=15, + upgrade_star_count=20, + ) + owned_gift_id = "some_owned_gift_id" + convert_star_count = 100 + prepaid_upgrade_star_count = 200 + can_be_upgraded = True + text = "test text" + entities = ( + MessageEntity(MessageEntity.BOLD, 0, 4), + MessageEntity(MessageEntity.ITALIC, 5, 8), + ) + is_private = True + + +class TestGiftInfoWithoutRequest(GiftInfoTestBase): + def test_slot_behaviour(self, gift_info): + for attr in gift_info.__slots__: + assert getattr(gift_info, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(gift_info)) == len(set(mro_slots(gift_info))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "gift": self.gift.to_dict(), + "owned_gift_id": self.owned_gift_id, + "convert_star_count": self.convert_star_count, + "prepaid_upgrade_star_count": self.prepaid_upgrade_star_count, + "can_be_upgraded": self.can_be_upgraded, + "text": self.text, + "entities": [e.to_dict() for e in self.entities], + "is_private": self.is_private, + } + gift_info = GiftInfo.de_json(json_dict, offline_bot) + assert gift_info.api_kwargs == {} + assert gift_info.gift == self.gift + assert gift_info.owned_gift_id == self.owned_gift_id + assert gift_info.convert_star_count == self.convert_star_count + assert gift_info.prepaid_upgrade_star_count == self.prepaid_upgrade_star_count + assert gift_info.can_be_upgraded == self.can_be_upgraded + assert gift_info.text == self.text + assert gift_info.entities == self.entities + assert gift_info.is_private == self.is_private + + def test_to_dict(self, gift_info): + json_dict = gift_info.to_dict() + assert json_dict["gift"] == self.gift.to_dict() + assert json_dict["owned_gift_id"] == self.owned_gift_id + assert json_dict["convert_star_count"] == self.convert_star_count + assert json_dict["prepaid_upgrade_star_count"] == self.prepaid_upgrade_star_count + assert json_dict["can_be_upgraded"] == self.can_be_upgraded + assert json_dict["text"] == self.text + assert json_dict["entities"] == [e.to_dict() for e in self.entities] + assert json_dict["is_private"] == self.is_private + + def test_parse_entity(self, gift_info): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + + assert gift_info.parse_entity(entity) == "test" + + with pytest.raises(RuntimeError, match="GiftInfo has no"): + GiftInfo( + gift=self.gift, + ).parse_entity(entity) + + def test_parse_entities(self, gift_info): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + entity_2 = MessageEntity(MessageEntity.ITALIC, 5, 8) + + assert gift_info.parse_entities(MessageEntity.BOLD) == {entity: "test"} + assert gift_info.parse_entities() == {entity: "test", entity_2: "text"} + + with pytest.raises(RuntimeError, match="GiftInfo has no"): + GiftInfo( + gift=self.gift, + ).parse_entities() + + def test_equality(self, gift_info): + a = gift_info + b = GiftInfo(gift=self.gift) + c = GiftInfo( + gift=Gift( + id="some_other_gift_id", + sticker=Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular"), + star_count=5, + ), + ) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def accepted_gift_types(): + return AcceptedGiftTypes( + unlimited_gifts=AcceptedGiftTypesTestBase.unlimited_gifts, + limited_gifts=AcceptedGiftTypesTestBase.limited_gifts, + unique_gifts=AcceptedGiftTypesTestBase.unique_gifts, + premium_subscription=AcceptedGiftTypesTestBase.premium_subscription, + ) + + +class AcceptedGiftTypesTestBase: + unlimited_gifts = False + limited_gifts = True + unique_gifts = True + premium_subscription = True + + +class TestAcceptedGiftTypesWithoutRequest(AcceptedGiftTypesTestBase): + def test_slot_behaviour(self, accepted_gift_types): + for attr in accepted_gift_types.__slots__: + assert getattr(accepted_gift_types, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(accepted_gift_types)) == len( + set(mro_slots(accepted_gift_types)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "unlimited_gifts": self.unlimited_gifts, + "limited_gifts": self.limited_gifts, + "unique_gifts": self.unique_gifts, + "premium_subscription": self.premium_subscription, + } + accepted_gift_types = AcceptedGiftTypes.de_json(json_dict, offline_bot) + assert accepted_gift_types.api_kwargs == {} + assert accepted_gift_types.unlimited_gifts == self.unlimited_gifts + assert accepted_gift_types.limited_gifts == self.limited_gifts + assert accepted_gift_types.unique_gifts == self.unique_gifts + assert accepted_gift_types.premium_subscription == self.premium_subscription + + def test_to_dict(self, accepted_gift_types): + json_dict = accepted_gift_types.to_dict() + assert json_dict["unlimited_gifts"] == self.unlimited_gifts + assert json_dict["limited_gifts"] == self.limited_gifts + assert json_dict["unique_gifts"] == self.unique_gifts + assert json_dict["premium_subscription"] == self.premium_subscription + + def test_equality(self, accepted_gift_types): + a = accepted_gift_types + b = AcceptedGiftTypes( + self.unlimited_gifts, self.limited_gifts, self.unique_gifts, self.premium_subscription + ) + c = AcceptedGiftTypes( + not self.unlimited_gifts, + self.limited_gifts, + self.unique_gifts, + self.premium_subscription, + ) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/test_message.py b/tests/test_message.py index 7150a0502a1..e145720d705 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -50,6 +50,7 @@ MessageOriginChat, PaidMediaInfo, PaidMediaPreview, + PaidMessagePriceChanged, PassportData, PhotoSize, Poll, @@ -75,6 +76,15 @@ Voice, WebAppData, ) +from telegram._gifts import Gift, GiftInfo +from telegram._uniquegift import ( + UniqueGift, + UniqueGiftBackdrop, + UniqueGiftBackdropColors, + UniqueGiftInfo, + UniqueGiftModel, + UniqueGiftSymbol, +) from telegram._utils.datetime import UTC from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ODVInput @@ -232,6 +242,42 @@ def message(bot): {"message_thread_id": 123}, {"users_shared": UsersShared(1, users=[SharedUser(2, "user2"), SharedUser(3, "user3")])}, {"chat_shared": ChatShared(3, 4)}, + { + "gift": GiftInfo( + gift=Gift( + "gift_id", + Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular"), + 5, + ) + ) + }, + { + "unique_gift": UniqueGiftInfo( + gift=UniqueGift( + "human_readable_name", + "unique_name", + 2, + UniqueGiftModel( + "model_name", + Sticker("file_id1", "file_unique_id1", 512, 512, False, False, "regular"), + 10, + ), + UniqueGiftSymbol( + "symbol_name", + Sticker("file_id2", "file_unique_id2", 512, 512, True, True, "mask"), + 20, + ), + UniqueGiftBackdrop( + "backdrop_name", + UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F), + 30, + ), + ), + origin=UniqueGiftInfo.UPGRADE, + owned_gift_id="id", + transfer_star_count=10, + ) + }, { "giveaway": Giveaway( chats=[Chat(1, Chat.SUPERGROUP)], @@ -283,6 +329,8 @@ def message(bot): {"show_caption_above_media": True}, {"paid_media": PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)])}, {"refunded_payment": RefundedPayment("EUR", 243, "payload", "charge_id", "provider_id")}, + {"paid_star_count": 291}, + {"paid_message_price_changed": PaidMessagePriceChanged(291)}, ], ids=[ "reply", @@ -337,6 +385,8 @@ def message(bot): "message_thread_id", "users_shared", "chat_shared", + "gift", + "unique_gift", "giveaway", "giveaway_created", "giveaway_winners", @@ -356,6 +406,8 @@ def message(bot): "show_caption_above_media", "paid_media", "refunded_payment", + "paid_star_count", + "paid_message_price_changed", ], ) def message_params(bot, request): @@ -2820,6 +2872,31 @@ 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() + async def test_read_business_message(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["business_connection_id"] == message.business_connection_id + and kwargs["message_id"] == message.message_id, + ) + + assert check_shortcut_signature( + Message.read_business_message, + Bot.read_business_message, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.read_business_message, + message.get_bot(), + "read_business_message", + shortcut_kwargs=["chat_id", "message_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.read_business_message, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "read_business_message", make_assertion) + assert await message.read_business_message() + 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. diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index d0d73fa4b7b..40144f803d3 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -97,6 +97,22 @@ class ParamTypeCheckingExceptions: "thumbnail": str, # actual: Union[str, FileInput] "cover": str, # actual: Union[str, FileInput] }, + "InputProfilePhotoStatic": { + "photo": str, # actual: Union[str, FileInput] + }, + "InputProfilePhotoAnimated": { + "animation": str, # actual: Union[str, FileInput] + "main_frame_timestamp": float, # actual: Union[float, dtm.timedelta] + }, + "InputSticker": { + "sticker": str, # actual: Union[str, FileInput] + }, + "InputStoryContent.*": { + "photo": str, # actual: Union[str, FileInput] + "video": str, # actual: Union[str, FileInput] + "duration": float, # actual: dtm.timedelta + "cover_frame_timestamp": float, # actual: dtm.timedelta + }, "EncryptedPassportElement": { "data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress] }, @@ -144,11 +160,19 @@ class ParamTypeCheckingExceptions: "ReactionType": {"type"}, # attributes common to all subclasses "BackgroundType": {"type"}, # attributes common to all subclasses "BackgroundFill": {"type"}, # attributes common to all subclasses + "OwnedGift": {"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 "PaidMedia": {"type"}, # attributes common to all subclasses "InputPaidMedia": {"type", "media"}, # attributes common to all subclasses + "InputStoryContent": {"type"}, # attributes common to all subclasses + "StoryAreaType": {"type"}, # attributes common to all subclasses + # backwards compatibility for api 9.0 changes + # tags: deprecated NEXT.VERSION, bot api 9.0 + "BusinessConnection": {"can_reply"}, + "ChatFullInfo": {"can_send_gift"}, + "InputProfilePhoto": {"type"}, # attributes common to all subclasses } @@ -177,6 +201,10 @@ def ptb_extra_params(object_name: str) -> set[str]: r"TransactionPartner\w+": {"type"}, r"PaidMedia\w+": {"type"}, r"InputPaidMedia\w+": {"type"}, + r"InputProfilePhoto\w+": {"type"}, + r"OwnedGift\w+": {"type"}, + r"InputStoryContent\w+": {"type"}, + r"StoryAreaType\w+": {"type"}, } @@ -192,6 +220,11 @@ def ptb_ignored_params(object_name: str) -> set[str]: "send_venue": {"latitude", "longitude", "title", "address"}, "send_contact": {"phone_number", "first_name"}, # ----> + # backwards compatibility for api 9.0 changes + # tags: deprecated NEXT.VERSION, bot api 9.0 + "BusinessConnection": {"is_enabled"}, + "ChatFullInfo": {"accepted_gift_types"}, + "TransactionPartnerUser": {"transaction_type"}, } diff --git a/tests/test_ownedgift.py b/tests/test_ownedgift.py new file mode 100644 index 00000000000..b37794f3483 --- /dev/null +++ b/tests/test_ownedgift.py @@ -0,0 +1,461 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 as dtm +from collections.abc import Sequence +from copy import deepcopy + +import pytest + +from telegram import Dice, User +from telegram._files.sticker import Sticker +from telegram._gifts import Gift +from telegram._messageentity import MessageEntity +from telegram._ownedgift import OwnedGift, OwnedGiftRegular, OwnedGifts, OwnedGiftUnique +from telegram._uniquegift import ( + UniqueGift, + UniqueGiftBackdrop, + UniqueGiftBackdropColors, + UniqueGiftModel, + UniqueGiftSymbol, +) +from telegram._utils.datetime import UTC, to_timestamp +from telegram.constants import OwnedGiftType +from tests.auxil.slots import mro_slots + + +@pytest.fixture +def owned_gift(): + return OwnedGift(type=OwnedGiftTestBase.type) + + +class OwnedGiftTestBase: + type = OwnedGiftType.REGULAR + gift = Gift( + id="some_id", + sticker=Sticker( + file_id="file_id", + file_unique_id="file_unique_id", + width=512, + height=512, + is_animated=False, + is_video=False, + type="regular", + ), + star_count=5, + ) + unique_gift = UniqueGift( + base_name="human_readable", + name="unique_name", + number=10, + model=UniqueGiftModel( + name="model_name", + sticker=Sticker("file_id1", "file_unique_id1", 512, 512, False, False, "regular"), + rarity_per_mille=10, + ), + symbol=UniqueGiftSymbol( + name="symbol_name", + sticker=Sticker("file_id2", "file_unique_id2", 512, 512, True, True, "mask"), + rarity_per_mille=20, + ), + backdrop=UniqueGiftBackdrop( + name="backdrop_name", + colors=UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F), + rarity_per_mille=30, + ), + ) + send_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) + owned_gift_id = "not_real_id" + sender_user = User(1, "test user", False) + text = "test text" + entities = ( + MessageEntity(MessageEntity.BOLD, 0, 4), + MessageEntity(MessageEntity.ITALIC, 5, 8), + ) + is_private = True + is_saved = True + can_be_upgraded = True + was_refunded = False + convert_star_count = 100 + prepaid_upgrade_star_count = 200 + can_be_transferred = True + transfer_star_count = 300 + + +class TestOwnedGiftWithoutRequest(OwnedGiftTestBase): + def test_slot_behaviour(self, owned_gift): + inst = owned_gift + 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_type_enum_conversion(self, owned_gift): + assert type(OwnedGift("regular").type) is OwnedGiftType + assert OwnedGift("unknown").type == "unknown" + + def test_de_json(self, offline_bot): + data = {"type": "unknown"} + paid_media = OwnedGift.de_json(data, offline_bot) + assert paid_media.api_kwargs == {} + assert paid_media.type == "unknown" + + @pytest.mark.parametrize( + ("og_type", "subclass", "gift"), + [ + ("regular", OwnedGiftRegular, OwnedGiftTestBase.gift), + ("unique", OwnedGiftUnique, OwnedGiftTestBase.unique_gift), + ], + ) + def test_de_json_subclass(self, offline_bot, og_type, subclass, gift): + json_dict = { + "type": og_type, + "gift": gift.to_dict(), + "send_date": to_timestamp(self.send_date), + "owned_gift_id": self.owned_gift_id, + "sender_user": self.sender_user.to_dict(), + "text": self.text, + "entities": [e.to_dict() for e in self.entities], + "is_private": self.is_private, + "is_saved": self.is_saved, + "can_be_upgraded": self.can_be_upgraded, + "was_refunded": self.was_refunded, + "convert_star_count": self.convert_star_count, + "prepaid_upgrade_star_count": self.prepaid_upgrade_star_count, + "can_be_transferred": self.can_be_transferred, + "transfer_star_count": self.transfer_star_count, + } + og = OwnedGift.de_json(json_dict, offline_bot) + + assert type(og) is subclass + assert set(og.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - { + "type" + } + assert og.type == og_type + + def test_to_dict(self, owned_gift): + assert owned_gift.to_dict() == {"type": owned_gift.type} + + def test_equality(self, owned_gift): + a = owned_gift + b = OwnedGift(self.type) + c = OwnedGift("unknown") + d = Dice(5, "test") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def owned_gift_regular(): + return OwnedGiftRegular( + gift=TestOwnedGiftRegularWithoutRequest.gift, + send_date=TestOwnedGiftRegularWithoutRequest.send_date, + owned_gift_id=TestOwnedGiftRegularWithoutRequest.owned_gift_id, + sender_user=TestOwnedGiftRegularWithoutRequest.sender_user, + text=TestOwnedGiftRegularWithoutRequest.text, + entities=TestOwnedGiftRegularWithoutRequest.entities, + is_private=TestOwnedGiftRegularWithoutRequest.is_private, + is_saved=TestOwnedGiftRegularWithoutRequest.is_saved, + can_be_upgraded=TestOwnedGiftRegularWithoutRequest.can_be_upgraded, + was_refunded=TestOwnedGiftRegularWithoutRequest.was_refunded, + convert_star_count=TestOwnedGiftRegularWithoutRequest.convert_star_count, + prepaid_upgrade_star_count=TestOwnedGiftRegularWithoutRequest.prepaid_upgrade_star_count, + ) + + +class TestOwnedGiftRegularWithoutRequest(OwnedGiftTestBase): + type = OwnedGiftType.REGULAR + + def test_slot_behaviour(self, owned_gift_regular): + inst = owned_gift_regular + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "gift": self.gift.to_dict(), + "send_date": to_timestamp(self.send_date), + "owned_gift_id": self.owned_gift_id, + "sender_user": self.sender_user.to_dict(), + "text": self.text, + "entities": [e.to_dict() for e in self.entities], + "is_private": self.is_private, + "is_saved": self.is_saved, + "can_be_upgraded": self.can_be_upgraded, + "was_refunded": self.was_refunded, + "convert_star_count": self.convert_star_count, + "prepaid_upgrade_star_count": self.prepaid_upgrade_star_count, + } + ogr = OwnedGiftRegular.de_json(json_dict, offline_bot) + assert ogr.gift == self.gift + assert ogr.send_date == self.send_date + assert ogr.owned_gift_id == self.owned_gift_id + assert ogr.sender_user == self.sender_user + assert ogr.text == self.text + assert ogr.entities == self.entities + assert ogr.is_private == self.is_private + assert ogr.is_saved == self.is_saved + assert ogr.can_be_upgraded == self.can_be_upgraded + assert ogr.was_refunded == self.was_refunded + assert ogr.convert_star_count == self.convert_star_count + assert ogr.prepaid_upgrade_star_count == self.prepaid_upgrade_star_count + assert ogr.api_kwargs == {} + + def test_to_dict(self, owned_gift_regular): + json_dict = owned_gift_regular.to_dict() + assert isinstance(json_dict, dict) + assert json_dict["type"] == self.type + assert json_dict["gift"] == self.gift.to_dict() + assert json_dict["send_date"] == to_timestamp(self.send_date) + assert json_dict["owned_gift_id"] == self.owned_gift_id + assert json_dict["sender_user"] == self.sender_user.to_dict() + assert json_dict["text"] == self.text + assert json_dict["entities"] == [e.to_dict() for e in self.entities] + assert json_dict["is_private"] == self.is_private + assert json_dict["is_saved"] == self.is_saved + assert json_dict["can_be_upgraded"] == self.can_be_upgraded + assert json_dict["was_refunded"] == self.was_refunded + assert json_dict["convert_star_count"] == self.convert_star_count + assert json_dict["prepaid_upgrade_star_count"] == self.prepaid_upgrade_star_count + + def test_parse_entity(self, owned_gift_regular): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + + assert owned_gift_regular.parse_entity(entity) == "test" + + with pytest.raises(RuntimeError, match="OwnedGiftRegular has no"): + OwnedGiftRegular( + gift=self.gift, + send_date=self.send_date, + ).parse_entity(entity) + + def test_parse_entities(self, owned_gift_regular): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + entity_2 = MessageEntity(MessageEntity.ITALIC, 5, 8) + + assert owned_gift_regular.parse_entities(MessageEntity.BOLD) == {entity: "test"} + assert owned_gift_regular.parse_entities() == {entity: "test", entity_2: "text"} + + with pytest.raises(RuntimeError, match="OwnedGiftRegular has no"): + OwnedGiftRegular( + gift=self.gift, + send_date=self.send_date, + ).parse_entities() + + def test_equality(self, owned_gift_regular): + a = owned_gift_regular + b = OwnedGiftRegular(deepcopy(self.gift), deepcopy(self.send_date)) + c = OwnedGiftRegular(self.gift, self.send_date + dtm.timedelta(seconds=1)) + d = Dice(5, "test") + + 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) + + +@pytest.fixture +def owned_gift_unique(): + return OwnedGiftUnique( + gift=TestOwnedGiftUniqueWithoutRequest.unique_gift, + send_date=TestOwnedGiftUniqueWithoutRequest.send_date, + owned_gift_id=TestOwnedGiftUniqueWithoutRequest.owned_gift_id, + sender_user=TestOwnedGiftUniqueWithoutRequest.sender_user, + is_saved=TestOwnedGiftUniqueWithoutRequest.is_saved, + can_be_transferred=TestOwnedGiftUniqueWithoutRequest.can_be_transferred, + transfer_star_count=TestOwnedGiftUniqueWithoutRequest.transfer_star_count, + ) + + +class TestOwnedGiftUniqueWithoutRequest(OwnedGiftTestBase): + type = OwnedGiftType.UNIQUE + + def test_slot_behaviour(self, owned_gift_unique): + inst = owned_gift_unique + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "gift": self.unique_gift.to_dict(), + "send_date": to_timestamp(self.send_date), + "owned_gift_id": self.owned_gift_id, + "sender_user": self.sender_user.to_dict(), + "is_saved": self.is_saved, + "can_be_transferred": self.can_be_transferred, + "transfer_star_count": self.transfer_star_count, + } + ogu = OwnedGiftUnique.de_json(json_dict, offline_bot) + assert ogu.gift == self.unique_gift + assert ogu.send_date == self.send_date + assert ogu.owned_gift_id == self.owned_gift_id + assert ogu.sender_user == self.sender_user + assert ogu.is_saved == self.is_saved + assert ogu.can_be_transferred == self.can_be_transferred + assert ogu.transfer_star_count == self.transfer_star_count + assert ogu.api_kwargs == {} + + def test_to_dict(self, owned_gift_unique): + json_dict = owned_gift_unique.to_dict() + assert isinstance(json_dict, dict) + assert json_dict["type"] == self.type + assert json_dict["gift"] == self.unique_gift.to_dict() + assert json_dict["send_date"] == to_timestamp(self.send_date) + assert json_dict["owned_gift_id"] == self.owned_gift_id + assert json_dict["sender_user"] == self.sender_user.to_dict() + assert json_dict["is_saved"] == self.is_saved + assert json_dict["can_be_transferred"] == self.can_be_transferred + assert json_dict["transfer_star_count"] == self.transfer_star_count + + def test_equality(self, owned_gift_unique): + a = owned_gift_unique + b = OwnedGiftUnique(deepcopy(self.unique_gift), deepcopy(self.send_date)) + c = OwnedGiftUnique(self.unique_gift, self.send_date + dtm.timedelta(seconds=1)) + d = Dice(5, "test") + + 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) + + +@pytest.fixture +def owned_gifts(request): + return OwnedGifts( + total_count=OwnedGiftsTestBase.total_count, + gifts=OwnedGiftsTestBase.gifts, + next_offset=OwnedGiftsTestBase.next_offset, + ) + + +class OwnedGiftsTestBase: + total_count = 2 + next_offset = "next_offset_str" + gifts: Sequence[OwnedGifts] = [ + OwnedGiftRegular( + gift=Gift( + id="id1", + sticker=Sticker( + file_id="file_id", + file_unique_id="file_unique_id", + width=512, + height=512, + is_animated=False, + is_video=False, + type="regular", + ), + star_count=5, + total_count=5, + remaining_count=5, + upgrade_star_count=5, + ), + send_date=dtm.datetime.now(tz=UTC).replace(microsecond=0), + owned_gift_id="some_id_1", + ), + OwnedGiftUnique( + gift=UniqueGift( + base_name="human_readable", + name="unique_name", + number=10, + model=UniqueGiftModel( + name="model_name", + sticker=Sticker( + "file_id1", "file_unique_id1", 512, 512, False, False, "regular" + ), + rarity_per_mille=10, + ), + symbol=UniqueGiftSymbol( + name="symbol_name", + sticker=Sticker("file_id2", "file_unique_id2", 512, 512, True, True, "mask"), + rarity_per_mille=20, + ), + backdrop=UniqueGiftBackdrop( + name="backdrop_name", + colors=UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F), + rarity_per_mille=30, + ), + ), + send_date=dtm.datetime.now(tz=UTC).replace(microsecond=0), + owned_gift_id="some_id_2", + ), + ] + + +class TestOwnedGiftsWithoutRequest(OwnedGiftsTestBase): + def test_slot_behaviour(self, owned_gifts): + for attr in owned_gifts.__slots__: + assert getattr(owned_gifts, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(owned_gifts)) == len(set(mro_slots(owned_gifts))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "total_count": self.total_count, + "gifts": [gift.to_dict() for gift in self.gifts], + "next_offset": self.next_offset, + } + owned_gifts = OwnedGifts.de_json(json_dict, offline_bot) + assert owned_gifts.api_kwargs == {} + + assert owned_gifts.total_count == self.total_count + assert owned_gifts.gifts == tuple(self.gifts) + assert type(owned_gifts.gifts[0]) is OwnedGiftRegular + assert type(owned_gifts.gifts[1]) is OwnedGiftUnique + assert owned_gifts.next_offset == self.next_offset + + def test_to_dict(self, owned_gifts): + gifts_dict = owned_gifts.to_dict() + + assert isinstance(gifts_dict, dict) + assert gifts_dict["total_count"] == self.total_count + assert gifts_dict["gifts"] == [gift.to_dict() for gift in self.gifts] + assert gifts_dict["next_offset"] == self.next_offset + + def test_equality(self, owned_gifts): + a = owned_gifts + b = OwnedGifts(self.total_count, self.gifts) + c = OwnedGifts(self.total_count - 1, self.gifts[:1]) + d = Dice(5, "test") + + 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/test_paidmessagepricechanged.py b/tests/test_paidmessagepricechanged.py new file mode 100644 index 00000000000..b97eafbab93 --- /dev/null +++ b/tests/test_paidmessagepricechanged.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 Dice, PaidMessagePriceChanged +from tests.auxil.slots import mro_slots + + +class PaidMessagePriceChangedTestBase: + paid_message_star_count = 291 + + +@pytest.fixture(scope="module") +def paid_message_price_changed(): + return PaidMessagePriceChanged(PaidMessagePriceChangedTestBase.paid_message_star_count) + + +class TestPaidMessagePriceChangedWithoutRequest(PaidMessagePriceChangedTestBase): + def test_slot_behaviour(self, paid_message_price_changed): + for attr in paid_message_price_changed.__slots__: + assert ( + getattr(paid_message_price_changed, attr, "err") != "err" + ), f"got extra slot '{attr}'" + assert len(mro_slots(paid_message_price_changed)) == len( + set(mro_slots(paid_message_price_changed)) + ), "duplicate slot" + + def test_to_dict(self, paid_message_price_changed): + pmpc_dict = paid_message_price_changed.to_dict() + assert isinstance(pmpc_dict, dict) + assert pmpc_dict["paid_message_star_count"] == self.paid_message_star_count + + def test_de_json(self, offline_bot): + json_dict = {"paid_message_star_count": self.paid_message_star_count} + pmpc = PaidMessagePriceChanged.de_json(json_dict, offline_bot) + assert isinstance(pmpc, PaidMessagePriceChanged) + assert pmpc.paid_message_star_count == self.paid_message_star_count + assert pmpc.api_kwargs == {} + + def test_equality(self): + pmpc1 = PaidMessagePriceChanged(self.paid_message_star_count) + pmpc2 = PaidMessagePriceChanged(self.paid_message_star_count) + pmpc3 = PaidMessagePriceChanged(3) + dice = Dice(5, "emoji") + + assert pmpc1 == pmpc2 + assert hash(pmpc1) == hash(pmpc2) + + assert pmpc1 != pmpc3 + assert hash(pmpc1) != hash(pmpc3) + + assert pmpc1 != dice + assert hash(pmpc1) != hash(dice) diff --git a/tests/test_storyarea.py b/tests/test_storyarea.py new file mode 100644 index 00000000000..dd9d043965e --- /dev/null +++ b/tests/test_storyarea.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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._dice import Dice +from telegram._reaction import ReactionTypeEmoji +from telegram._storyarea import ( + LocationAddress, + StoryArea, + StoryAreaPosition, + StoryAreaType, + StoryAreaTypeLink, + StoryAreaTypeLocation, + StoryAreaTypeSuggestedReaction, + StoryAreaTypeUniqueGift, + StoryAreaTypeWeather, +) +from telegram.constants import StoryAreaTypeType +from tests.auxil.slots import mro_slots + + +@pytest.fixture +def story_area_position(): + return StoryAreaPosition( + x_percentage=StoryAreaPositionTestBase.x_percentage, + y_percentage=StoryAreaPositionTestBase.y_percentage, + width_percentage=StoryAreaPositionTestBase.width_percentage, + height_percentage=StoryAreaPositionTestBase.height_percentage, + rotation_angle=StoryAreaPositionTestBase.rotation_angle, + corner_radius_percentage=StoryAreaPositionTestBase.corner_radius_percentage, + ) + + +class StoryAreaPositionTestBase: + x_percentage = 50.0 + y_percentage = 10.0 + width_percentage = 15 + height_percentage = 15 + rotation_angle = 0.0 + corner_radius_percentage = 8.0 + + +class TestStoryAreaPositionWithoutRequest(StoryAreaPositionTestBase): + def test_slot_behaviour(self, story_area_position): + inst = story_area_position + 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, story_area_position): + assert story_area_position.x_percentage == self.x_percentage + assert story_area_position.y_percentage == self.y_percentage + assert story_area_position.width_percentage == self.width_percentage + assert story_area_position.height_percentage == self.height_percentage + assert story_area_position.rotation_angle == self.rotation_angle + assert story_area_position.corner_radius_percentage == self.corner_radius_percentage + + def test_to_dict(self, story_area_position): + json_dict = story_area_position.to_dict() + assert json_dict["x_percentage"] == self.x_percentage + assert json_dict["y_percentage"] == self.y_percentage + assert json_dict["width_percentage"] == self.width_percentage + assert json_dict["height_percentage"] == self.height_percentage + assert json_dict["rotation_angle"] == self.rotation_angle + assert json_dict["corner_radius_percentage"] == self.corner_radius_percentage + + def test_equality(self, story_area_position): + a = story_area_position + b = StoryAreaPosition( + self.x_percentage, + self.y_percentage, + self.width_percentage, + self.height_percentage, + self.rotation_angle, + self.corner_radius_percentage, + ) + c = StoryAreaPosition( + self.x_percentage + 10.0, + self.y_percentage, + self.width_percentage, + self.height_percentage, + self.rotation_angle, + self.corner_radius_percentage, + ) + d = Dice(5, "test") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def location_address(): + return LocationAddress( + country_code=LocationAddressTestBase.country_code, + state=LocationAddressTestBase.state, + city=LocationAddressTestBase.city, + street=LocationAddressTestBase.street, + ) + + +class LocationAddressTestBase: + country_code = "CC" + state = "State" + city = "City" + street = "12 downtown" + + +class TestLocationAddressWithoutRequest(LocationAddressTestBase): + def test_slot_behaviour(self, location_address): + inst = location_address + 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, location_address): + assert location_address.country_code == self.country_code + assert location_address.state == self.state + assert location_address.city == self.city + assert location_address.street == self.street + + def test_to_dict(self, location_address): + json_dict = location_address.to_dict() + assert json_dict["country_code"] == self.country_code + assert json_dict["state"] == self.state + assert json_dict["city"] == self.city + assert json_dict["street"] == self.street + + def test_equality(self, location_address): + a = location_address + b = LocationAddress(self.country_code, self.state, self.city, self.street) + c = LocationAddress("some_other_code", self.state, self.city, self.street) + d = Dice(5, "test") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def story_area(): + return StoryArea( + position=StoryAreaTestBase.position, + type=StoryAreaTestBase.type, + ) + + +class StoryAreaTestBase: + position = StoryAreaPosition( + x_percentage=50.0, + y_percentage=10.0, + width_percentage=15, + height_percentage=15, + rotation_angle=0.0, + corner_radius_percentage=8.0, + ) + type = StoryAreaTypeLink(url="some_url") + + +class TestStoryAreaWithoutRequest(StoryAreaTestBase): + def test_slot_behaviour(self, story_area): + inst = story_area + 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, story_area): + assert story_area.position == self.position + assert story_area.type == self.type + + def test_to_dict(self, story_area): + json_dict = story_area.to_dict() + assert json_dict["position"] == self.position.to_dict() + assert json_dict["type"] == self.type.to_dict() + + def test_equality(self, story_area): + a = story_area + b = StoryArea(self.position, self.type) + c = StoryArea(self.position, StoryAreaTypeLink(url="some_other_url")) + d = Dice(5, "test") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def story_area_type(): + return StoryAreaType(type=StoryAreaTypeTestBase.type) + + +class StoryAreaTypeTestBase: + type = StoryAreaTypeType.LOCATION + latitude = 100.5 + longitude = 200.5 + address = LocationAddress( + country_code="cc", + state="State", + city="City", + street="12 downtown", + ) + reaction_type = ReactionTypeEmoji(emoji="emoji") + is_dark = False + is_flipped = False + url = "http_url" + temperature = 35.0 + emoji = "emoji" + background_color = 0xFF66CCFF + name = "unique_gift_name" + + +class TestStoryAreaTypeWithoutRequest(StoryAreaTypeTestBase): + def test_slot_behaviour(self, story_area_type): + inst = story_area_type + 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, story_area_type): + assert story_area_type.type == self.type + + def test_type_enum_conversion(self, story_area_type): + assert type(StoryAreaType("location").type) is StoryAreaTypeType + assert StoryAreaType("unknown").type == "unknown" + + def test_to_dict(self, story_area_type): + assert story_area_type.to_dict() == {"type": self.type} + + def test_equality(self, story_area_type): + a = story_area_type + b = StoryAreaType(self.type) + c = StoryAreaType("unknown") + d = Dice(5, "test") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def story_area_type_location(): + return StoryAreaTypeLocation( + latitude=TestStoryAreaTypeLocationWithoutRequest.latitude, + longitude=TestStoryAreaTypeLocationWithoutRequest.longitude, + address=TestStoryAreaTypeLocationWithoutRequest.address, + ) + + +class TestStoryAreaTypeLocationWithoutRequest(StoryAreaTypeTestBase): + type = StoryAreaTypeType.LOCATION + + def test_slot_behaviour(self, story_area_type_location): + inst = story_area_type_location + 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, story_area_type_location): + assert story_area_type_location.type == self.type + assert story_area_type_location.latitude == self.latitude + assert story_area_type_location.longitude == self.longitude + assert story_area_type_location.address == self.address + + def test_to_dict(self, story_area_type_location): + json_dict = story_area_type_location.to_dict() + assert isinstance(json_dict, dict) + assert json_dict["type"] == self.type + assert json_dict["latitude"] == self.latitude + assert json_dict["longitude"] == self.longitude + assert json_dict["address"] == self.address.to_dict() + + def test_equality(self, story_area_type_location): + a = story_area_type_location + b = StoryAreaTypeLocation(self.latitude, self.longitude, self.address) + c = StoryAreaTypeLocation(self.latitude + 0.5, self.longitude, self.address) + d = Dice(5, "test") + + 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) + + +@pytest.fixture +def story_area_type_suggested_reaction(): + return StoryAreaTypeSuggestedReaction( + reaction_type=TestStoryAreaTypeSuggestedReactionWithoutRequest.reaction_type, + is_dark=TestStoryAreaTypeSuggestedReactionWithoutRequest.is_dark, + is_flipped=TestStoryAreaTypeSuggestedReactionWithoutRequest.is_flipped, + ) + + +class TestStoryAreaTypeSuggestedReactionWithoutRequest(StoryAreaTypeTestBase): + type = StoryAreaTypeType.SUGGESTED_REACTION + + def test_slot_behaviour(self, story_area_type_suggested_reaction): + inst = story_area_type_suggested_reaction + 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, story_area_type_suggested_reaction): + assert story_area_type_suggested_reaction.type == self.type + assert story_area_type_suggested_reaction.reaction_type == self.reaction_type + assert story_area_type_suggested_reaction.is_dark is self.is_dark + assert story_area_type_suggested_reaction.is_flipped is self.is_flipped + + def test_to_dict(self, story_area_type_suggested_reaction): + json_dict = story_area_type_suggested_reaction.to_dict() + assert isinstance(json_dict, dict) + assert json_dict["type"] == self.type + assert json_dict["reaction_type"] == self.reaction_type.to_dict() + assert json_dict["is_dark"] is self.is_dark + assert json_dict["is_flipped"] is self.is_flipped + + def test_equality(self, story_area_type_suggested_reaction): + a = story_area_type_suggested_reaction + b = StoryAreaTypeSuggestedReaction(self.reaction_type, self.is_dark, self.is_flipped) + c = StoryAreaTypeSuggestedReaction(self.reaction_type, not self.is_dark, self.is_flipped) + d = Dice(5, "test") + + 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) + + +@pytest.fixture +def story_area_type_link(): + return StoryAreaTypeLink( + url=TestStoryAreaTypeLinkWithoutRequest.url, + ) + + +class TestStoryAreaTypeLinkWithoutRequest(StoryAreaTypeTestBase): + type = StoryAreaTypeType.LINK + + def test_slot_behaviour(self, story_area_type_link): + inst = story_area_type_link + 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, story_area_type_link): + assert story_area_type_link.type == self.type + assert story_area_type_link.url == self.url + + def test_to_dict(self, story_area_type_link): + json_dict = story_area_type_link.to_dict() + assert isinstance(json_dict, dict) + assert json_dict["type"] == self.type + assert json_dict["url"] == self.url + + def test_equality(self, story_area_type_link): + a = story_area_type_link + b = StoryAreaTypeLink(self.url) + c = StoryAreaTypeLink("other_http_url") + d = Dice(5, "test") + + 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) + + +@pytest.fixture +def story_area_type_weather(): + return StoryAreaTypeWeather( + temperature=TestStoryAreaTypeWeatherWithoutRequest.temperature, + emoji=TestStoryAreaTypeWeatherWithoutRequest.emoji, + background_color=TestStoryAreaTypeWeatherWithoutRequest.background_color, + ) + + +class TestStoryAreaTypeWeatherWithoutRequest(StoryAreaTypeTestBase): + type = StoryAreaTypeType.WEATHER + + def test_slot_behaviour(self, story_area_type_weather): + inst = story_area_type_weather + 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, story_area_type_weather): + assert story_area_type_weather.type == self.type + assert story_area_type_weather.temperature == self.temperature + assert story_area_type_weather.emoji == self.emoji + assert story_area_type_weather.background_color == self.background_color + + def test_to_dict(self, story_area_type_weather): + json_dict = story_area_type_weather.to_dict() + assert isinstance(json_dict, dict) + assert json_dict["type"] == self.type + assert json_dict["temperature"] == self.temperature + assert json_dict["emoji"] == self.emoji + assert json_dict["background_color"] == self.background_color + + def test_equality(self, story_area_type_weather): + a = story_area_type_weather + b = StoryAreaTypeWeather(self.temperature, self.emoji, self.background_color) + c = StoryAreaTypeWeather(self.temperature - 5.0, self.emoji, self.background_color) + d = Dice(5, "test") + + 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) + + +@pytest.fixture +def story_area_type_unique_gift(): + return StoryAreaTypeUniqueGift( + name=TestStoryAreaTypeUniqueGiftWithoutRequest.name, + ) + + +class TestStoryAreaTypeUniqueGiftWithoutRequest(StoryAreaTypeTestBase): + type = StoryAreaTypeType.UNIQUE_GIFT + + def test_slot_behaviour(self, story_area_type_unique_gift): + inst = story_area_type_unique_gift + 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, story_area_type_unique_gift): + assert story_area_type_unique_gift.type == self.type + assert story_area_type_unique_gift.name == self.name + + def test_to_dict(self, story_area_type_unique_gift): + json_dict = story_area_type_unique_gift.to_dict() + assert isinstance(json_dict, dict) + assert json_dict["type"] == self.type + assert json_dict["name"] == self.name + + def test_equality(self, story_area_type_unique_gift): + a = story_area_type_unique_gift + b = StoryAreaTypeUniqueGift(self.name) + c = StoryAreaTypeUniqueGift("other_name") + d = Dice(5, "test") + + 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/test_uniquegift.py b/tests/test_uniquegift.py new file mode 100644 index 00000000000..051974b959b --- /dev/null +++ b/tests/test_uniquegift.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 ( + BotCommand, + Sticker, + UniqueGift, + UniqueGiftBackdrop, + UniqueGiftBackdropColors, + UniqueGiftInfo, + UniqueGiftModel, + UniqueGiftSymbol, +) +from tests.auxil.slots import mro_slots + + +@pytest.fixture +def unique_gift(): + return UniqueGift( + base_name=UniqueGiftTestBase.base_name, + name=UniqueGiftTestBase.name, + number=UniqueGiftTestBase.number, + model=UniqueGiftTestBase.model, + symbol=UniqueGiftTestBase.symbol, + backdrop=UniqueGiftTestBase.backdrop, + ) + + +class UniqueGiftTestBase: + base_name = "human_readable" + name = "unique_name" + number = 10 + model = UniqueGiftModel( + name="model_name", + sticker=Sticker("file_id1", "file_unique_id1", 512, 512, False, False, "regular"), + rarity_per_mille=10, + ) + symbol = UniqueGiftSymbol( + name="symbol_name", + sticker=Sticker("file_id2", "file_unique_id2", 512, 512, True, True, "mask"), + rarity_per_mille=20, + ) + backdrop = UniqueGiftBackdrop( + name="backdrop_name", + colors=UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F), + rarity_per_mille=30, + ) + + +class TestUniqueGiftWithoutRequest(UniqueGiftTestBase): + def test_slot_behaviour(self, unique_gift): + for attr in unique_gift.__slots__: + assert getattr(unique_gift, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(unique_gift)) == len(set(mro_slots(unique_gift))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "base_name": self.base_name, + "name": self.name, + "number": self.number, + "model": self.model.to_dict(), + "symbol": self.symbol.to_dict(), + "backdrop": self.backdrop.to_dict(), + } + unique_gift = UniqueGift.de_json(json_dict, offline_bot) + assert unique_gift.api_kwargs == {} + + assert unique_gift.base_name == self.base_name + assert unique_gift.name == self.name + assert unique_gift.number == self.number + assert unique_gift.model == self.model + assert unique_gift.symbol == self.symbol + assert unique_gift.backdrop == self.backdrop + + def test_to_dict(self, unique_gift): + gift_dict = unique_gift.to_dict() + + assert isinstance(gift_dict, dict) + assert gift_dict["base_name"] == self.base_name + assert gift_dict["name"] == self.name + assert gift_dict["number"] == self.number + assert gift_dict["model"] == self.model.to_dict() + assert gift_dict["symbol"] == self.symbol.to_dict() + assert gift_dict["backdrop"] == self.backdrop.to_dict() + + def test_equality(self, unique_gift): + a = unique_gift + b = UniqueGift( + self.base_name, + self.name, + self.number, + self.model, + self.symbol, + self.backdrop, + ) + c = UniqueGift( + "other_base_name", + self.name, + self.number, + self.model, + self.symbol, + self.backdrop, + ) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def unique_gift_model(): + return UniqueGiftModel( + name=UniqueGiftModelTestBase.name, + sticker=UniqueGiftModelTestBase.sticker, + rarity_per_mille=UniqueGiftModelTestBase.rarity_per_mille, + ) + + +class UniqueGiftModelTestBase: + name = "model_name" + sticker = Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular") + rarity_per_mille = 10 + + +class TestUniqueGiftModelWithoutRequest(UniqueGiftModelTestBase): + def test_slot_behaviour(self, unique_gift_model): + for attr in unique_gift_model.__slots__: + assert getattr(unique_gift_model, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(unique_gift_model)) == len( + set(mro_slots(unique_gift_model)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "name": self.name, + "sticker": self.sticker.to_dict(), + "rarity_per_mille": self.rarity_per_mille, + } + unique_gift_model = UniqueGiftModel.de_json(json_dict, offline_bot) + assert unique_gift_model.api_kwargs == {} + assert unique_gift_model.name == self.name + assert unique_gift_model.sticker == self.sticker + assert unique_gift_model.rarity_per_mille == self.rarity_per_mille + + def test_to_dict(self, unique_gift_model): + json_dict = unique_gift_model.to_dict() + assert json_dict["name"] == self.name + assert json_dict["sticker"] == self.sticker.to_dict() + assert json_dict["rarity_per_mille"] == self.rarity_per_mille + + def test_equality(self, unique_gift_model): + a = unique_gift_model + b = UniqueGiftModel(self.name, self.sticker, self.rarity_per_mille) + c = UniqueGiftModel("other_name", self.sticker, self.rarity_per_mille) + d = UniqueGiftSymbol(self.name, self.sticker, self.rarity_per_mille) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def unique_gift_symbol(): + return UniqueGiftSymbol( + name=UniqueGiftSymbolTestBase.name, + sticker=UniqueGiftSymbolTestBase.sticker, + rarity_per_mille=UniqueGiftSymbolTestBase.rarity_per_mille, + ) + + +class UniqueGiftSymbolTestBase: + name = "symbol_name" + sticker = Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular") + rarity_per_mille = 20 + + +class TestUniqueGiftSymbolWithoutRequest(UniqueGiftSymbolTestBase): + def test_slot_behaviour(self, unique_gift_symbol): + for attr in unique_gift_symbol.__slots__: + assert getattr(unique_gift_symbol, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(unique_gift_symbol)) == len( + set(mro_slots(unique_gift_symbol)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "name": self.name, + "sticker": self.sticker.to_dict(), + "rarity_per_mille": self.rarity_per_mille, + } + unique_gift_symbol = UniqueGiftSymbol.de_json(json_dict, offline_bot) + assert unique_gift_symbol.api_kwargs == {} + assert unique_gift_symbol.name == self.name + assert unique_gift_symbol.sticker == self.sticker + assert unique_gift_symbol.rarity_per_mille == self.rarity_per_mille + + def test_to_dict(self, unique_gift_symbol): + json_dict = unique_gift_symbol.to_dict() + assert json_dict["name"] == self.name + assert json_dict["sticker"] == self.sticker.to_dict() + assert json_dict["rarity_per_mille"] == self.rarity_per_mille + + def test_equality(self, unique_gift_symbol): + a = unique_gift_symbol + b = UniqueGiftSymbol(self.name, self.sticker, self.rarity_per_mille) + c = UniqueGiftSymbol("other_name", self.sticker, self.rarity_per_mille) + d = UniqueGiftModel(self.name, self.sticker, self.rarity_per_mille) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def unique_gift_backdrop(): + return UniqueGiftBackdrop( + name=UniqueGiftBackdropTestBase.name, + colors=UniqueGiftBackdropTestBase.colors, + rarity_per_mille=UniqueGiftBackdropTestBase.rarity_per_mille, + ) + + +class UniqueGiftBackdropTestBase: + name = "backdrop_name" + colors = UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F) + rarity_per_mille = 30 + + +class TestUniqueGiftBackdropWithoutRequest(UniqueGiftBackdropTestBase): + def test_slot_behaviour(self, unique_gift_backdrop): + for attr in unique_gift_backdrop.__slots__: + assert getattr(unique_gift_backdrop, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(unique_gift_backdrop)) == len( + set(mro_slots(unique_gift_backdrop)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "name": self.name, + "colors": self.colors.to_dict(), + "rarity_per_mille": self.rarity_per_mille, + } + unique_gift_backdrop = UniqueGiftBackdrop.de_json(json_dict, offline_bot) + assert unique_gift_backdrop.api_kwargs == {} + assert unique_gift_backdrop.name == self.name + assert unique_gift_backdrop.colors == self.colors + assert unique_gift_backdrop.rarity_per_mille == self.rarity_per_mille + + def test_to_dict(self, unique_gift_backdrop): + json_dict = unique_gift_backdrop.to_dict() + assert json_dict["name"] == self.name + assert json_dict["colors"] == self.colors.to_dict() + assert json_dict["rarity_per_mille"] == self.rarity_per_mille + + def test_equality(self, unique_gift_backdrop): + a = unique_gift_backdrop + b = UniqueGiftBackdrop(self.name, self.colors, self.rarity_per_mille) + c = UniqueGiftBackdrop("other_name", self.colors, self.rarity_per_mille) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def unique_gift_backdrop_colors(): + return UniqueGiftBackdropColors( + center_color=UniqueGiftBackdropColorsTestBase.center_color, + edge_color=UniqueGiftBackdropColorsTestBase.edge_color, + symbol_color=UniqueGiftBackdropColorsTestBase.symbol_color, + text_color=UniqueGiftBackdropColorsTestBase.text_color, + ) + + +class UniqueGiftBackdropColorsTestBase: + center_color = 0x00FF00 + edge_color = 0xEE00FF + symbol_color = 0xAA22BB + text_color = 0x20FE8F + + +class TestUniqueGiftBackdropColorsWithoutRequest(UniqueGiftBackdropColorsTestBase): + def test_slot_behaviour(self, unique_gift_backdrop_colors): + for attr in unique_gift_backdrop_colors.__slots__: + assert ( + getattr(unique_gift_backdrop_colors, attr, "err") != "err" + ), f"got extra slot '{attr}'" + assert len(mro_slots(unique_gift_backdrop_colors)) == len( + set(mro_slots(unique_gift_backdrop_colors)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "center_color": self.center_color, + "edge_color": self.edge_color, + "symbol_color": self.symbol_color, + "text_color": self.text_color, + } + unique_gift_backdrop_colors = UniqueGiftBackdropColors.de_json(json_dict, offline_bot) + assert unique_gift_backdrop_colors.api_kwargs == {} + assert unique_gift_backdrop_colors.center_color == self.center_color + assert unique_gift_backdrop_colors.edge_color == self.edge_color + assert unique_gift_backdrop_colors.symbol_color == self.symbol_color + assert unique_gift_backdrop_colors.text_color == self.text_color + + def test_to_dict(self, unique_gift_backdrop_colors): + json_dict = unique_gift_backdrop_colors.to_dict() + assert json_dict["center_color"] == self.center_color + assert json_dict["edge_color"] == self.edge_color + assert json_dict["symbol_color"] == self.symbol_color + assert json_dict["text_color"] == self.text_color + + def test_equality(self, unique_gift_backdrop_colors): + a = unique_gift_backdrop_colors + b = UniqueGiftBackdropColors( + center_color=self.center_color, + edge_color=self.edge_color, + symbol_color=self.symbol_color, + text_color=self.text_color, + ) + c = UniqueGiftBackdropColors( + center_color=0x000000, + edge_color=self.edge_color, + symbol_color=self.symbol_color, + text_color=self.text_color, + ) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture +def unique_gift_info(): + return UniqueGiftInfo( + gift=UniqueGiftInfoTestBase.gift, + origin=UniqueGiftInfoTestBase.origin, + owned_gift_id=UniqueGiftInfoTestBase.owned_gift_id, + transfer_star_count=UniqueGiftInfoTestBase.transfer_star_count, + ) + + +class UniqueGiftInfoTestBase: + gift = UniqueGift( + "human_readable_name", + "unique_name", + 10, + UniqueGiftModel( + name="model_name", + sticker=Sticker("file_id1", "file_unique_id1", 512, 512, False, False, "regular"), + rarity_per_mille=10, + ), + UniqueGiftSymbol( + name="symbol_name", + sticker=Sticker("file_id2", "file_unique_id2", 512, 512, True, True, "mask"), + rarity_per_mille=20, + ), + UniqueGiftBackdrop( + name="backdrop_name", + colors=UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F), + rarity_per_mille=2, + ), + ) + origin = UniqueGiftInfo.UPGRADE + owned_gift_id = "some_id" + transfer_star_count = 10 + + +class TestUniqueGiftInfoWithoutRequest(UniqueGiftInfoTestBase): + def test_slot_behaviour(self, unique_gift_info): + for attr in unique_gift_info.__slots__: + assert getattr(unique_gift_info, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(unique_gift_info)) == len( + set(mro_slots(unique_gift_info)) + ), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict = { + "gift": self.gift.to_dict(), + "origin": self.origin, + "owned_gift_id": self.owned_gift_id, + "transfer_star_count": self.transfer_star_count, + } + unique_gift_info = UniqueGiftInfo.de_json(json_dict, offline_bot) + assert unique_gift_info.api_kwargs == {} + assert unique_gift_info.gift == self.gift + assert unique_gift_info.origin == self.origin + assert unique_gift_info.owned_gift_id == self.owned_gift_id + assert unique_gift_info.transfer_star_count == self.transfer_star_count + + def test_to_dict(self, unique_gift_info): + json_dict = unique_gift_info.to_dict() + assert json_dict["gift"] == self.gift.to_dict() + assert json_dict["origin"] == self.origin + assert json_dict["owned_gift_id"] == self.owned_gift_id + assert json_dict["transfer_star_count"] == self.transfer_star_count + + def test_equality(self, unique_gift_info): + a = unique_gift_info + b = UniqueGiftInfo(self.gift, self.origin, self.owned_gift_id, self.transfer_star_count) + c = UniqueGiftInfo( + self.gift, UniqueGiftInfo.TRANSFER, self.owned_gift_id, self.transfer_star_count + ) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/test_user.py b/tests/test_user.py index b7ea5f8bd26..490aa6052ec 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -745,6 +745,36 @@ async def make_assertion(*_, **kwargs): text_entities="text_entities", ) + async def test_instance_method_gift_premium_subscription(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return ( + kwargs["user_id"] == user.id + and kwargs["month_count"] == 3 + and kwargs["star_count"] == 1000 + and kwargs["text"] == "text" + and kwargs["text_parse_mode"] == "text_parse_mode" + and kwargs["text_entities"] == "text_entities" + ) + + assert check_shortcut_signature( + user.gift_premium_subscription, Bot.gift_premium_subscription, ["user_id"], [] + ) + assert await check_shortcut_call( + user.gift_premium_subscription, + user.get_bot(), + "gift_premium_subscription", + ) + assert await check_defaults_handling(user.gift_premium_subscription, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "gift_premium_subscription", make_assertion) + assert await user.gift_premium_subscription( + month_count=3, + star_count=1000, + text="text", + text_parse_mode="text_parse_mode", + text_entities="text_entities", + ) + async def test_instance_method_verify_user(self, monkeypatch, user): async def make_assertion(*_, **kwargs): return ( From c57e9fa5d6634d4218bff8c74d88eb8b2b97b1c9 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 15 May 2025 21:57:07 +0200 Subject: [PATCH 31/36] Documentation Improvements (#4730) Co-authored-by: Poolitzer Co-authored-by: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> --- .github/CONTRIBUTING.rst | 60 ++++++++++--------- .../4692.dVZs28GuwTFnNJdWkvPbNv.toml | 2 +- .../4730.GsSUQSXzV8TF2Wav4CXRdd.toml | 9 +++ .../4733.BRLwsEuh76974FPJRuiBjf.toml | 2 +- .../4758.dSyCdBJWEJroH2GynR2VaJ.toml | 2 +- docs/source/examples.rst | 2 +- telegram/_files/inputmedia.py | 4 +- telegram/_user.py | 3 + telegram/request/_httpxrequest.py | 4 -- 9 files changed, 49 insertions(+), 39 deletions(-) create mode 100644 changes/unreleased/4730.GsSUQSXzV8TF2Wav4CXRdd.toml diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index b64a368ac43..475c41d203d 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -157,45 +157,47 @@ Check-list for PRs This checklist is a non-exhaustive reminder of things that should be done before a PR is merged, both for you as contributor and for the maintainers. Feel free to copy (parts of) the checklist to the PR description to remind you or the maintainers of open points or if you have questions on anything. -- Added ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION``, ``.. deprecated:: NEXT.VERSION`` or ``.. versionremoved:: NEXT.VERSION`` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes) -- Created new or adapted existing unit tests -- Documented code changes according to the `CSI standard `__ -- Added myself alphabetically to ``AUTHORS.rst`` (optional) -- Added new classes & modules to the docs and all suitable ``__all__`` s -- Checked the `Stability Policy `_ in case of deprecations or changes to documented behavior +.. code-block:: markdown -**If the PR contains API changes (otherwise, you can ignore this passage)** + ## Check-list for PRs -- Checked the Bot API specific sections of the `Stability Policy `_ -- Created a PR to remove functionality deprecated in the previous Bot API release (`see here `_) + - [ ] Added `.. versionadded:: NEXT.VERSION`, ``.. versionchanged:: NEXT.VERSION``, ``.. deprecated:: NEXT.VERSION`` or ``.. versionremoved:: NEXT.VERSION` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes) + - [ ] Created new or adapted existing unit tests + - [ ] Documented code changes according to the [CSI standard](https://standards.mousepawmedia.com/en/stable/csi.html) + - [ ] Added myself alphabetically to `AUTHORS.rst` (optional) + - [ ] Added new classes & modules to the docs and all suitable ``__all__`` s + - [ ] Checked the [Stability Policy](https://docs.python-telegram-bot.org/stability_policy.html) in case of deprecations or changes to documented behavior -- New classes: + **If the PR contains API changes (otherwise, you can ignore this passage)** - - Added ``self._id_attrs`` and corresponding documentation - - ``__init__`` accepts ``api_kwargs`` as kw-only + - [ ] Checked the Bot API specific sections of the [Stability Policy](https://docs.python-telegram-bot.org/stability_policy.html) + - [ ] Created a PR to remove functionality deprecated in the previous Bot API release ([see here](https://docs.python-telegram-bot.org/en/stable/stability_policy.html#case-2)) -- Added new shortcuts: + - New Classes - - In :class:`~telegram.Chat` & :class:`~telegram.User` for all methods that accept ``chat/user_id`` - - In :class:`~telegram.Message` for all methods that accept ``chat_id`` and ``message_id`` - - For new :class:`~telegram.Message` shortcuts: Added ``quote`` argument if methods accepts ``reply_to_message_id`` - - In :class:`~telegram.CallbackQuery` for all methods that accept either ``chat_id`` and ``message_id`` or ``inline_message_id`` + - [ ] Added `self._id_attrs` and corresponding documentation + - [ ] `__init__` accepts `api_kwargs` as keyword-only -- If relevant: + - Added New Shortcuts - - Added new constants at :mod:`telegram.constants` and shortcuts to them as class variables - - Link new and existing constants in docstrings instead of hard-coded numbers and strings - - Add new message types to :attr:`telegram.Message.effective_attachment` - - Added new handlers for new update types + - [ ] In [`telegram.Chat`](https://python-telegram-bot.readthedocs.io/en/stable/telegram.chat.html) \& [`telegram.User`](https://python-telegram-bot.readthedocs.io/en/stable/telegram.user.html) for all methods that accept `chat/user_id` + - [ ] In [`telegram.Message`](https://python-telegram-bot.readthedocs.io/en/stable/telegram.message.html) for all methods that accept `chat_id` and `message_id` + - [ ] For new `telegram.Message` shortcuts: Added `quote` argument if methods accept `reply_to_message_id` + - [ ] In [`telegram.CallbackQuery`](https://python-telegram-bot.readthedocs.io/en/stable/telegram.callbackquery.html) for all methods that accept either `chat_id` and `message_id` or `inline_message_id` - - Add the handlers to the warning loop in the :class:`~telegram.ext.ConversationHandler` + - If Relevant - - Added new filters for new message (sub)types - - 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`` (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` + - [ ] Added new constants at `telegram.constants` and shortcuts to them as class variables + - [ ] Linked new and existing constants in docstrings instead of hard-coded numbers and strings + - [ ] Added new message types to `telegram.Message.effective_attachment` + - [ ] Added new handlers for new update types + - [ ] Added the handlers to the warning loop in the [`telegram.ext.ConversationHandler`](https://python-telegram-bot.readthedocs.io/en/stable/telegram.ext.conversationhandler.html) + - [ ] Added new filters for new message (sub)types + - [ ] 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` (including the badge) and `telegram.constants.BOT_API_VERSION_INFO` + - [ ] Added logic for arbitrary callback data in `telegram.ext.ExtBot` for new methods that either accept a `reply_markup` in some form or have a return type that is/contains [`telegram.Message`](https://python-telegram-bot.readthedocs.io/en/stable/telegram.message.html) Documenting =========== diff --git a/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml b/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml index aebbd7e67c1..a70d3418d7c 100644 --- a/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml +++ b/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml @@ -1,4 +1,4 @@ -breaking = "Drop backward compatibility for `user_id` in `send_gift` by updating the order of parameters. Please adapt your code accordingly or use keyword arguments." +breaking = "Drop backward compatibility for ``user_id`` in ``send_gift`` by updating the order of parameters. Please adapt your code accordingly or use keyword arguments." [[pull_requests]] uid = "4692" author_uid = "Bibo-Joshi" diff --git a/changes/unreleased/4730.GsSUQSXzV8TF2Wav4CXRdd.toml b/changes/unreleased/4730.GsSUQSXzV8TF2Wav4CXRdd.toml new file mode 100644 index 00000000000..9a4d9369e2f --- /dev/null +++ b/changes/unreleased/4730.GsSUQSXzV8TF2Wav4CXRdd.toml @@ -0,0 +1,9 @@ +documentation = "Documentation Improvements. Among others, add missing ``Returns`` field in ``User.get_profile_photos``" +[[pull_requests]] +uid = "4730" +author_uid = "Bibo-Joshi" +closes_threads = [] +[[pull_requests]] +uid = "4740" +author_uid = "aelkheir" +closes_threads = [] diff --git a/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml b/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml index 579b6c3b37d..ebc3a8519f8 100644 --- a/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml +++ b/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml @@ -1,4 +1,4 @@ -bugfixes = "Ensure execution of ``Bot.shutdown`` even if ``Bot.get_me()`` fails in ``Bot.initialize()``" +bugfixes = "Ensure execution of ``Bot.shutdown()`` even if ``Bot.get_me()`` fails in ``Bot.initialize()``" [[pull_requests]] uid = "4733" author_uid = "Poolitzer" diff --git a/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml b/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml index 23ffc153339..c602a07af7c 100644 --- a/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml +++ b/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml @@ -1,4 +1,4 @@ -internal = "Fine Tune ``chango`` and Release Workflows" +internal = "Fine-tune ``chango`` and release workflows" [[pull_requests]] uid = "4758" author_uid = "Bibo-Joshi" diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 53815770685..ce87c73450e 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -61,7 +61,7 @@ for this one, too! :any:`examples.nestedconversationbot` ------------------------------------- -A even more complex example of a bot that uses the nested +An even more complex example of a bot that uses the nested ``ConversationHandler``\ s. While itโ€™s certainly not that complex that you couldnโ€™t built it without nested ``ConversationHanldler``\ s, it gives a good impression on how to work with them. Of course, there is a diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 017e1b423fe..2b7e6b21fd5 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -115,8 +115,8 @@ class InputPaidMedia(TelegramObject): """ Base class for Telegram InputPaidMedia Objects. Currently, it can be one of: - * :class:`telegram.InputMediaPhoto` - * :class:`telegram.InputMediaVideo` + * :class:`telegram.InputPaidMediaPhoto` + * :class:`telegram.InputPaidMediaVideo` .. seealso:: :wiki:`Working with Files and Media ` diff --git a/telegram/_user.py b/telegram/_user.py index 63cb9625046..6bb8dc08db1 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -247,6 +247,9 @@ async def get_profile_photos( For the documentation of the arguments, please see :meth:`telegram.Bot.get_user_profile_photos`. + Returns: + :class:`telegram.UserProfilePhotos` + """ return await self.get_bot().get_user_profile_photos( user_id=self.id, diff --git a/telegram/request/_httpxrequest.py b/telegram/request/_httpxrequest.py index 32b639526ee..bb35501f178 100644 --- a/telegram/request/_httpxrequest.py +++ b/telegram/request/_httpxrequest.py @@ -49,10 +49,6 @@ class HTTPXRequest(BaseRequest): Args: connection_pool_size (:obj:`int`, optional): Number of connections to keep in the connection pool. Defaults to ``1``. - - Note: - Independent of the value, one additional connection will be reserved for - :meth:`telegram.Bot.get_updates`. read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server. This value is used unless a different value is passed to :meth:`do_request`. From fc3863ac9a86d8ed5bd8307badac142aebf59a87 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 15 May 2025 22:18:11 +0200 Subject: [PATCH 32/36] Bump Version to v22.1 (#4791) --- .../4692.dVZs28GuwTFnNJdWkvPbNv.toml | 0 .../4730.GsSUQSXzV8TF2Wav4CXRdd.toml | 0 .../4733.BRLwsEuh76974FPJRuiBjf.toml | 0 .../4741.nVLBrFX4p8jTCBjMRqaYoQ.toml | 0 .../4742.oEA6MjYXMafdbu2akWT5tC.toml | 0 .../4743.SpMm4vvAMjEreykTcGwzcF.toml | 0 .../4744.a4tsF64kZPA2noP7HtTzTX.toml | 0 .../4745.emNmhxtvtTP9uLNQxpcVSj.toml | 0 .../4746.gWnX3BCxbvujQ8B2QejtTK.toml | 0 .../4747.MLmApvpGdwN7J24j7fXsDU.toml | 0 .../4748.j3cKusZZKqTLbc542K4sqJ.toml | 0 .../4756.JT5nmUmGRG6qDEh5ScMn5f.toml | 0 .../4758.dSyCdBJWEJroH2GynR2VaJ.toml | 0 .../4761.mmsngFA6b4ccdEzEpFTZS3.toml | 0 .../4762.PbcJGM8KPBMbKri3fdHKjh.toml | 0 .../4775.kkLon84t7Vy5REKRe9LwPH.toml | 0 .../4776.g83DxRk4WVWCC8rCd6ocFC.toml | 0 .../4777.Lhgz8xFtQjgrKM2KvNfxwD.toml | 0 .../4778.CeUSPNLbGGsqP2Vo4xKkdp.toml | 0 .../4779.UqcbJVKYxwTtrBEGDgb3VS.toml | 0 .../4791.QycEvQbiaiJKraKDhAfFWM.toml | 5 ++ telegram/_bot.py | 40 +++++++------- telegram/_business.py | 18 +++---- telegram/_chat.py | 4 +- telegram/_chatfullinfo.py | 14 ++--- telegram/_files/_inputstorycontent.py | 6 +-- telegram/_files/inputprofilephoto.py | 6 +-- telegram/_gifts.py | 4 +- telegram/_message.py | 18 +++---- telegram/_ownedgift.py | 8 +-- telegram/_paidmessagepricechanged.py | 2 +- telegram/_payment/stars/transactionpartner.py | 12 ++--- telegram/_storyarea.py | 18 +++---- telegram/_uniquegift.py | 12 ++--- telegram/_user.py | 2 +- telegram/_version.py | 2 +- telegram/constants.py | 52 +++++++++---------- telegram/ext/filters.py | 6 +-- 38 files changed, 117 insertions(+), 112 deletions(-) rename changes/{unreleased => 22.1_2025-05-15}/4692.dVZs28GuwTFnNJdWkvPbNv.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4730.GsSUQSXzV8TF2Wav4CXRdd.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4733.BRLwsEuh76974FPJRuiBjf.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4742.oEA6MjYXMafdbu2akWT5tC.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4743.SpMm4vvAMjEreykTcGwzcF.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4744.a4tsF64kZPA2noP7HtTzTX.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4745.emNmhxtvtTP9uLNQxpcVSj.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4746.gWnX3BCxbvujQ8B2QejtTK.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4747.MLmApvpGdwN7J24j7fXsDU.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4748.j3cKusZZKqTLbc542K4sqJ.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4756.JT5nmUmGRG6qDEh5ScMn5f.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4758.dSyCdBJWEJroH2GynR2VaJ.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4761.mmsngFA6b4ccdEzEpFTZS3.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4762.PbcJGM8KPBMbKri3fdHKjh.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4775.kkLon84t7Vy5REKRe9LwPH.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4776.g83DxRk4WVWCC8rCd6ocFC.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml (100%) rename changes/{unreleased => 22.1_2025-05-15}/4779.UqcbJVKYxwTtrBEGDgb3VS.toml (100%) create mode 100644 changes/22.1_2025-05-15/4791.QycEvQbiaiJKraKDhAfFWM.toml diff --git a/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml b/changes/22.1_2025-05-15/4692.dVZs28GuwTFnNJdWkvPbNv.toml similarity index 100% rename from changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml rename to changes/22.1_2025-05-15/4692.dVZs28GuwTFnNJdWkvPbNv.toml diff --git a/changes/unreleased/4730.GsSUQSXzV8TF2Wav4CXRdd.toml b/changes/22.1_2025-05-15/4730.GsSUQSXzV8TF2Wav4CXRdd.toml similarity index 100% rename from changes/unreleased/4730.GsSUQSXzV8TF2Wav4CXRdd.toml rename to changes/22.1_2025-05-15/4730.GsSUQSXzV8TF2Wav4CXRdd.toml diff --git a/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml b/changes/22.1_2025-05-15/4733.BRLwsEuh76974FPJRuiBjf.toml similarity index 100% rename from changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml rename to changes/22.1_2025-05-15/4733.BRLwsEuh76974FPJRuiBjf.toml diff --git a/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml b/changes/22.1_2025-05-15/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml similarity index 100% rename from changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml rename to changes/22.1_2025-05-15/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml diff --git a/changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml b/changes/22.1_2025-05-15/4742.oEA6MjYXMafdbu2akWT5tC.toml similarity index 100% rename from changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml rename to changes/22.1_2025-05-15/4742.oEA6MjYXMafdbu2akWT5tC.toml diff --git a/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml b/changes/22.1_2025-05-15/4743.SpMm4vvAMjEreykTcGwzcF.toml similarity index 100% rename from changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml rename to changes/22.1_2025-05-15/4743.SpMm4vvAMjEreykTcGwzcF.toml diff --git a/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml b/changes/22.1_2025-05-15/4744.a4tsF64kZPA2noP7HtTzTX.toml similarity index 100% rename from changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml rename to changes/22.1_2025-05-15/4744.a4tsF64kZPA2noP7HtTzTX.toml diff --git a/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml b/changes/22.1_2025-05-15/4745.emNmhxtvtTP9uLNQxpcVSj.toml similarity index 100% rename from changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml rename to changes/22.1_2025-05-15/4745.emNmhxtvtTP9uLNQxpcVSj.toml diff --git a/changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml b/changes/22.1_2025-05-15/4746.gWnX3BCxbvujQ8B2QejtTK.toml similarity index 100% rename from changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml rename to changes/22.1_2025-05-15/4746.gWnX3BCxbvujQ8B2QejtTK.toml diff --git a/changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml b/changes/22.1_2025-05-15/4747.MLmApvpGdwN7J24j7fXsDU.toml similarity index 100% rename from changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml rename to changes/22.1_2025-05-15/4747.MLmApvpGdwN7J24j7fXsDU.toml diff --git a/changes/unreleased/4748.j3cKusZZKqTLbc542K4sqJ.toml b/changes/22.1_2025-05-15/4748.j3cKusZZKqTLbc542K4sqJ.toml similarity index 100% rename from changes/unreleased/4748.j3cKusZZKqTLbc542K4sqJ.toml rename to changes/22.1_2025-05-15/4748.j3cKusZZKqTLbc542K4sqJ.toml diff --git a/changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml b/changes/22.1_2025-05-15/4756.JT5nmUmGRG6qDEh5ScMn5f.toml similarity index 100% rename from changes/unreleased/4756.JT5nmUmGRG6qDEh5ScMn5f.toml rename to changes/22.1_2025-05-15/4756.JT5nmUmGRG6qDEh5ScMn5f.toml diff --git a/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml b/changes/22.1_2025-05-15/4758.dSyCdBJWEJroH2GynR2VaJ.toml similarity index 100% rename from changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml rename to changes/22.1_2025-05-15/4758.dSyCdBJWEJroH2GynR2VaJ.toml diff --git a/changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml b/changes/22.1_2025-05-15/4761.mmsngFA6b4ccdEzEpFTZS3.toml similarity index 100% rename from changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml rename to changes/22.1_2025-05-15/4761.mmsngFA6b4ccdEzEpFTZS3.toml diff --git a/changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml b/changes/22.1_2025-05-15/4762.PbcJGM8KPBMbKri3fdHKjh.toml similarity index 100% rename from changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml rename to changes/22.1_2025-05-15/4762.PbcJGM8KPBMbKri3fdHKjh.toml diff --git a/changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml b/changes/22.1_2025-05-15/4775.kkLon84t7Vy5REKRe9LwPH.toml similarity index 100% rename from changes/unreleased/4775.kkLon84t7Vy5REKRe9LwPH.toml rename to changes/22.1_2025-05-15/4775.kkLon84t7Vy5REKRe9LwPH.toml diff --git a/changes/unreleased/4776.g83DxRk4WVWCC8rCd6ocFC.toml b/changes/22.1_2025-05-15/4776.g83DxRk4WVWCC8rCd6ocFC.toml similarity index 100% rename from changes/unreleased/4776.g83DxRk4WVWCC8rCd6ocFC.toml rename to changes/22.1_2025-05-15/4776.g83DxRk4WVWCC8rCd6ocFC.toml diff --git a/changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml b/changes/22.1_2025-05-15/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml similarity index 100% rename from changes/unreleased/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml rename to changes/22.1_2025-05-15/4777.Lhgz8xFtQjgrKM2KvNfxwD.toml diff --git a/changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml b/changes/22.1_2025-05-15/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml similarity index 100% rename from changes/unreleased/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml rename to changes/22.1_2025-05-15/4778.CeUSPNLbGGsqP2Vo4xKkdp.toml diff --git a/changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml b/changes/22.1_2025-05-15/4779.UqcbJVKYxwTtrBEGDgb3VS.toml similarity index 100% rename from changes/unreleased/4779.UqcbJVKYxwTtrBEGDgb3VS.toml rename to changes/22.1_2025-05-15/4779.UqcbJVKYxwTtrBEGDgb3VS.toml diff --git a/changes/22.1_2025-05-15/4791.QycEvQbiaiJKraKDhAfFWM.toml b/changes/22.1_2025-05-15/4791.QycEvQbiaiJKraKDhAfFWM.toml new file mode 100644 index 00000000000..c5db706c800 --- /dev/null +++ b/changes/22.1_2025-05-15/4791.QycEvQbiaiJKraKDhAfFWM.toml @@ -0,0 +1,5 @@ +other = "Bump Version to v22.1" +[[pull_requests]] +uid = "4791" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/telegram/_bot.py b/telegram/_bot.py index 43c350f1a79..90f6cf0bf42 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9386,7 +9386,7 @@ async def gift_premium_subscription( """ Gifts a Telegram Premium subscription to the given user. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: user_id (:obj:`int`): Unique identifier of the target user who will receive a Telegram @@ -9506,7 +9506,7 @@ async def get_business_account_gifts( Returns the gifts received and owned by a managed business account. Requires the :attr:`~telegram.BusinessBotRights.can_view_gifts_and_stars` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -9572,7 +9572,7 @@ async def get_business_account_star_balance( Returns the amount of Telegram Stars owned by a managed business account. Requires the :attr:`~telegram.BusinessBotRights.can_view_gifts_and_stars` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -9613,7 +9613,7 @@ async def read_business_message( Marks incoming message as read on behalf of a business account. Requires the :attr:`~telegram.BusinessBotRights.can_read_messages` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection on @@ -9663,7 +9663,7 @@ async def delete_business_messages( :attr:`~telegram.BusinessBotRights.can_delete_all_messages` business bot right to delete any message. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`int` | :obj:`str`): Unique identifier of the business @@ -9716,7 +9716,7 @@ async def post_story( Posts a story on behalf of a managed business account. Requires the :attr:`~telegram.BusinessBotRights.can_manage_stories` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -9805,7 +9805,7 @@ async def edit_story( Edits a story previously posted by the bot on behalf of a managed business account. Requires the :attr:`~telegram.BusinessBotRights.can_manage_stories` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -9878,7 +9878,7 @@ async def delete_story( Deletes a story previously posted by the bot on behalf of a managed business account. Requires the :attr:`~telegram.BusinessBotRights.can_manage_stories` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -9920,7 +9920,7 @@ async def set_business_account_name( Changes the first and last name of a managed business account. Requires the :attr:`~telegram.BusinessBotRights.can_edit_name` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`int` | :obj:`str`): Unique identifier of the business @@ -9967,7 +9967,7 @@ async def set_business_account_username( Changes the username of a managed business account. Requires the :attr:`~telegram.BusinessBotRights.can_edit_username` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -10009,7 +10009,7 @@ async def set_business_account_bio( Changes the bio of a managed business account. Requires the :attr:`~telegram.BusinessBotRights.can_edit_bio` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -10053,7 +10053,7 @@ async def set_business_account_gift_settings( Requires the :attr:`~telegram.BusinessBotRights.can_change_gift_settings` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business @@ -10101,7 +10101,7 @@ async def set_business_account_profile_photo( Requires the :attr:`~telegram.BusinessBotRights.can_edit_profile_photo` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -10148,7 +10148,7 @@ async def remove_business_account_profile_photo( Requires the :attr:`~telegram.BusinessBotRights.can_edit_profile_photo` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business connection. @@ -10192,7 +10192,7 @@ async def convert_gift_to_stars( Converts a given regular gift to Telegram Stars. Requires the :attr:`~telegram.BusinessBotRights.can_convert_gifts_to_stars` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business @@ -10239,7 +10239,7 @@ async def upgrade_gift( Additionally requires the :attr:`~telegram.BusinessBotRights.can_transfer_stars` business bot right if the upgrade is paid. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business @@ -10296,7 +10296,7 @@ async def transfer_gift( Requires :attr:`~telegram.BusinessBotRights.can_transfer_stars` business bot right if the transfer is paid. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business @@ -10349,7 +10349,7 @@ async def transfer_business_account_stars( Transfers Telegram Stars from the business account balance to the bot's balance. Requires the :attr:`~telegram.BusinessBotRights.can_transfer_stars` business bot right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: business_connection_id (:obj:`str`): Unique identifier of the business @@ -10840,8 +10840,8 @@ async def send_gift( The gift can't be converted to Telegram Stars by the receiver. .. versionadded:: 21.8 - .. versionchanged:: NEXT.VERSION - Bot API 8.3 made :paramref:`user_id` optional. In version NEXT.VERSION, the methods + .. versionchanged:: 22.1 + Bot API 8.3 made :paramref:`user_id` optional. In version 22.1, the methods signature was changed accordingly. Args: diff --git a/telegram/_business.py b/telegram/_business.py index 5f4b5f4e184..dd055426654 100644 --- a/telegram/_business.py +++ b/telegram/_business.py @@ -48,7 +48,7 @@ class BusinessBotRights(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if all their attributes are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: can_reply (:obj:`bool`, optional): True, if the bot can send and edit messages in the @@ -194,7 +194,7 @@ class BusinessConnection(TelegramObject): :attr:`rights`, and :attr:`is_enabled` are equal. .. versionadded:: 21.1 - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.1 Equality comparison now considers :attr:`rights` instead of :attr:`can_reply`. Args: @@ -206,12 +206,12 @@ class BusinessConnection(TelegramObject): can_reply (:obj:`bool`, optional): True, if the bot can act on behalf of the business account in chats that were active in the last 24 hours. - .. deprecated:: NEXT.VERSION + .. deprecated:: 22.1 Bot API 9.0 deprecated this argument in favor of :paramref:`rights`. is_enabled (:obj:`bool`): True, if the connection is active. rights (:class:`BusinessBotRights`, optional): Rights of the business bot. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Attributes: id (:obj:`str`): Unique identifier of the business connection. @@ -222,7 +222,7 @@ class BusinessConnection(TelegramObject): is_enabled (:obj:`bool`): True, if the connection is active. rights (:class:`BusinessBotRights`): Optional. Rights of the business bot. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = ( @@ -243,7 +243,7 @@ def __init__( date: dtm.datetime, can_reply: Optional[bool] = None, # temporarily optional to account for changed signature - # tags: deprecated NEXT.VERSION; bot api 9.0 + # tags: deprecated 22.1; bot api 9.0 is_enabled: Optional[bool] = None, rights: Optional[BusinessBotRights] = None, *, @@ -255,7 +255,7 @@ def __init__( if can_reply is not None: warn( PTBDeprecationWarning( - version="NEXT.VERSION", + version="22.1", message=build_deprecation_warning_message( deprecated_name="can_reply", new_name="rights", @@ -291,14 +291,14 @@ def can_reply(self) -> Optional[bool]: """:obj:`bool`: Optional. True, if the bot can act on behalf of the business account in chats that were active in the last 24 hours. - .. deprecated:: NEXT.VERSION + .. deprecated:: 22.1 Bot API 9.0 deprecated this argument in favor of :attr:`rights` """ warn_about_deprecated_attr_in_property( deprecated_attr_name="can_reply", new_attr_name="rights", bot_api_version="9.0", - ptb_version="NEXT.VERSION", + ptb_version="22.1", ) return self._can_reply diff --git a/telegram/_chat.py b/telegram/_chat.py index ab77b6d5a67..02eb6629d6d 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -3524,7 +3524,7 @@ async def transfer_gift( For the documentation of the arguments, please see :meth:`telegram.Bot.transfer_gift`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -3621,7 +3621,7 @@ async def read_business_message( For the documentation of the arguments, please see :meth:`telegram.Bot.read_business_message`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Returns: :obj:`bool`: On success, :obj:`True` is returned. diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index 7d0bffe7e92..4b0fae53c6b 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -74,7 +74,7 @@ class ChatFullInfo(_ChatBase): accepted_gift_types (:class:`telegram.AcceptedGiftTypes`): Information about types of gifts that are accepted by the chat or by the corresponding user for private chats. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 title (:obj:`str`, optional): Title, for supergroups, channels and group chats. username (:obj:`str`, optional): Username, for private chats, supergroups and channels if available. @@ -215,7 +215,7 @@ class ChatFullInfo(_ChatBase): .. versionadded:: 21.11 - .. deprecated:: NEXT.VERSION + .. deprecated:: 22.1 Bot API 9.0 introduced :paramref:`accepted_gift_types`, replacing this argument. Hence, this argument will be removed in future versions. @@ -236,7 +236,7 @@ class ChatFullInfo(_ChatBase): accepted_gift_types (:class:`telegram.AcceptedGiftTypes`): Information about types of gifts that are accepted by the chat or by the corresponding user for private chats. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 title (:obj:`str`, optional): Title, for supergroups, channels and group chats. username (:obj:`str`, optional): Username, for private chats, supergroups and channels if available. @@ -469,7 +469,7 @@ def __init__( linked_chat_id: Optional[int] = None, location: Optional[ChatLocation] = None, can_send_paid_media: Optional[bool] = None, - # tags: deprecated NEXT.VERSION; bot api 9.0 + # tags: deprecated 22.1; bot api 9.0 can_send_gift: Optional[bool] = None, # temporarily optional to account for changed signature accepted_gift_types: Optional[AcceptedGiftTypes] = None, @@ -492,7 +492,7 @@ def __init__( if can_send_gift is not None: warn( PTBDeprecationWarning( - "NEXT.VERSION", + "22.1", build_deprecation_warning_message( deprecated_name="can_send_gift", new_name="accepted_gift_types", @@ -562,7 +562,7 @@ def can_send_gift(self) -> Optional[bool]: """ :obj:`bool`: Optional. :obj:`True`, if gifts can be sent to the chat. - .. deprecated:: NEXT.VERSION + .. deprecated:: 22.1 As Bot API 9.0 replaces this attribute with :attr:`accepted_gift_types`, this attribute will be removed in future versions. @@ -571,7 +571,7 @@ def can_send_gift(self) -> Optional[bool]: deprecated_attr_name="can_send_gift", new_attr_name="accepted_gift_types", bot_api_version="9.0", - ptb_version="NEXT.VERSION", + ptb_version="22.1", stacklevel=2, ) return self._can_send_gift diff --git a/telegram/_files/_inputstorycontent.py b/telegram/_files/_inputstorycontent.py index 3d9ee40e017..1eaf14682f3 100644 --- a/telegram/_files/_inputstorycontent.py +++ b/telegram/_files/_inputstorycontent.py @@ -35,7 +35,7 @@ class InputStoryContent(TelegramObject): * :class:`telegram.InputStoryContentPhoto` * :class:`telegram.InputStoryContentVideo` - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: type (:obj:`str`): Type of the content. @@ -72,7 +72,7 @@ def _parse_file_input(file_input: FileInput) -> Union[str, InputFile]: class InputStoryContentPhoto(InputStoryContent): """Describes a photo to post as a story. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: photo (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ @@ -109,7 +109,7 @@ class InputStoryContentVideo(InputStoryContent): """ Describes a video to post as a story. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: video (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ diff --git a/telegram/_files/inputprofilephoto.py b/telegram/_files/inputprofilephoto.py index ec94b8e001e..8ec1ae93492 100644 --- a/telegram/_files/inputprofilephoto.py +++ b/telegram/_files/inputprofilephoto.py @@ -37,7 +37,7 @@ class InputProfilePhoto(TelegramObject): * :class:`InputProfilePhotoStatic` * :class:`InputProfilePhotoAnimated` - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: type (:obj:`str`): Type of the profile photo. @@ -69,7 +69,7 @@ def __init__( class InputProfilePhotoStatic(InputProfilePhoto): """A static profile photo in the .JPG format. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: photo (:term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ @@ -101,7 +101,7 @@ def __init__( class InputProfilePhotoAnimated(InputProfilePhoto): """An animated profile photo in the MPEG4 format. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: animation (:term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ diff --git a/telegram/_gifts.py b/telegram/_gifts.py index ad17451aa19..42ec1c45297 100644 --- a/telegram/_gifts.py +++ b/telegram/_gifts.py @@ -155,7 +155,7 @@ class GiftInfo(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`gift` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: gift (:class:`Gift`): Information about the gift. @@ -305,7 +305,7 @@ class AcceptedGiftTypes(TelegramObject): considered equal if their :attr:`unlimited_gifts`, :attr:`limited_gifts`, :attr:`unique_gifts` and :attr:`premium_subscription` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: unlimited_gifts (:class:`bool`): :obj:`True`, if unlimited regular gifts are accepted. diff --git a/telegram/_message.py b/telegram/_message.py index f39e52e7851..58e18aa0e01 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -449,7 +449,7 @@ class Message(MaybeInaccessibleMessage): paid_star_count (:obj:`int`, optional): The number of Telegram Stars that were paid by the sender of the message to send it - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data. poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the poll. @@ -535,11 +535,11 @@ class Message(MaybeInaccessibleMessage): gift (:class:`telegram.GiftInfo`, optional): Service message: a regular gift was sent or received. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 unique_gift (:class:`telegram.UniqueGiftInfo`, optional): Service message: a unique gift was sent or received - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 giveaway_created (:class:`telegram.GiveawayCreated`, optional): Service message: a scheduled giveaway was created @@ -559,7 +559,7 @@ class Message(MaybeInaccessibleMessage): paid_message_price_changed (:class:`telegram.PaidMessagePriceChanged`, optional): Service message: the price for paid messages has changed in the chat - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 external_reply (:class:`telegram.ExternalReplyInfo`, optional): Information about the message that is being replied to, which may come from another chat or forum topic. @@ -793,7 +793,7 @@ class Message(MaybeInaccessibleMessage): paid_star_count (:obj:`int`): Optional. The number of Telegram Stars that were paid by the sender of the message to send it - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data. Examples: @@ -879,11 +879,11 @@ class Message(MaybeInaccessibleMessage): gift (:class:`telegram.GiftInfo`): Optional. Service message: a regular gift was sent or received. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 unique_gift (:class:`telegram.UniqueGiftInfo`): Optional. Service message: a unique gift was sent or received - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 giveaway_created (:class:`telegram.GiveawayCreated`): Optional. Service message: a scheduled giveaway was created @@ -903,7 +903,7 @@ class Message(MaybeInaccessibleMessage): paid_message_price_changed (:class:`telegram.PaidMessagePriceChanged`): Optional. Service message: the price for paid messages has changed in the chat - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 external_reply (:class:`telegram.ExternalReplyInfo`): Optional. Information about the message that is being replied to, which may come from another chat or forum topic. @@ -4554,7 +4554,7 @@ async def read_business_message( For the documentation of the arguments, please see :meth:`telegram.Bot.read_business_message`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Returns: :obj:`bool` On success, :obj:`True` is returned. diff --git a/telegram/_ownedgift.py b/telegram/_ownedgift.py index 6481eb33de3..875a01540f1 100644 --- a/telegram/_ownedgift.py +++ b/telegram/_ownedgift.py @@ -48,7 +48,7 @@ class OwnedGift(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:: 22.1 Args: type (:obj:`str`): Type of the owned gift. @@ -108,7 +108,7 @@ class OwnedGifts(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`total_count` and :attr:`gifts` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: total_count (:obj:`int`): The total number of gifts owned by the user or the chat. @@ -161,7 +161,7 @@ class OwnedGiftRegular(OwnedGift): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`gift` and :attr:`send_date` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: gift (:class:`telegram.Gift`): Information about the regular gift. @@ -339,7 +339,7 @@ class OwnedGiftUnique(OwnedGift): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`gift` and :attr:`send_date` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: gift (:class:`telegram.UniqueGift`): Information about the unique gift. diff --git a/telegram/_paidmessagepricechanged.py b/telegram/_paidmessagepricechanged.py index f31d7293b40..d77cb6d54b0 100644 --- a/telegram/_paidmessagepricechanged.py +++ b/telegram/_paidmessagepricechanged.py @@ -29,7 +29,7 @@ class PaidMessagePriceChanged(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`paid_message_star_count` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: paid_message_star_count (:obj:`int`): The new number of Telegram Stars that must be paid by diff --git a/telegram/_payment/stars/transactionpartner.py b/telegram/_payment/stars/transactionpartner.py index cf086f6bff9..723e4d826c7 100644 --- a/telegram/_payment/stars/transactionpartner.py +++ b/telegram/_payment/stars/transactionpartner.py @@ -285,7 +285,7 @@ class TransactionPartnerUser(TransactionPartner): .. versionadded:: 21.4 - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.1 Equality comparison now includes the new required argument :paramref:`transaction_type`, introduced in Bot API 9.0. @@ -300,7 +300,7 @@ class TransactionPartnerUser(TransactionPartner): :tg-const:`telegram.constants.TransactionPartnerUser.BUSINESS_ACCOUNT_TRANSFER` for direct transfers from managed business accounts. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 user (:class:`telegram.User`): Information about the user. affiliate (:class:`telegram.AffiliateInfo`, optional): Information about the affiliate that received a commission via this transaction. Can be available only for @@ -337,7 +337,7 @@ class TransactionPartnerUser(TransactionPartner): :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` transactions only. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Attributes: type (:obj:`str`): The type of the transaction partner, @@ -352,7 +352,7 @@ class TransactionPartnerUser(TransactionPartner): :tg-const:`telegram.constants.TransactionPartnerUser.BUSINESS_ACCOUNT_TRANSFER` for direct transfers from managed business accounts. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 user (:class:`telegram.User`): Information about the user. affiliate (:class:`telegram.AffiliateInfo`): Optional. Information about the affiliate that received a commission via this transaction. Can be available only for @@ -389,7 +389,7 @@ class TransactionPartnerUser(TransactionPartner): :tg-const:`telegram.constants.TransactionPartnerUser.PREMIUM_PURCHASE` transactions only. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ @@ -422,7 +422,7 @@ def __init__( ) -> None: super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) - # tags: deprecated NEXT.VERSION, bot api 9.0 + # tags: deprecated 22.1, bot api 9.0 if transaction_type is None: raise TypeError("`transaction_type` is a required argument since Bot API 9.0") diff --git a/telegram/_storyarea.py b/telegram/_storyarea.py index 1b72587fdd9..7335be68951 100644 --- a/telegram/_storyarea.py +++ b/telegram/_storyarea.py @@ -33,7 +33,7 @@ class StoryAreaPosition(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if all of their attributes are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: x_percentage (:obj:`float`): The abscissa of the area's center, as a percentage of the @@ -111,7 +111,7 @@ class LocationAddress(TelegramObject): considered equal, if their :attr:`country_code`, :attr:`state`, :attr:`city` and :attr:`street` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: country_code (:obj:`str`): The two-letter ``ISO 3166-1 alpha-2`` country code of the @@ -162,7 +162,7 @@ class StoryAreaType(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:: 22.1 Args: type (:obj:`str`): Type of the area. @@ -205,7 +205,7 @@ class StoryAreaTypeLocation(StoryAreaType): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`latitude` and :attr:`longitude` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: latitude (:obj:`float`): Location latitude in degrees. @@ -250,7 +250,7 @@ class StoryAreaTypeSuggestedReaction(StoryAreaType): considered equal, if their :attr:`reaction_type`, :attr:`is_dark` and :attr:`is_flipped` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: reaction_type (:class:`ReactionType`): Type of the reaction. @@ -295,7 +295,7 @@ class StoryAreaTypeLink(StoryAreaType): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`url` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: url (:obj:`str`): ``HTTP`` or ``tg://`` URL to be opened when the area is clicked. @@ -331,7 +331,7 @@ class StoryAreaTypeWeather(StoryAreaType): considered equal, if their :attr:`temperature`, :attr:`emoji` and :attr:`background_color` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: temperature (:obj:`float`): Temperature, in degree Celsius. @@ -375,7 +375,7 @@ class StoryAreaTypeUniqueGift(StoryAreaType): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`name` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: name (:obj:`str`): Unique name of the gift. @@ -409,7 +409,7 @@ class StoryArea(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`position` and :attr:`type` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: position (:class:`telegram.StoryAreaPosition`): Position of the area. diff --git a/telegram/_uniquegift.py b/telegram/_uniquegift.py index 61926552f3f..fa494a8e55a 100644 --- a/telegram/_uniquegift.py +++ b/telegram/_uniquegift.py @@ -37,7 +37,7 @@ class UniqueGiftModel(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`name`, :attr:`sticker` and :attr:`rarity_per_mille` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: name (:obj:`str`): Name of the model. @@ -92,7 +92,7 @@ class UniqueGiftSymbol(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`name`, :attr:`sticker` and :attr:`rarity_per_mille` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: name (:obj:`str`): Name of the symbol. @@ -148,7 +148,7 @@ class UniqueGiftBackdropColors(TelegramObject): considered equal if their :attr:`center_color`, :attr:`edge_color`, :attr:`symbol_color`, and :attr:`text_color` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: center_color (:obj:`int`): The color in the center of the backdrop in RGB format. @@ -197,7 +197,7 @@ class UniqueGiftBackdrop(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`name`, :attr:`colors`, and :attr:`rarity_per_mille` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: name (:obj:`str`): Name of the backdrop. @@ -253,7 +253,7 @@ class UniqueGift(TelegramObject): considered equal if their :attr:`base_name`, :attr:`name`, :attr:`number`, :class:`model`, :attr:`symbol`, and :attr:`backdrop` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: base_name (:obj:`str`): Human-readable name of the regular gift from which this unique @@ -336,7 +336,7 @@ class UniqueGiftInfo(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`gift`, and :attr:`origin` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Args: gift (:class:`UniqueGift`): Information about the gift. diff --git a/telegram/_user.py b/telegram/_user.py index 6bb8dc08db1..ce6c3bbbb7b 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -1722,7 +1722,7 @@ async def gift_premium_subscription( For the documentation of the arguments, please see :meth:`telegram.Bot.gift_premium_subscription`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 Returns: :obj:`bool`: On success, :obj:`True` is returned. diff --git a/telegram/_version.py b/telegram/_version.py index 88eb033c8b8..412650e88d6 100644 --- a/telegram/_version.py +++ b/telegram/_version.py @@ -51,6 +51,6 @@ def __str__(self) -> str: __version_info__: Final[Version] = Version( - major=22, minor=0, micro=0, releaselevel="final", serial=0 + major=22, minor=1, micro=0, releaselevel="final", serial=0 ) __version__: Final[str] = str(__version_info__) diff --git a/telegram/constants.py b/telegram/constants.py index 78873a8da19..2c4b7526354 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -720,7 +720,7 @@ class BusinessLimit(IntEnum): """This enum contains limitations related to handling business accounts. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -961,7 +961,7 @@ class ChatSubscriptionLimit(IntEnum): MAX_PRICE = 10000 """:obj:`int`: Amount of stars a user pays, maximum amount the subscription can be set to. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.1 Bot API 9.0 changed the value to 10000. """ @@ -1448,7 +1448,7 @@ class InputProfilePhotoType(StringEnum): """This enum contains the available types of :class:`telegram.InputProfilePhoto`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -1464,7 +1464,7 @@ class InputStoryContentLimit(StringEnum): :class:`telegram.InputStoryContentVideo`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -1510,7 +1510,7 @@ class InputStoryContentType(StringEnum): """This enum contains the available types of :class:`telegram.InputStoryContent`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -2104,7 +2104,7 @@ class MessageType(StringEnum): GIFT = "gift" """:obj:`str`: Messages with :attr:`telegram.Message.gift`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ GIVEAWAY = "giveaway" """:obj:`str`: Messages with :attr:`telegram.Message.giveaway`. @@ -2197,7 +2197,7 @@ class MessageType(StringEnum): UNIQUE_GIFT = "unique_gift" """:obj:`str`: Messages with :attr:`telegram.Message.unique_gift`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ USERS_SHARED = "users_shared" """:obj:`str`: Messages with :attr:`telegram.Message.users_shared`. @@ -2239,7 +2239,7 @@ class Nanostar(FloatEnum): The enum members of this enumeration are instances of :class:`float` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -2260,7 +2260,7 @@ class NanostarLimit(IntEnum): and :class:`telegram.StarAmount`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -2284,7 +2284,7 @@ class OwnedGiftType(StringEnum): """This enum contains the available types of :class:`telegram.OwnedGift`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -2336,7 +2336,7 @@ class PremiumSubscription(IntEnum): """This enum contains limitations for :meth:`~telegram.Bot.gift_premium_subscription`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -2737,7 +2737,7 @@ class RevenueWithdrawalStateType(StringEnum): """:obj:`str`: A withdrawal failed and the transaction was refunded.""" -# tags: deprecated NEXT.VERSION, bot api 9.0 +# tags: deprecated 22.1, bot api 9.0 class StarTransactions(FloatEnum): """This enum contains constants for :class:`telegram.StarTransaction`. The enum members of this enumeration are instances of :class:`float` and can be treated as @@ -2745,7 +2745,7 @@ class StarTransactions(FloatEnum): .. versionadded:: 21.9 - .. deprecated:: NEXT.VERSION + .. deprecated:: 22.1 This class will be removed as its only member :attr:`NANOSTAR_VALUE` will be replaced by :attr:`telegram.constants.Nanostar.VALUE`. """ @@ -2756,7 +2756,7 @@ class StarTransactions(FloatEnum): """:obj:`float`: The value of one nanostar as used in :attr:`telegram.StarTransaction.nanostar_amount`. - .. deprecated:: NEXT.VERSION + .. deprecated:: 22.1 This member will be replaced by :attr:`telegram.constants.Nanostar.VALUE`. """ @@ -2779,17 +2779,17 @@ class StarTransactionsLimit(IntEnum): """:obj:`int`: Maximum value allowed for the :paramref:`~telegram.Bot.get_star_transactions.limit` parameter of :meth:`telegram.Bot.get_star_transactions`.""" - # tags: deprecated NEXT.VERSION, bot api 9.0 + # tags: deprecated 22.1, bot api 9.0 NANOSTAR_MIN_AMOUNT = NanostarLimit.MIN_AMOUNT """:obj:`int`: Minimum value allowed for :paramref:`~telegram.AffiliateInfo.nanostar_amount` parameter of :class:`telegram.AffiliateInfo`. .. versionadded:: 21.9 - .. deprecated:: NEXT.VERSION + .. deprecated:: 22.1 This member will be replaced by :attr:`telegram.constants.NanostarLimit.MIN_AMOUNT`. """ - # tags: deprecated NEXT.VERSION, bot api 9.0 + # tags: deprecated 22.1, bot api 9.0 NANOSTAR_MAX_AMOUNT = NanostarLimit.MAX_AMOUNT """:obj:`int`: Maximum value allowed for :paramref:`~telegram.StarTransaction.nanostar_amount` parameter of :class:`telegram.StarTransaction` and @@ -2798,7 +2798,7 @@ class StarTransactionsLimit(IntEnum): .. versionadded:: 21.9 - .. deprecated:: NEXT.VERSION + .. deprecated:: 22.1 This member will be replaced by :attr:`telegram.constants.NanostarLimit.MAX_AMOUNT`. """ @@ -2940,7 +2940,7 @@ class StoryAreaPositionLimit(IntEnum): """This enum contains limitations for :class:`telegram.StoryAreaPosition`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -2956,7 +2956,7 @@ class StoryAreaTypeLimit(IntEnum): """This enum contains limitations for subclasses of :class:`telegram.StoryAreaType`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -2982,7 +2982,7 @@ class StoryAreaTypeType(StringEnum): """This enum contains the available types of :class:`telegram.StoryAreaType`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -3004,7 +3004,7 @@ class StoryLimit(StringEnum): :meth:`~telegram.Bot.edit_story`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -3068,7 +3068,7 @@ class TransactionPartnerUser(StringEnum): The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -3201,7 +3201,7 @@ class UniqueGiftInfoOrigin(StringEnum): """This enum contains the available origins for :class:`telegram.UniqueGiftInfo`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ __slots__ = () @@ -3389,7 +3389,7 @@ class InvoiceLimit(IntEnum): :meth:`telegram.Bot.send_paid_media`. .. versionadded:: 21.6 - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.1 Bot API 9.0 changed the value to 10000. """ SUBSCRIPTION_PERIOD = dtm.timedelta(days=30).total_seconds() @@ -3404,7 +3404,7 @@ class InvoiceLimit(IntEnum): :meth:`telegram.Bot.create_invoice_link`. .. versionadded:: 21.9 - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.1 Bot API 9.0 changed the value to 10000. """ diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 323c0e0f646..6322dafd296 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -2091,7 +2091,7 @@ def filter(self, message: Message) -> bool: GIFT = _Gift(name="filters.StatusUpdate.GIFT") """Messages that contain :attr:`telegram.Message.gift`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ class _GiveawayCreated(MessageFilter): @@ -2188,7 +2188,7 @@ def filter(self, message: Message) -> bool: ) """Messages that contain :attr:`telegram.Message.paid_message_price_changed`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ class _PinnedMessage(MessageFilter): @@ -2231,7 +2231,7 @@ def filter(self, message: Message) -> bool: UNIQUE_GIFT = _UniqueGift(name="filters.StatusUpdate.UNIQUE_GIFT") """Messages that contain :attr:`telegram.Message.unique_gift`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.1 """ class _UsersShared(MessageFilter): From 8782ae7bb563421f27e0901be2473f848aee62c4 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 May 2025 20:58:14 +0200 Subject: [PATCH 33/36] Fix a Failing Test Case (#4793) --- changes/unreleased/4793.mDR5p3mrSmPFQkvWFGWBmD.toml | 5 +++++ tests/_passport/test_passport.py | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4793.mDR5p3mrSmPFQkvWFGWBmD.toml diff --git a/changes/unreleased/4793.mDR5p3mrSmPFQkvWFGWBmD.toml b/changes/unreleased/4793.mDR5p3mrSmPFQkvWFGWBmD.toml new file mode 100644 index 00000000000..7a6ca4c3e95 --- /dev/null +++ b/changes/unreleased/4793.mDR5p3mrSmPFQkvWFGWBmD.toml @@ -0,0 +1,5 @@ +internal = "Fix a Failing Test Case" +[[pull_requests]] +uid = "4793" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/tests/_passport/test_passport.py b/tests/_passport/test_passport.py index 8f5776fd819..4104a2c6b4c 100644 --- a/tests/_passport/test_passport.py +++ b/tests/_passport/test_passport.py @@ -418,7 +418,10 @@ def test_bot_init_invalid_key(self, offline_bot): with pytest.raises(TypeError): Bot(offline_bot.token, private_key="Invalid key!") - with pytest.raises(ValueError, match="Could not deserialize key data"): + # Different error messages for different cryptography versions + with pytest.raises( + ValueError, match="(Could not deserialize key data)|(Unable to load PEM file)" + ): Bot(offline_bot.token, private_key=b"Invalid key!") def test_all_types(self, passport_data, offline_bot, all_passport_data): From 8289a4fda61c37e429975a659973ded48140daa5 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 May 2025 20:58:37 +0200 Subject: [PATCH 34/36] Fix Bug in Automated Channel Announcement (#4792) --- .github/workflows/release_pypi.yml | 4 +++- changes/unreleased/4792.YsK6LmbEhZv6y3dvhHbXD7.toml | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4792.YsK6LmbEhZv6y3dvhHbXD7.toml diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index a9e9e468010..ab4ea06a4fe 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -145,7 +145,9 @@ jobs: telegram-channel: name: Publish to Telegram Channel needs: - - github-release + # required to have the output available for the env var + - build + - github-release runs-on: ubuntu-latest environment: diff --git a/changes/unreleased/4792.YsK6LmbEhZv6y3dvhHbXD7.toml b/changes/unreleased/4792.YsK6LmbEhZv6y3dvhHbXD7.toml new file mode 100644 index 00000000000..675c2904b4d --- /dev/null +++ b/changes/unreleased/4792.YsK6LmbEhZv6y3dvhHbXD7.toml @@ -0,0 +1,5 @@ +internal = "Fix Bug in Automated Channel Announcement" +[[pull_requests]] +uid = "4792" +author_uid = "Bibo-Joshi" +closes_threads = [] From 98e94a187f43458a97f28233f19d7eea400fd328 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sat, 24 May 2025 03:48:52 -0400 Subject: [PATCH 35/36] Implement PEP 735 Dependency Groups (#4800) --- .github/CONTRIBUTING.rst | 2 +- .github/workflows/chango.yml | 2 +- .github/workflows/docs-admonitions.yml | 4 +-- .github/workflows/docs-linkcheck.yml | 2 +- .github/workflows/test_official.yml | 3 +- .github/workflows/unit_tests.yml | 6 +--- .readthedocs.yml | 5 ++- .../4800.2Z9Q8uvzdU2TqMJ9biBLam.toml | 5 +++ docs/requirements-docs.txt | 10 ------ pyproject.toml | 31 +++++++++++++++++++ requirements-dev-all.txt | 5 --- requirements-unit-tests.txt | 23 -------------- 12 files changed, 47 insertions(+), 51 deletions(-) create mode 100644 changes/unreleased/4800.2Z9Q8uvzdU2TqMJ9biBLam.toml delete mode 100644 docs/requirements-docs.txt delete mode 100644 requirements-dev-all.txt delete mode 100644 requirements-unit-tests.txt diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index 475c41d203d..b545fd29bf1 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -26,7 +26,7 @@ Setting things up .. code-block:: bash - $ pip install -r requirements-dev-all.txt + $ pip install .[all] --group all 5. Install pre-commit hooks: diff --git a/.github/workflows/chango.yml b/.github/workflows/chango.yml index d845f6bc019..8010ccec486 100644 --- a/.github/workflows/chango.yml +++ b/.github/workflows/chango.yml @@ -54,7 +54,7 @@ jobs: run: | cd ./target-repo git add changes/unreleased/* - pip install . -r docs/requirements-docs.txt + pip install . --group docs VERSION_TAG=$(python -c "from telegram import __version__; print(f'{__version__}')") chango release --uid $VERSION_TAG diff --git a/.github/workflows/docs-admonitions.yml b/.github/workflows/docs-admonitions.yml index 00b03ae4cca..a2a6468a9a7 100644 --- a/.github/workflows/docs-admonitions.yml +++ b/.github/workflows/docs-admonitions.yml @@ -32,10 +32,10 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' - cache-dependency-path: '**/requirements*.txt' + cache-dependency-path: 'pyproject.toml' - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip - python -W ignore -m pip install -r requirements-dev-all.txt + python -W ignore -m pip install .[all] --group all - name: Test autogeneration of admonitions run: pytest -v --tb=short tests/docs/admonition_inserter.py \ No newline at end of file diff --git a/.github/workflows/docs-linkcheck.yml b/.github/workflows/docs-linkcheck.yml index 65453ad11f3..f87424bd74b 100644 --- a/.github/workflows/docs-linkcheck.yml +++ b/.github/workflows/docs-linkcheck.yml @@ -29,7 +29,7 @@ jobs: - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip - python -W ignore -m pip install -r requirements-dev-all.txt + python -W ignore -m pip install .[all] --group all - name: Check Links run: sphinx-build docs/source docs/build/html --keep-going -j auto -b linkcheck - name: Upload linkcheck output diff --git a/.github/workflows/test_official.yml b/.github/workflows/test_official.yml index 14224d0901a..054aa1fb733 100644 --- a/.github/workflows/test_official.yml +++ b/.github/workflows/test_official.yml @@ -33,8 +33,7 @@ jobs: - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip - python -W ignore -m pip install .[all] - python -W ignore -m pip install -r requirements-unit-tests.txt + python -W ignore -m pip install .[all] --group tests - 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/unit_tests.yml b/.github/workflows/unit_tests.yml index affb519fce2..0ac877748d0 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -6,7 +6,6 @@ on: - tests/** - .github/workflows/unit_tests.yml - pyproject.toml - - requirements-unit-tests.txt push: branches: - master @@ -34,14 +33,11 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' - cache-dependency-path: '**/requirements*.txt' - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -U pytest-cov - 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 + python -W ignore -m pip install . --group tests - name: Test with pytest # We run 4 different suites here diff --git a/.readthedocs.yml b/.readthedocs.yml index 11075b0fe2b..6d89b823dba 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,13 +18,16 @@ python: install: - method: pip path: . - - requirements: requirements-dev-all.txt build: os: ubuntu-22.04 tools: python: "3" # latest stable cpython version jobs: + install: + - pip install -U pip + - pip install .[all] --group 'all' # install all the dependency groups + post_build: # Based on https://github.com/readthedocs/readthedocs.org/issues/3242#issuecomment-1410321534 # This provides a HTML zip file for download, with the same structure as the hosted website diff --git a/changes/unreleased/4800.2Z9Q8uvzdU2TqMJ9biBLam.toml b/changes/unreleased/4800.2Z9Q8uvzdU2TqMJ9biBLam.toml new file mode 100644 index 00000000000..4f670da8bd0 --- /dev/null +++ b/changes/unreleased/4800.2Z9Q8uvzdU2TqMJ9biBLam.toml @@ -0,0 +1,5 @@ +dependencies = "Implement PEP 735 Dependency Groups for Development Dependencies" +[[pull_requests]] +uid = "4800" +author_uid = "harshil21" +closes_threads = ["4795"] diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt deleted file mode 100644 index e207cc48175..00000000000 --- a/docs/requirements-docs.txt +++ /dev/null @@ -1,10 +0,0 @@ -chango~=0.4.0 -sphinx==8.2.3 -furo==2024.8.6 -furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1 -sphinx-paramlinks==0.6.0 -sphinxcontrib-mermaid==1.0.0 -sphinx-copybutton==0.5.2 -sphinx-inline-tabs==2023.4.21 -# Temporary. See #4387 -sphinx-build-compatibility @ git+https://github.com/readthedocs/sphinx-build-compatibility.git@58aabc5f207c6c2421f23d3578adc0b14af57047 diff --git a/pyproject.toml b/pyproject.toml index 1ffe02f8efe..6e5e124f40c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,37 @@ webhooks = [ "tornado~=6.4", ] +[dependency-groups] +tests = [ + # required for building the wheels for releases + "build", + # For the test suite + "pytest==8.3.5", + # 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>=3.8.1", + # used in test_official for parsing tg docs + "beautifulsoup4", + # For testing with timezones. Might not be needed on all systems, but to ensure that unit tests + # run correctly on all systems, we include it here. + "tzdata", +] +docs = [ + "chango~=0.4.0; python_version >= '3.12'", + "sphinx==8.2.3; python_version >= '3.11'", + "furo==2024.8.6", + "furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1", + "sphinx-paramlinks==0.6.0", + "sphinxcontrib-mermaid==1.0.0", + "sphinx-copybutton==0.5.2", + "sphinx-inline-tabs==2023.4.21", + # Temporary. See #4387 + "sphinx-build-compatibility @ git+https://github.com/readthedocs/sphinx-build-compatibility.git@58aabc5f207c6c2421f23d3578adc0b14af57047", +] +all = ["pre-commit", { include-group = "tests" }, { include-group = "docs" }] # HATCH [tool.hatch.version] diff --git a/requirements-dev-all.txt b/requirements-dev-all.txt deleted file mode 100644 index 995e067c420..00000000000 --- a/requirements-dev-all.txt +++ /dev/null @@ -1,5 +0,0 @@ --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-unit-tests.txt b/requirements-unit-tests.txt deleted file mode 100644 index f90d2950f60..00000000000 --- a/requirements-unit-tests.txt +++ /dev/null @@ -1,23 +0,0 @@ --e . - -# required for building the wheels for releases -build - -# For the test suite -pytest==8.3.5 - -# 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>=3.8.1 - -# used in test_official for parsing tg docs -beautifulsoup4 - -# For testing with timezones. Might not be needed on all systems, but to ensure that unit tests -# run correctly on all systems, we include it here. -tzdata \ No newline at end of file From 828eda7c33f845ec6c0d164a59d68b2b6b543939 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 25 May 2025 13:50:24 +0200 Subject: [PATCH 36/36] Rework Repository to `src` Layout (#4798) --- .github/CONTRIBUTING.rst | 6 +++-- .github/workflows/docs-admonitions.yml | 2 +- .github/workflows/test_official.yml | 2 +- .github/workflows/type_completeness.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- .../4798.g7G3jRf2ns4ath9LRFEcit.toml | 5 ++++ pyproject.toml | 24 ++++++++++++------- {telegram => src/telegram}/__init__.py | 0 {telegram => src/telegram}/__main__.py | 0 {telegram => src/telegram}/_birthdate.py | 0 {telegram => src/telegram}/_bot.py | 0 {telegram => src/telegram}/_botcommand.py | 0 .../telegram}/_botcommandscope.py | 0 {telegram => src/telegram}/_botdescription.py | 0 {telegram => src/telegram}/_botname.py | 0 {telegram => src/telegram}/_business.py | 0 {telegram => src/telegram}/_callbackquery.py | 0 {telegram => src/telegram}/_chat.py | 0 .../telegram}/_chatadministratorrights.py | 0 {telegram => src/telegram}/_chatbackground.py | 0 {telegram => src/telegram}/_chatboost.py | 0 {telegram => src/telegram}/_chatfullinfo.py | 0 {telegram => src/telegram}/_chatinvitelink.py | 0 .../telegram}/_chatjoinrequest.py | 0 {telegram => src/telegram}/_chatlocation.py | 0 {telegram => src/telegram}/_chatmember.py | 0 .../telegram}/_chatmemberupdated.py | 0 .../telegram}/_chatpermissions.py | 0 .../telegram}/_choseninlineresult.py | 0 {telegram => src/telegram}/_copytextbutton.py | 0 {telegram => src/telegram}/_dice.py | 0 {telegram => src/telegram}/_files/__init__.py | 0 .../telegram}/_files/_basemedium.py | 0 .../telegram}/_files/_basethumbedmedium.py | 0 .../telegram}/_files/_inputstorycontent.py | 0 .../telegram}/_files/animation.py | 0 {telegram => src/telegram}/_files/audio.py | 0 .../telegram}/_files/chatphoto.py | 0 {telegram => src/telegram}/_files/contact.py | 0 {telegram => src/telegram}/_files/document.py | 0 {telegram => src/telegram}/_files/file.py | 0 .../telegram}/_files/inputfile.py | 0 .../telegram}/_files/inputmedia.py | 0 .../telegram}/_files/inputprofilephoto.py | 0 .../telegram}/_files/inputsticker.py | 0 {telegram => src/telegram}/_files/location.py | 0 .../telegram}/_files/photosize.py | 0 {telegram => src/telegram}/_files/sticker.py | 0 {telegram => src/telegram}/_files/venue.py | 0 {telegram => src/telegram}/_files/video.py | 0 .../telegram}/_files/videonote.py | 0 {telegram => src/telegram}/_files/voice.py | 0 {telegram => src/telegram}/_forcereply.py | 0 {telegram => src/telegram}/_forumtopic.py | 0 {telegram => src/telegram}/_games/__init__.py | 0 .../telegram}/_games/callbackgame.py | 0 {telegram => src/telegram}/_games/game.py | 0 .../telegram}/_games/gamehighscore.py | 0 {telegram => src/telegram}/_gifts.py | 0 {telegram => src/telegram}/_giveaway.py | 0 .../telegram}/_inline/__init__.py | 0 .../telegram}/_inline/inlinekeyboardbutton.py | 0 .../telegram}/_inline/inlinekeyboardmarkup.py | 0 .../telegram}/_inline/inlinequery.py | 0 .../telegram}/_inline/inlinequeryresult.py | 0 .../_inline/inlinequeryresultarticle.py | 0 .../_inline/inlinequeryresultaudio.py | 0 .../_inline/inlinequeryresultcachedaudio.py | 0 .../inlinequeryresultcacheddocument.py | 0 .../_inline/inlinequeryresultcachedgif.py | 0 .../inlinequeryresultcachedmpeg4gif.py | 0 .../_inline/inlinequeryresultcachedphoto.py | 0 .../_inline/inlinequeryresultcachedsticker.py | 0 .../_inline/inlinequeryresultcachedvideo.py | 0 .../_inline/inlinequeryresultcachedvoice.py | 0 .../_inline/inlinequeryresultcontact.py | 0 .../_inline/inlinequeryresultdocument.py | 0 .../_inline/inlinequeryresultgame.py | 0 .../telegram}/_inline/inlinequeryresultgif.py | 0 .../_inline/inlinequeryresultlocation.py | 0 .../_inline/inlinequeryresultmpeg4gif.py | 0 .../_inline/inlinequeryresultphoto.py | 0 .../_inline/inlinequeryresultsbutton.py | 0 .../_inline/inlinequeryresultvenue.py | 0 .../_inline/inlinequeryresultvideo.py | 0 .../_inline/inlinequeryresultvoice.py | 0 .../_inline/inputcontactmessagecontent.py | 0 .../_inline/inputinvoicemessagecontent.py | 0 .../_inline/inputlocationmessagecontent.py | 0 .../telegram}/_inline/inputmessagecontent.py | 0 .../_inline/inputtextmessagecontent.py | 0 .../_inline/inputvenuemessagecontent.py | 0 .../_inline/preparedinlinemessage.py | 0 {telegram => src/telegram}/_keyboardbutton.py | 0 .../telegram}/_keyboardbuttonpolltype.py | 0 .../telegram}/_keyboardbuttonrequest.py | 0 .../telegram}/_linkpreviewoptions.py | 0 {telegram => src/telegram}/_loginurl.py | 0 {telegram => src/telegram}/_menubutton.py | 0 {telegram => src/telegram}/_message.py | 0 .../_messageautodeletetimerchanged.py | 0 {telegram => src/telegram}/_messageentity.py | 0 {telegram => src/telegram}/_messageid.py | 0 {telegram => src/telegram}/_messageorigin.py | 0 .../telegram}/_messagereactionupdated.py | 0 {telegram => src/telegram}/_ownedgift.py | 0 {telegram => src/telegram}/_paidmedia.py | 0 .../telegram}/_paidmessagepricechanged.py | 0 .../telegram}/_passport/__init__.py | 0 .../telegram}/_passport/credentials.py | 0 {telegram => src/telegram}/_passport/data.py | 0 .../_passport/encryptedpassportelement.py | 0 .../telegram}/_passport/passportdata.py | 0 .../_passport/passportelementerrors.py | 0 .../telegram}/_passport/passportfile.py | 0 .../telegram}/_payment/__init__.py | 0 .../telegram}/_payment/invoice.py | 0 .../telegram}/_payment/labeledprice.py | 0 .../telegram}/_payment/orderinfo.py | 0 .../telegram}/_payment/precheckoutquery.py | 0 .../telegram}/_payment/refundedpayment.py | 0 .../telegram}/_payment/shippingaddress.py | 0 .../telegram}/_payment/shippingoption.py | 0 .../telegram}/_payment/shippingquery.py | 0 .../telegram}/_payment/stars/__init__.py | 0 .../telegram}/_payment/stars/affiliateinfo.py | 0 .../_payment/stars/revenuewithdrawalstate.py | 0 .../telegram}/_payment/stars/staramount.py | 0 .../_payment/stars/startransactions.py | 0 .../_payment/stars/transactionpartner.py | 0 .../telegram}/_payment/successfulpayment.py | 0 {telegram => src/telegram}/_poll.py | 0 .../telegram}/_proximityalerttriggered.py | 0 {telegram => src/telegram}/_reaction.py | 0 {telegram => src/telegram}/_reply.py | 0 .../telegram}/_replykeyboardmarkup.py | 0 .../telegram}/_replykeyboardremove.py | 0 .../telegram}/_sentwebappmessage.py | 0 {telegram => src/telegram}/_shared.py | 0 {telegram => src/telegram}/_story.py | 0 {telegram => src/telegram}/_storyarea.py | 0 .../telegram}/_switchinlinequerychosenchat.py | 0 {telegram => src/telegram}/_telegramobject.py | 0 {telegram => src/telegram}/_uniquegift.py | 0 {telegram => src/telegram}/_update.py | 0 {telegram => src/telegram}/_user.py | 0 .../telegram}/_userprofilephotos.py | 0 {telegram => src/telegram}/_utils/__init__.py | 0 .../telegram}/_utils/argumentparsing.py | 0 {telegram => src/telegram}/_utils/datetime.py | 0 .../telegram}/_utils/defaultvalue.py | 0 {telegram => src/telegram}/_utils/entities.py | 0 {telegram => src/telegram}/_utils/enum.py | 0 {telegram => src/telegram}/_utils/files.py | 0 {telegram => src/telegram}/_utils/logging.py | 0 {telegram => src/telegram}/_utils/markup.py | 0 {telegram => src/telegram}/_utils/repr.py | 0 {telegram => src/telegram}/_utils/strings.py | 0 {telegram => src/telegram}/_utils/types.py | 0 {telegram => src/telegram}/_utils/warnings.py | 0 .../telegram}/_utils/warnings_transition.py | 0 {telegram => src/telegram}/_version.py | 0 {telegram => src/telegram}/_videochat.py | 0 {telegram => src/telegram}/_webappdata.py | 0 {telegram => src/telegram}/_webappinfo.py | 0 {telegram => src/telegram}/_webhookinfo.py | 0 .../telegram}/_writeaccessallowed.py | 0 {telegram => src/telegram}/constants.py | 0 {telegram => src/telegram}/error.py | 0 {telegram => src/telegram}/ext/__init__.py | 0 .../telegram}/ext/_aioratelimiter.py | 0 .../telegram}/ext/_application.py | 0 .../telegram}/ext/_applicationbuilder.py | 0 .../telegram}/ext/_basepersistence.py | 0 .../telegram}/ext/_baseratelimiter.py | 0 .../telegram}/ext/_baseupdateprocessor.py | 0 .../telegram}/ext/_callbackcontext.py | 0 .../telegram}/ext/_callbackdatacache.py | 0 .../telegram}/ext/_contexttypes.py | 0 {telegram => src/telegram}/ext/_defaults.py | 0 .../telegram}/ext/_dictpersistence.py | 0 {telegram => src/telegram}/ext/_extbot.py | 0 .../telegram}/ext/_handlers/__init__.py | 0 .../telegram}/ext/_handlers/basehandler.py | 0 .../_handlers/businessconnectionhandler.py | 0 .../businessmessagesdeletedhandler.py | 0 .../ext/_handlers/callbackqueryhandler.py | 0 .../ext/_handlers/chatboosthandler.py | 0 .../ext/_handlers/chatjoinrequesthandler.py | 0 .../ext/_handlers/chatmemberhandler.py | 0 .../_handlers/choseninlineresulthandler.py | 0 .../telegram}/ext/_handlers/commandhandler.py | 0 .../ext/_handlers/conversationhandler.py | 0 .../ext/_handlers/inlinequeryhandler.py | 0 .../telegram}/ext/_handlers/messagehandler.py | 0 .../ext/_handlers/messagereactionhandler.py | 0 .../_handlers/paidmediapurchasedhandler.py | 0 .../ext/_handlers/pollanswerhandler.py | 0 .../telegram}/ext/_handlers/pollhandler.py | 0 .../ext/_handlers/precheckoutqueryhandler.py | 0 .../telegram}/ext/_handlers/prefixhandler.py | 0 .../ext/_handlers/shippingqueryhandler.py | 0 .../ext/_handlers/stringcommandhandler.py | 0 .../ext/_handlers/stringregexhandler.py | 0 .../telegram}/ext/_handlers/typehandler.py | 0 {telegram => src/telegram}/ext/_jobqueue.py | 0 .../telegram}/ext/_picklepersistence.py | 0 {telegram => src/telegram}/ext/_updater.py | 0 .../telegram}/ext/_utils/__init__.py | 0 .../telegram}/ext/_utils/_update_parsing.py | 0 .../telegram}/ext/_utils/asyncio.py | 0 .../telegram}/ext/_utils/networkloop.py | 0 .../telegram}/ext/_utils/stack.py | 0 .../telegram}/ext/_utils/trackingdict.py | 0 .../telegram}/ext/_utils/types.py | 0 .../telegram}/ext/_utils/webhookhandler.py | 0 {telegram => src/telegram}/ext/filters.py | 0 {telegram => src/telegram}/helpers.py | 0 {telegram => src/telegram}/py.typed | 0 .../telegram}/request/__init__.py | 0 .../telegram}/request/_baserequest.py | 0 .../telegram}/request/_httpxrequest.py | 0 .../telegram}/request/_requestdata.py | 0 .../telegram}/request/_requestparameter.py | 0 {telegram => src/telegram}/warnings.py | 0 tests/README.rst | 6 +++++ tests/auxil/files.py | 1 + tests/ext/test_application.py | 6 ++--- tests/ext/test_conversationhandler.py | 13 ++++------ tests/ext/test_picklepersistence.py | 5 ++-- tests/test_modules.py | 5 +++- tests/test_warnings.py | 4 ++-- 232 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 changes/unreleased/4798.g7G3jRf2ns4ath9LRFEcit.toml rename {telegram => src/telegram}/__init__.py (100%) rename {telegram => src/telegram}/__main__.py (100%) rename {telegram => src/telegram}/_birthdate.py (100%) rename {telegram => src/telegram}/_bot.py (100%) rename {telegram => src/telegram}/_botcommand.py (100%) rename {telegram => src/telegram}/_botcommandscope.py (100%) rename {telegram => src/telegram}/_botdescription.py (100%) rename {telegram => src/telegram}/_botname.py (100%) rename {telegram => src/telegram}/_business.py (100%) rename {telegram => src/telegram}/_callbackquery.py (100%) rename {telegram => src/telegram}/_chat.py (100%) rename {telegram => src/telegram}/_chatadministratorrights.py (100%) rename {telegram => src/telegram}/_chatbackground.py (100%) rename {telegram => src/telegram}/_chatboost.py (100%) rename {telegram => src/telegram}/_chatfullinfo.py (100%) rename {telegram => src/telegram}/_chatinvitelink.py (100%) rename {telegram => src/telegram}/_chatjoinrequest.py (100%) rename {telegram => src/telegram}/_chatlocation.py (100%) rename {telegram => src/telegram}/_chatmember.py (100%) rename {telegram => src/telegram}/_chatmemberupdated.py (100%) rename {telegram => src/telegram}/_chatpermissions.py (100%) rename {telegram => src/telegram}/_choseninlineresult.py (100%) rename {telegram => src/telegram}/_copytextbutton.py (100%) rename {telegram => src/telegram}/_dice.py (100%) rename {telegram => src/telegram}/_files/__init__.py (100%) rename {telegram => src/telegram}/_files/_basemedium.py (100%) rename {telegram => src/telegram}/_files/_basethumbedmedium.py (100%) rename {telegram => src/telegram}/_files/_inputstorycontent.py (100%) rename {telegram => src/telegram}/_files/animation.py (100%) rename {telegram => src/telegram}/_files/audio.py (100%) rename {telegram => src/telegram}/_files/chatphoto.py (100%) rename {telegram => src/telegram}/_files/contact.py (100%) rename {telegram => src/telegram}/_files/document.py (100%) rename {telegram => src/telegram}/_files/file.py (100%) rename {telegram => src/telegram}/_files/inputfile.py (100%) rename {telegram => src/telegram}/_files/inputmedia.py (100%) rename {telegram => src/telegram}/_files/inputprofilephoto.py (100%) rename {telegram => src/telegram}/_files/inputsticker.py (100%) rename {telegram => src/telegram}/_files/location.py (100%) rename {telegram => src/telegram}/_files/photosize.py (100%) rename {telegram => src/telegram}/_files/sticker.py (100%) rename {telegram => src/telegram}/_files/venue.py (100%) rename {telegram => src/telegram}/_files/video.py (100%) rename {telegram => src/telegram}/_files/videonote.py (100%) rename {telegram => src/telegram}/_files/voice.py (100%) rename {telegram => src/telegram}/_forcereply.py (100%) rename {telegram => src/telegram}/_forumtopic.py (100%) rename {telegram => src/telegram}/_games/__init__.py (100%) rename {telegram => src/telegram}/_games/callbackgame.py (100%) rename {telegram => src/telegram}/_games/game.py (100%) rename {telegram => src/telegram}/_games/gamehighscore.py (100%) rename {telegram => src/telegram}/_gifts.py (100%) rename {telegram => src/telegram}/_giveaway.py (100%) rename {telegram => src/telegram}/_inline/__init__.py (100%) rename {telegram => src/telegram}/_inline/inlinekeyboardbutton.py (100%) rename {telegram => src/telegram}/_inline/inlinekeyboardmarkup.py (100%) rename {telegram => src/telegram}/_inline/inlinequery.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresult.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultarticle.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultaudio.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcachedaudio.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcacheddocument.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcachedgif.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcachedmpeg4gif.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcachedphoto.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcachedsticker.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcachedvideo.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcachedvoice.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultcontact.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultdocument.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultgame.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultgif.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultlocation.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultmpeg4gif.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultphoto.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultsbutton.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultvenue.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultvideo.py (100%) rename {telegram => src/telegram}/_inline/inlinequeryresultvoice.py (100%) rename {telegram => src/telegram}/_inline/inputcontactmessagecontent.py (100%) rename {telegram => src/telegram}/_inline/inputinvoicemessagecontent.py (100%) rename {telegram => src/telegram}/_inline/inputlocationmessagecontent.py (100%) rename {telegram => src/telegram}/_inline/inputmessagecontent.py (100%) rename {telegram => src/telegram}/_inline/inputtextmessagecontent.py (100%) rename {telegram => src/telegram}/_inline/inputvenuemessagecontent.py (100%) rename {telegram => src/telegram}/_inline/preparedinlinemessage.py (100%) rename {telegram => src/telegram}/_keyboardbutton.py (100%) rename {telegram => src/telegram}/_keyboardbuttonpolltype.py (100%) rename {telegram => src/telegram}/_keyboardbuttonrequest.py (100%) rename {telegram => src/telegram}/_linkpreviewoptions.py (100%) rename {telegram => src/telegram}/_loginurl.py (100%) rename {telegram => src/telegram}/_menubutton.py (100%) rename {telegram => src/telegram}/_message.py (100%) rename {telegram => src/telegram}/_messageautodeletetimerchanged.py (100%) rename {telegram => src/telegram}/_messageentity.py (100%) rename {telegram => src/telegram}/_messageid.py (100%) rename {telegram => src/telegram}/_messageorigin.py (100%) rename {telegram => src/telegram}/_messagereactionupdated.py (100%) rename {telegram => src/telegram}/_ownedgift.py (100%) rename {telegram => src/telegram}/_paidmedia.py (100%) rename {telegram => src/telegram}/_paidmessagepricechanged.py (100%) rename {telegram => src/telegram}/_passport/__init__.py (100%) rename {telegram => src/telegram}/_passport/credentials.py (100%) rename {telegram => src/telegram}/_passport/data.py (100%) rename {telegram => src/telegram}/_passport/encryptedpassportelement.py (100%) rename {telegram => src/telegram}/_passport/passportdata.py (100%) rename {telegram => src/telegram}/_passport/passportelementerrors.py (100%) rename {telegram => src/telegram}/_passport/passportfile.py (100%) rename {telegram => src/telegram}/_payment/__init__.py (100%) rename {telegram => src/telegram}/_payment/invoice.py (100%) rename {telegram => src/telegram}/_payment/labeledprice.py (100%) rename {telegram => src/telegram}/_payment/orderinfo.py (100%) rename {telegram => src/telegram}/_payment/precheckoutquery.py (100%) rename {telegram => src/telegram}/_payment/refundedpayment.py (100%) rename {telegram => src/telegram}/_payment/shippingaddress.py (100%) rename {telegram => src/telegram}/_payment/shippingoption.py (100%) rename {telegram => src/telegram}/_payment/shippingquery.py (100%) rename {telegram => src/telegram}/_payment/stars/__init__.py (100%) rename {telegram => src/telegram}/_payment/stars/affiliateinfo.py (100%) rename {telegram => src/telegram}/_payment/stars/revenuewithdrawalstate.py (100%) rename {telegram => src/telegram}/_payment/stars/staramount.py (100%) rename {telegram => src/telegram}/_payment/stars/startransactions.py (100%) rename {telegram => src/telegram}/_payment/stars/transactionpartner.py (100%) rename {telegram => src/telegram}/_payment/successfulpayment.py (100%) rename {telegram => src/telegram}/_poll.py (100%) rename {telegram => src/telegram}/_proximityalerttriggered.py (100%) rename {telegram => src/telegram}/_reaction.py (100%) rename {telegram => src/telegram}/_reply.py (100%) rename {telegram => src/telegram}/_replykeyboardmarkup.py (100%) rename {telegram => src/telegram}/_replykeyboardremove.py (100%) rename {telegram => src/telegram}/_sentwebappmessage.py (100%) rename {telegram => src/telegram}/_shared.py (100%) rename {telegram => src/telegram}/_story.py (100%) rename {telegram => src/telegram}/_storyarea.py (100%) rename {telegram => src/telegram}/_switchinlinequerychosenchat.py (100%) rename {telegram => src/telegram}/_telegramobject.py (100%) rename {telegram => src/telegram}/_uniquegift.py (100%) rename {telegram => src/telegram}/_update.py (100%) rename {telegram => src/telegram}/_user.py (100%) rename {telegram => src/telegram}/_userprofilephotos.py (100%) rename {telegram => src/telegram}/_utils/__init__.py (100%) rename {telegram => src/telegram}/_utils/argumentparsing.py (100%) rename {telegram => src/telegram}/_utils/datetime.py (100%) rename {telegram => src/telegram}/_utils/defaultvalue.py (100%) rename {telegram => src/telegram}/_utils/entities.py (100%) rename {telegram => src/telegram}/_utils/enum.py (100%) rename {telegram => src/telegram}/_utils/files.py (100%) rename {telegram => src/telegram}/_utils/logging.py (100%) rename {telegram => src/telegram}/_utils/markup.py (100%) rename {telegram => src/telegram}/_utils/repr.py (100%) rename {telegram => src/telegram}/_utils/strings.py (100%) rename {telegram => src/telegram}/_utils/types.py (100%) rename {telegram => src/telegram}/_utils/warnings.py (100%) rename {telegram => src/telegram}/_utils/warnings_transition.py (100%) rename {telegram => src/telegram}/_version.py (100%) rename {telegram => src/telegram}/_videochat.py (100%) rename {telegram => src/telegram}/_webappdata.py (100%) rename {telegram => src/telegram}/_webappinfo.py (100%) rename {telegram => src/telegram}/_webhookinfo.py (100%) rename {telegram => src/telegram}/_writeaccessallowed.py (100%) rename {telegram => src/telegram}/constants.py (100%) rename {telegram => src/telegram}/error.py (100%) rename {telegram => src/telegram}/ext/__init__.py (100%) rename {telegram => src/telegram}/ext/_aioratelimiter.py (100%) rename {telegram => src/telegram}/ext/_application.py (100%) rename {telegram => src/telegram}/ext/_applicationbuilder.py (100%) rename {telegram => src/telegram}/ext/_basepersistence.py (100%) rename {telegram => src/telegram}/ext/_baseratelimiter.py (100%) rename {telegram => src/telegram}/ext/_baseupdateprocessor.py (100%) rename {telegram => src/telegram}/ext/_callbackcontext.py (100%) rename {telegram => src/telegram}/ext/_callbackdatacache.py (100%) rename {telegram => src/telegram}/ext/_contexttypes.py (100%) rename {telegram => src/telegram}/ext/_defaults.py (100%) rename {telegram => src/telegram}/ext/_dictpersistence.py (100%) rename {telegram => src/telegram}/ext/_extbot.py (100%) rename {telegram => src/telegram}/ext/_handlers/__init__.py (100%) rename {telegram => src/telegram}/ext/_handlers/basehandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/businessconnectionhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/businessmessagesdeletedhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/callbackqueryhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/chatboosthandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/chatjoinrequesthandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/chatmemberhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/choseninlineresulthandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/commandhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/conversationhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/inlinequeryhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/messagehandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/messagereactionhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/paidmediapurchasedhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/pollanswerhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/pollhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/precheckoutqueryhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/prefixhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/shippingqueryhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/stringcommandhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/stringregexhandler.py (100%) rename {telegram => src/telegram}/ext/_handlers/typehandler.py (100%) rename {telegram => src/telegram}/ext/_jobqueue.py (100%) rename {telegram => src/telegram}/ext/_picklepersistence.py (100%) rename {telegram => src/telegram}/ext/_updater.py (100%) rename {telegram => src/telegram}/ext/_utils/__init__.py (100%) rename {telegram => src/telegram}/ext/_utils/_update_parsing.py (100%) rename {telegram => src/telegram}/ext/_utils/asyncio.py (100%) rename {telegram => src/telegram}/ext/_utils/networkloop.py (100%) rename {telegram => src/telegram}/ext/_utils/stack.py (100%) rename {telegram => src/telegram}/ext/_utils/trackingdict.py (100%) rename {telegram => src/telegram}/ext/_utils/types.py (100%) rename {telegram => src/telegram}/ext/_utils/webhookhandler.py (100%) rename {telegram => src/telegram}/ext/filters.py (100%) rename {telegram => src/telegram}/helpers.py (100%) rename {telegram => src/telegram}/py.typed (100%) rename {telegram => src/telegram}/request/__init__.py (100%) rename {telegram => src/telegram}/request/_baserequest.py (100%) rename {telegram => src/telegram}/request/_httpxrequest.py (100%) rename {telegram => src/telegram}/request/_requestdata.py (100%) rename {telegram => src/telegram}/request/_requestparameter.py (100%) rename {telegram => src/telegram}/warnings.py (100%) diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index b545fd29bf1..c4708371ff8 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -22,12 +22,14 @@ Setting things up $ git remote add upstream https://github.com/python-telegram-bot/python-telegram-bot -4. Install dependencies: +4. Install the package in development mode as well as optional dependencies and development dependencies. + Note that the `--group` argument requires `pip` 25.1 or later. .. code-block:: bash - $ pip install .[all] --group all + $ pip install -e .[all] --group all + Installing the package itself is necessary because python-telegram-bot uses a src-based layout where the package code is located in the ``src/`` directory. 5. Install pre-commit hooks: diff --git a/.github/workflows/docs-admonitions.yml b/.github/workflows/docs-admonitions.yml index a2a6468a9a7..c408cad2662 100644 --- a/.github/workflows/docs-admonitions.yml +++ b/.github/workflows/docs-admonitions.yml @@ -2,7 +2,7 @@ name: Test Admonitions Generation on: pull_request: paths: - - telegram/** + - src/telegram/** - docs/** - .github/workflows/docs-admonitions.yml push: diff --git a/.github/workflows/test_official.yml b/.github/workflows/test_official.yml index 054aa1fb733..692bacd8ab7 100644 --- a/.github/workflows/test_official.yml +++ b/.github/workflows/test_official.yml @@ -2,7 +2,7 @@ name: Bot API Tests on: pull_request: paths: - - telegram/** + - src/telegram/** - tests/** push: branches: diff --git a/.github/workflows/type_completeness.yml b/.github/workflows/type_completeness.yml index 3b3f30e4873..56b57f5e539 100644 --- a/.github/workflows/type_completeness.yml +++ b/.github/workflows/type_completeness.yml @@ -2,7 +2,7 @@ name: Check Type Completeness on: pull_request: paths: - - telegram/** + - src/telegram/** - pyproject.toml - .github/workflows/type_completeness.yml push: diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 0ac877748d0..012468d1126 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -2,7 +2,7 @@ name: Unit Tests on: pull_request: paths: - - telegram/** + - src/telegram/** - tests/** - .github/workflows/unit_tests.yml - pyproject.toml diff --git a/changes/unreleased/4798.g7G3jRf2ns4ath9LRFEcit.toml b/changes/unreleased/4798.g7G3jRf2ns4ath9LRFEcit.toml new file mode 100644 index 00000000000..c238ebdfc67 --- /dev/null +++ b/changes/unreleased/4798.g7G3jRf2ns4ath9LRFEcit.toml @@ -0,0 +1,5 @@ +internal = "Rework Repository to `src` Layout" +[[pull_requests]] +uid = "4798" +author_uid = "Bibo-Joshi" +closes_threads = ["4797"] diff --git a/pyproject.toml b/pyproject.toml index 6e5e124f40c..d1f35f0591d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,11 +130,15 @@ all = ["pre-commit", { include-group = "tests" }, { include-group = "docs" }] [tool.hatch.version] # dynamically evaluates the `__version__` variable in that file source = "code" -path = "telegram/_version.py" -search-paths = ["telegram"] +path = "src/telegram/_version.py" -[tool.hatch.build] -packages = ["telegram"] +# See also https://github.com/pypa/hatch/issues/1230 for discussion +# the source distribution will include most of the files in the root directory +[tool.hatch.build.targets.sdist] +exclude = [".venv*", "venv*", ".github"] +# the wheel will only include the src/telegram package +[tool.hatch.build.targets.wheel] +packages = ["src/telegram"] # CHANGO [tool.chango] @@ -167,9 +171,9 @@ select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET [tool.ruff.lint.per-file-ignores] "tests/*.py" = ["B018"] "tests/**.py" = ["RUF012", "ASYNC230", "DTZ", "ARG", "T201", "ASYNC109", "D", "S", "TRY"] -"telegram/**.py" = ["TRY003"] -"telegram/ext/_applicationbuilder.py" = ["TRY004"] -"telegram/ext/filters.py" = ["D102"] +"src/telegram/**.py" = ["TRY003"] +"src/telegram/ext/_applicationbuilder.py" = ["TRY004"] +"src/telegram/ext/filters.py" = ["D102"] "docs/**.py" = ["INP001", "ARG", "D", "TRY003", "S"] "examples/**.py" = ["ARG", "D", "S105", "TRY003"] @@ -197,6 +201,7 @@ exclude-protected = ["_unfrozen"] # PYTEST: [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["src"] addopts = "--no-success-flaky-report -rX" filterwarnings = [ "error", @@ -219,6 +224,7 @@ log_cli_format = "%(funcName)s - Line %(lineno)d - %(message)s" # MYPY: [tool.mypy] +mypy_path = "src" warn_unused_ignores = true warn_unused_configs = true disallow_untyped_defs = true @@ -261,12 +267,12 @@ ignore_missing_imports = true # COVERAGE: [tool.coverage.run] branch = true -source = ["telegram"] +source = ["src/telegram"] parallel = true concurrency = ["thread", "multiprocessing"] omit = [ "tests/", - "telegram/__main__.py" + "src/telegram/__main__.py" ] [tool.coverage.report] diff --git a/telegram/__init__.py b/src/telegram/__init__.py similarity index 100% rename from telegram/__init__.py rename to src/telegram/__init__.py diff --git a/telegram/__main__.py b/src/telegram/__main__.py similarity index 100% rename from telegram/__main__.py rename to src/telegram/__main__.py diff --git a/telegram/_birthdate.py b/src/telegram/_birthdate.py similarity index 100% rename from telegram/_birthdate.py rename to src/telegram/_birthdate.py diff --git a/telegram/_bot.py b/src/telegram/_bot.py similarity index 100% rename from telegram/_bot.py rename to src/telegram/_bot.py diff --git a/telegram/_botcommand.py b/src/telegram/_botcommand.py similarity index 100% rename from telegram/_botcommand.py rename to src/telegram/_botcommand.py diff --git a/telegram/_botcommandscope.py b/src/telegram/_botcommandscope.py similarity index 100% rename from telegram/_botcommandscope.py rename to src/telegram/_botcommandscope.py diff --git a/telegram/_botdescription.py b/src/telegram/_botdescription.py similarity index 100% rename from telegram/_botdescription.py rename to src/telegram/_botdescription.py diff --git a/telegram/_botname.py b/src/telegram/_botname.py similarity index 100% rename from telegram/_botname.py rename to src/telegram/_botname.py diff --git a/telegram/_business.py b/src/telegram/_business.py similarity index 100% rename from telegram/_business.py rename to src/telegram/_business.py diff --git a/telegram/_callbackquery.py b/src/telegram/_callbackquery.py similarity index 100% rename from telegram/_callbackquery.py rename to src/telegram/_callbackquery.py diff --git a/telegram/_chat.py b/src/telegram/_chat.py similarity index 100% rename from telegram/_chat.py rename to src/telegram/_chat.py diff --git a/telegram/_chatadministratorrights.py b/src/telegram/_chatadministratorrights.py similarity index 100% rename from telegram/_chatadministratorrights.py rename to src/telegram/_chatadministratorrights.py diff --git a/telegram/_chatbackground.py b/src/telegram/_chatbackground.py similarity index 100% rename from telegram/_chatbackground.py rename to src/telegram/_chatbackground.py diff --git a/telegram/_chatboost.py b/src/telegram/_chatboost.py similarity index 100% rename from telegram/_chatboost.py rename to src/telegram/_chatboost.py diff --git a/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py similarity index 100% rename from telegram/_chatfullinfo.py rename to src/telegram/_chatfullinfo.py diff --git a/telegram/_chatinvitelink.py b/src/telegram/_chatinvitelink.py similarity index 100% rename from telegram/_chatinvitelink.py rename to src/telegram/_chatinvitelink.py diff --git a/telegram/_chatjoinrequest.py b/src/telegram/_chatjoinrequest.py similarity index 100% rename from telegram/_chatjoinrequest.py rename to src/telegram/_chatjoinrequest.py diff --git a/telegram/_chatlocation.py b/src/telegram/_chatlocation.py similarity index 100% rename from telegram/_chatlocation.py rename to src/telegram/_chatlocation.py diff --git a/telegram/_chatmember.py b/src/telegram/_chatmember.py similarity index 100% rename from telegram/_chatmember.py rename to src/telegram/_chatmember.py diff --git a/telegram/_chatmemberupdated.py b/src/telegram/_chatmemberupdated.py similarity index 100% rename from telegram/_chatmemberupdated.py rename to src/telegram/_chatmemberupdated.py diff --git a/telegram/_chatpermissions.py b/src/telegram/_chatpermissions.py similarity index 100% rename from telegram/_chatpermissions.py rename to src/telegram/_chatpermissions.py diff --git a/telegram/_choseninlineresult.py b/src/telegram/_choseninlineresult.py similarity index 100% rename from telegram/_choseninlineresult.py rename to src/telegram/_choseninlineresult.py diff --git a/telegram/_copytextbutton.py b/src/telegram/_copytextbutton.py similarity index 100% rename from telegram/_copytextbutton.py rename to src/telegram/_copytextbutton.py diff --git a/telegram/_dice.py b/src/telegram/_dice.py similarity index 100% rename from telegram/_dice.py rename to src/telegram/_dice.py diff --git a/telegram/_files/__init__.py b/src/telegram/_files/__init__.py similarity index 100% rename from telegram/_files/__init__.py rename to src/telegram/_files/__init__.py diff --git a/telegram/_files/_basemedium.py b/src/telegram/_files/_basemedium.py similarity index 100% rename from telegram/_files/_basemedium.py rename to src/telegram/_files/_basemedium.py diff --git a/telegram/_files/_basethumbedmedium.py b/src/telegram/_files/_basethumbedmedium.py similarity index 100% rename from telegram/_files/_basethumbedmedium.py rename to src/telegram/_files/_basethumbedmedium.py diff --git a/telegram/_files/_inputstorycontent.py b/src/telegram/_files/_inputstorycontent.py similarity index 100% rename from telegram/_files/_inputstorycontent.py rename to src/telegram/_files/_inputstorycontent.py diff --git a/telegram/_files/animation.py b/src/telegram/_files/animation.py similarity index 100% rename from telegram/_files/animation.py rename to src/telegram/_files/animation.py diff --git a/telegram/_files/audio.py b/src/telegram/_files/audio.py similarity index 100% rename from telegram/_files/audio.py rename to src/telegram/_files/audio.py diff --git a/telegram/_files/chatphoto.py b/src/telegram/_files/chatphoto.py similarity index 100% rename from telegram/_files/chatphoto.py rename to src/telegram/_files/chatphoto.py diff --git a/telegram/_files/contact.py b/src/telegram/_files/contact.py similarity index 100% rename from telegram/_files/contact.py rename to src/telegram/_files/contact.py diff --git a/telegram/_files/document.py b/src/telegram/_files/document.py similarity index 100% rename from telegram/_files/document.py rename to src/telegram/_files/document.py diff --git a/telegram/_files/file.py b/src/telegram/_files/file.py similarity index 100% rename from telegram/_files/file.py rename to src/telegram/_files/file.py diff --git a/telegram/_files/inputfile.py b/src/telegram/_files/inputfile.py similarity index 100% rename from telegram/_files/inputfile.py rename to src/telegram/_files/inputfile.py diff --git a/telegram/_files/inputmedia.py b/src/telegram/_files/inputmedia.py similarity index 100% rename from telegram/_files/inputmedia.py rename to src/telegram/_files/inputmedia.py diff --git a/telegram/_files/inputprofilephoto.py b/src/telegram/_files/inputprofilephoto.py similarity index 100% rename from telegram/_files/inputprofilephoto.py rename to src/telegram/_files/inputprofilephoto.py diff --git a/telegram/_files/inputsticker.py b/src/telegram/_files/inputsticker.py similarity index 100% rename from telegram/_files/inputsticker.py rename to src/telegram/_files/inputsticker.py diff --git a/telegram/_files/location.py b/src/telegram/_files/location.py similarity index 100% rename from telegram/_files/location.py rename to src/telegram/_files/location.py diff --git a/telegram/_files/photosize.py b/src/telegram/_files/photosize.py similarity index 100% rename from telegram/_files/photosize.py rename to src/telegram/_files/photosize.py diff --git a/telegram/_files/sticker.py b/src/telegram/_files/sticker.py similarity index 100% rename from telegram/_files/sticker.py rename to src/telegram/_files/sticker.py diff --git a/telegram/_files/venue.py b/src/telegram/_files/venue.py similarity index 100% rename from telegram/_files/venue.py rename to src/telegram/_files/venue.py diff --git a/telegram/_files/video.py b/src/telegram/_files/video.py similarity index 100% rename from telegram/_files/video.py rename to src/telegram/_files/video.py diff --git a/telegram/_files/videonote.py b/src/telegram/_files/videonote.py similarity index 100% rename from telegram/_files/videonote.py rename to src/telegram/_files/videonote.py diff --git a/telegram/_files/voice.py b/src/telegram/_files/voice.py similarity index 100% rename from telegram/_files/voice.py rename to src/telegram/_files/voice.py diff --git a/telegram/_forcereply.py b/src/telegram/_forcereply.py similarity index 100% rename from telegram/_forcereply.py rename to src/telegram/_forcereply.py diff --git a/telegram/_forumtopic.py b/src/telegram/_forumtopic.py similarity index 100% rename from telegram/_forumtopic.py rename to src/telegram/_forumtopic.py diff --git a/telegram/_games/__init__.py b/src/telegram/_games/__init__.py similarity index 100% rename from telegram/_games/__init__.py rename to src/telegram/_games/__init__.py diff --git a/telegram/_games/callbackgame.py b/src/telegram/_games/callbackgame.py similarity index 100% rename from telegram/_games/callbackgame.py rename to src/telegram/_games/callbackgame.py diff --git a/telegram/_games/game.py b/src/telegram/_games/game.py similarity index 100% rename from telegram/_games/game.py rename to src/telegram/_games/game.py diff --git a/telegram/_games/gamehighscore.py b/src/telegram/_games/gamehighscore.py similarity index 100% rename from telegram/_games/gamehighscore.py rename to src/telegram/_games/gamehighscore.py diff --git a/telegram/_gifts.py b/src/telegram/_gifts.py similarity index 100% rename from telegram/_gifts.py rename to src/telegram/_gifts.py diff --git a/telegram/_giveaway.py b/src/telegram/_giveaway.py similarity index 100% rename from telegram/_giveaway.py rename to src/telegram/_giveaway.py diff --git a/telegram/_inline/__init__.py b/src/telegram/_inline/__init__.py similarity index 100% rename from telegram/_inline/__init__.py rename to src/telegram/_inline/__init__.py diff --git a/telegram/_inline/inlinekeyboardbutton.py b/src/telegram/_inline/inlinekeyboardbutton.py similarity index 100% rename from telegram/_inline/inlinekeyboardbutton.py rename to src/telegram/_inline/inlinekeyboardbutton.py diff --git a/telegram/_inline/inlinekeyboardmarkup.py b/src/telegram/_inline/inlinekeyboardmarkup.py similarity index 100% rename from telegram/_inline/inlinekeyboardmarkup.py rename to src/telegram/_inline/inlinekeyboardmarkup.py diff --git a/telegram/_inline/inlinequery.py b/src/telegram/_inline/inlinequery.py similarity index 100% rename from telegram/_inline/inlinequery.py rename to src/telegram/_inline/inlinequery.py diff --git a/telegram/_inline/inlinequeryresult.py b/src/telegram/_inline/inlinequeryresult.py similarity index 100% rename from telegram/_inline/inlinequeryresult.py rename to src/telegram/_inline/inlinequeryresult.py diff --git a/telegram/_inline/inlinequeryresultarticle.py b/src/telegram/_inline/inlinequeryresultarticle.py similarity index 100% rename from telegram/_inline/inlinequeryresultarticle.py rename to src/telegram/_inline/inlinequeryresultarticle.py diff --git a/telegram/_inline/inlinequeryresultaudio.py b/src/telegram/_inline/inlinequeryresultaudio.py similarity index 100% rename from telegram/_inline/inlinequeryresultaudio.py rename to src/telegram/_inline/inlinequeryresultaudio.py diff --git a/telegram/_inline/inlinequeryresultcachedaudio.py b/src/telegram/_inline/inlinequeryresultcachedaudio.py similarity index 100% rename from telegram/_inline/inlinequeryresultcachedaudio.py rename to src/telegram/_inline/inlinequeryresultcachedaudio.py diff --git a/telegram/_inline/inlinequeryresultcacheddocument.py b/src/telegram/_inline/inlinequeryresultcacheddocument.py similarity index 100% rename from telegram/_inline/inlinequeryresultcacheddocument.py rename to src/telegram/_inline/inlinequeryresultcacheddocument.py diff --git a/telegram/_inline/inlinequeryresultcachedgif.py b/src/telegram/_inline/inlinequeryresultcachedgif.py similarity index 100% rename from telegram/_inline/inlinequeryresultcachedgif.py rename to src/telegram/_inline/inlinequeryresultcachedgif.py diff --git a/telegram/_inline/inlinequeryresultcachedmpeg4gif.py b/src/telegram/_inline/inlinequeryresultcachedmpeg4gif.py similarity index 100% rename from telegram/_inline/inlinequeryresultcachedmpeg4gif.py rename to src/telegram/_inline/inlinequeryresultcachedmpeg4gif.py diff --git a/telegram/_inline/inlinequeryresultcachedphoto.py b/src/telegram/_inline/inlinequeryresultcachedphoto.py similarity index 100% rename from telegram/_inline/inlinequeryresultcachedphoto.py rename to src/telegram/_inline/inlinequeryresultcachedphoto.py diff --git a/telegram/_inline/inlinequeryresultcachedsticker.py b/src/telegram/_inline/inlinequeryresultcachedsticker.py similarity index 100% rename from telegram/_inline/inlinequeryresultcachedsticker.py rename to src/telegram/_inline/inlinequeryresultcachedsticker.py diff --git a/telegram/_inline/inlinequeryresultcachedvideo.py b/src/telegram/_inline/inlinequeryresultcachedvideo.py similarity index 100% rename from telegram/_inline/inlinequeryresultcachedvideo.py rename to src/telegram/_inline/inlinequeryresultcachedvideo.py diff --git a/telegram/_inline/inlinequeryresultcachedvoice.py b/src/telegram/_inline/inlinequeryresultcachedvoice.py similarity index 100% rename from telegram/_inline/inlinequeryresultcachedvoice.py rename to src/telegram/_inline/inlinequeryresultcachedvoice.py diff --git a/telegram/_inline/inlinequeryresultcontact.py b/src/telegram/_inline/inlinequeryresultcontact.py similarity index 100% rename from telegram/_inline/inlinequeryresultcontact.py rename to src/telegram/_inline/inlinequeryresultcontact.py diff --git a/telegram/_inline/inlinequeryresultdocument.py b/src/telegram/_inline/inlinequeryresultdocument.py similarity index 100% rename from telegram/_inline/inlinequeryresultdocument.py rename to src/telegram/_inline/inlinequeryresultdocument.py diff --git a/telegram/_inline/inlinequeryresultgame.py b/src/telegram/_inline/inlinequeryresultgame.py similarity index 100% rename from telegram/_inline/inlinequeryresultgame.py rename to src/telegram/_inline/inlinequeryresultgame.py diff --git a/telegram/_inline/inlinequeryresultgif.py b/src/telegram/_inline/inlinequeryresultgif.py similarity index 100% rename from telegram/_inline/inlinequeryresultgif.py rename to src/telegram/_inline/inlinequeryresultgif.py diff --git a/telegram/_inline/inlinequeryresultlocation.py b/src/telegram/_inline/inlinequeryresultlocation.py similarity index 100% rename from telegram/_inline/inlinequeryresultlocation.py rename to src/telegram/_inline/inlinequeryresultlocation.py diff --git a/telegram/_inline/inlinequeryresultmpeg4gif.py b/src/telegram/_inline/inlinequeryresultmpeg4gif.py similarity index 100% rename from telegram/_inline/inlinequeryresultmpeg4gif.py rename to src/telegram/_inline/inlinequeryresultmpeg4gif.py diff --git a/telegram/_inline/inlinequeryresultphoto.py b/src/telegram/_inline/inlinequeryresultphoto.py similarity index 100% rename from telegram/_inline/inlinequeryresultphoto.py rename to src/telegram/_inline/inlinequeryresultphoto.py diff --git a/telegram/_inline/inlinequeryresultsbutton.py b/src/telegram/_inline/inlinequeryresultsbutton.py similarity index 100% rename from telegram/_inline/inlinequeryresultsbutton.py rename to src/telegram/_inline/inlinequeryresultsbutton.py diff --git a/telegram/_inline/inlinequeryresultvenue.py b/src/telegram/_inline/inlinequeryresultvenue.py similarity index 100% rename from telegram/_inline/inlinequeryresultvenue.py rename to src/telegram/_inline/inlinequeryresultvenue.py diff --git a/telegram/_inline/inlinequeryresultvideo.py b/src/telegram/_inline/inlinequeryresultvideo.py similarity index 100% rename from telegram/_inline/inlinequeryresultvideo.py rename to src/telegram/_inline/inlinequeryresultvideo.py diff --git a/telegram/_inline/inlinequeryresultvoice.py b/src/telegram/_inline/inlinequeryresultvoice.py similarity index 100% rename from telegram/_inline/inlinequeryresultvoice.py rename to src/telegram/_inline/inlinequeryresultvoice.py diff --git a/telegram/_inline/inputcontactmessagecontent.py b/src/telegram/_inline/inputcontactmessagecontent.py similarity index 100% rename from telegram/_inline/inputcontactmessagecontent.py rename to src/telegram/_inline/inputcontactmessagecontent.py diff --git a/telegram/_inline/inputinvoicemessagecontent.py b/src/telegram/_inline/inputinvoicemessagecontent.py similarity index 100% rename from telegram/_inline/inputinvoicemessagecontent.py rename to src/telegram/_inline/inputinvoicemessagecontent.py diff --git a/telegram/_inline/inputlocationmessagecontent.py b/src/telegram/_inline/inputlocationmessagecontent.py similarity index 100% rename from telegram/_inline/inputlocationmessagecontent.py rename to src/telegram/_inline/inputlocationmessagecontent.py diff --git a/telegram/_inline/inputmessagecontent.py b/src/telegram/_inline/inputmessagecontent.py similarity index 100% rename from telegram/_inline/inputmessagecontent.py rename to src/telegram/_inline/inputmessagecontent.py diff --git a/telegram/_inline/inputtextmessagecontent.py b/src/telegram/_inline/inputtextmessagecontent.py similarity index 100% rename from telegram/_inline/inputtextmessagecontent.py rename to src/telegram/_inline/inputtextmessagecontent.py diff --git a/telegram/_inline/inputvenuemessagecontent.py b/src/telegram/_inline/inputvenuemessagecontent.py similarity index 100% rename from telegram/_inline/inputvenuemessagecontent.py rename to src/telegram/_inline/inputvenuemessagecontent.py diff --git a/telegram/_inline/preparedinlinemessage.py b/src/telegram/_inline/preparedinlinemessage.py similarity index 100% rename from telegram/_inline/preparedinlinemessage.py rename to src/telegram/_inline/preparedinlinemessage.py diff --git a/telegram/_keyboardbutton.py b/src/telegram/_keyboardbutton.py similarity index 100% rename from telegram/_keyboardbutton.py rename to src/telegram/_keyboardbutton.py diff --git a/telegram/_keyboardbuttonpolltype.py b/src/telegram/_keyboardbuttonpolltype.py similarity index 100% rename from telegram/_keyboardbuttonpolltype.py rename to src/telegram/_keyboardbuttonpolltype.py diff --git a/telegram/_keyboardbuttonrequest.py b/src/telegram/_keyboardbuttonrequest.py similarity index 100% rename from telegram/_keyboardbuttonrequest.py rename to src/telegram/_keyboardbuttonrequest.py diff --git a/telegram/_linkpreviewoptions.py b/src/telegram/_linkpreviewoptions.py similarity index 100% rename from telegram/_linkpreviewoptions.py rename to src/telegram/_linkpreviewoptions.py diff --git a/telegram/_loginurl.py b/src/telegram/_loginurl.py similarity index 100% rename from telegram/_loginurl.py rename to src/telegram/_loginurl.py diff --git a/telegram/_menubutton.py b/src/telegram/_menubutton.py similarity index 100% rename from telegram/_menubutton.py rename to src/telegram/_menubutton.py diff --git a/telegram/_message.py b/src/telegram/_message.py similarity index 100% rename from telegram/_message.py rename to src/telegram/_message.py diff --git a/telegram/_messageautodeletetimerchanged.py b/src/telegram/_messageautodeletetimerchanged.py similarity index 100% rename from telegram/_messageautodeletetimerchanged.py rename to src/telegram/_messageautodeletetimerchanged.py diff --git a/telegram/_messageentity.py b/src/telegram/_messageentity.py similarity index 100% rename from telegram/_messageentity.py rename to src/telegram/_messageentity.py diff --git a/telegram/_messageid.py b/src/telegram/_messageid.py similarity index 100% rename from telegram/_messageid.py rename to src/telegram/_messageid.py diff --git a/telegram/_messageorigin.py b/src/telegram/_messageorigin.py similarity index 100% rename from telegram/_messageorigin.py rename to src/telegram/_messageorigin.py diff --git a/telegram/_messagereactionupdated.py b/src/telegram/_messagereactionupdated.py similarity index 100% rename from telegram/_messagereactionupdated.py rename to src/telegram/_messagereactionupdated.py diff --git a/telegram/_ownedgift.py b/src/telegram/_ownedgift.py similarity index 100% rename from telegram/_ownedgift.py rename to src/telegram/_ownedgift.py diff --git a/telegram/_paidmedia.py b/src/telegram/_paidmedia.py similarity index 100% rename from telegram/_paidmedia.py rename to src/telegram/_paidmedia.py diff --git a/telegram/_paidmessagepricechanged.py b/src/telegram/_paidmessagepricechanged.py similarity index 100% rename from telegram/_paidmessagepricechanged.py rename to src/telegram/_paidmessagepricechanged.py diff --git a/telegram/_passport/__init__.py b/src/telegram/_passport/__init__.py similarity index 100% rename from telegram/_passport/__init__.py rename to src/telegram/_passport/__init__.py diff --git a/telegram/_passport/credentials.py b/src/telegram/_passport/credentials.py similarity index 100% rename from telegram/_passport/credentials.py rename to src/telegram/_passport/credentials.py diff --git a/telegram/_passport/data.py b/src/telegram/_passport/data.py similarity index 100% rename from telegram/_passport/data.py rename to src/telegram/_passport/data.py diff --git a/telegram/_passport/encryptedpassportelement.py b/src/telegram/_passport/encryptedpassportelement.py similarity index 100% rename from telegram/_passport/encryptedpassportelement.py rename to src/telegram/_passport/encryptedpassportelement.py diff --git a/telegram/_passport/passportdata.py b/src/telegram/_passport/passportdata.py similarity index 100% rename from telegram/_passport/passportdata.py rename to src/telegram/_passport/passportdata.py diff --git a/telegram/_passport/passportelementerrors.py b/src/telegram/_passport/passportelementerrors.py similarity index 100% rename from telegram/_passport/passportelementerrors.py rename to src/telegram/_passport/passportelementerrors.py diff --git a/telegram/_passport/passportfile.py b/src/telegram/_passport/passportfile.py similarity index 100% rename from telegram/_passport/passportfile.py rename to src/telegram/_passport/passportfile.py diff --git a/telegram/_payment/__init__.py b/src/telegram/_payment/__init__.py similarity index 100% rename from telegram/_payment/__init__.py rename to src/telegram/_payment/__init__.py diff --git a/telegram/_payment/invoice.py b/src/telegram/_payment/invoice.py similarity index 100% rename from telegram/_payment/invoice.py rename to src/telegram/_payment/invoice.py diff --git a/telegram/_payment/labeledprice.py b/src/telegram/_payment/labeledprice.py similarity index 100% rename from telegram/_payment/labeledprice.py rename to src/telegram/_payment/labeledprice.py diff --git a/telegram/_payment/orderinfo.py b/src/telegram/_payment/orderinfo.py similarity index 100% rename from telegram/_payment/orderinfo.py rename to src/telegram/_payment/orderinfo.py diff --git a/telegram/_payment/precheckoutquery.py b/src/telegram/_payment/precheckoutquery.py similarity index 100% rename from telegram/_payment/precheckoutquery.py rename to src/telegram/_payment/precheckoutquery.py diff --git a/telegram/_payment/refundedpayment.py b/src/telegram/_payment/refundedpayment.py similarity index 100% rename from telegram/_payment/refundedpayment.py rename to src/telegram/_payment/refundedpayment.py diff --git a/telegram/_payment/shippingaddress.py b/src/telegram/_payment/shippingaddress.py similarity index 100% rename from telegram/_payment/shippingaddress.py rename to src/telegram/_payment/shippingaddress.py diff --git a/telegram/_payment/shippingoption.py b/src/telegram/_payment/shippingoption.py similarity index 100% rename from telegram/_payment/shippingoption.py rename to src/telegram/_payment/shippingoption.py diff --git a/telegram/_payment/shippingquery.py b/src/telegram/_payment/shippingquery.py similarity index 100% rename from telegram/_payment/shippingquery.py rename to src/telegram/_payment/shippingquery.py diff --git a/telegram/_payment/stars/__init__.py b/src/telegram/_payment/stars/__init__.py similarity index 100% rename from telegram/_payment/stars/__init__.py rename to src/telegram/_payment/stars/__init__.py diff --git a/telegram/_payment/stars/affiliateinfo.py b/src/telegram/_payment/stars/affiliateinfo.py similarity index 100% rename from telegram/_payment/stars/affiliateinfo.py rename to src/telegram/_payment/stars/affiliateinfo.py diff --git a/telegram/_payment/stars/revenuewithdrawalstate.py b/src/telegram/_payment/stars/revenuewithdrawalstate.py similarity index 100% rename from telegram/_payment/stars/revenuewithdrawalstate.py rename to src/telegram/_payment/stars/revenuewithdrawalstate.py diff --git a/telegram/_payment/stars/staramount.py b/src/telegram/_payment/stars/staramount.py similarity index 100% rename from telegram/_payment/stars/staramount.py rename to src/telegram/_payment/stars/staramount.py diff --git a/telegram/_payment/stars/startransactions.py b/src/telegram/_payment/stars/startransactions.py similarity index 100% rename from telegram/_payment/stars/startransactions.py rename to src/telegram/_payment/stars/startransactions.py diff --git a/telegram/_payment/stars/transactionpartner.py b/src/telegram/_payment/stars/transactionpartner.py similarity index 100% rename from telegram/_payment/stars/transactionpartner.py rename to src/telegram/_payment/stars/transactionpartner.py diff --git a/telegram/_payment/successfulpayment.py b/src/telegram/_payment/successfulpayment.py similarity index 100% rename from telegram/_payment/successfulpayment.py rename to src/telegram/_payment/successfulpayment.py diff --git a/telegram/_poll.py b/src/telegram/_poll.py similarity index 100% rename from telegram/_poll.py rename to src/telegram/_poll.py diff --git a/telegram/_proximityalerttriggered.py b/src/telegram/_proximityalerttriggered.py similarity index 100% rename from telegram/_proximityalerttriggered.py rename to src/telegram/_proximityalerttriggered.py diff --git a/telegram/_reaction.py b/src/telegram/_reaction.py similarity index 100% rename from telegram/_reaction.py rename to src/telegram/_reaction.py diff --git a/telegram/_reply.py b/src/telegram/_reply.py similarity index 100% rename from telegram/_reply.py rename to src/telegram/_reply.py diff --git a/telegram/_replykeyboardmarkup.py b/src/telegram/_replykeyboardmarkup.py similarity index 100% rename from telegram/_replykeyboardmarkup.py rename to src/telegram/_replykeyboardmarkup.py diff --git a/telegram/_replykeyboardremove.py b/src/telegram/_replykeyboardremove.py similarity index 100% rename from telegram/_replykeyboardremove.py rename to src/telegram/_replykeyboardremove.py diff --git a/telegram/_sentwebappmessage.py b/src/telegram/_sentwebappmessage.py similarity index 100% rename from telegram/_sentwebappmessage.py rename to src/telegram/_sentwebappmessage.py diff --git a/telegram/_shared.py b/src/telegram/_shared.py similarity index 100% rename from telegram/_shared.py rename to src/telegram/_shared.py diff --git a/telegram/_story.py b/src/telegram/_story.py similarity index 100% rename from telegram/_story.py rename to src/telegram/_story.py diff --git a/telegram/_storyarea.py b/src/telegram/_storyarea.py similarity index 100% rename from telegram/_storyarea.py rename to src/telegram/_storyarea.py diff --git a/telegram/_switchinlinequerychosenchat.py b/src/telegram/_switchinlinequerychosenchat.py similarity index 100% rename from telegram/_switchinlinequerychosenchat.py rename to src/telegram/_switchinlinequerychosenchat.py diff --git a/telegram/_telegramobject.py b/src/telegram/_telegramobject.py similarity index 100% rename from telegram/_telegramobject.py rename to src/telegram/_telegramobject.py diff --git a/telegram/_uniquegift.py b/src/telegram/_uniquegift.py similarity index 100% rename from telegram/_uniquegift.py rename to src/telegram/_uniquegift.py diff --git a/telegram/_update.py b/src/telegram/_update.py similarity index 100% rename from telegram/_update.py rename to src/telegram/_update.py diff --git a/telegram/_user.py b/src/telegram/_user.py similarity index 100% rename from telegram/_user.py rename to src/telegram/_user.py diff --git a/telegram/_userprofilephotos.py b/src/telegram/_userprofilephotos.py similarity index 100% rename from telegram/_userprofilephotos.py rename to src/telegram/_userprofilephotos.py diff --git a/telegram/_utils/__init__.py b/src/telegram/_utils/__init__.py similarity index 100% rename from telegram/_utils/__init__.py rename to src/telegram/_utils/__init__.py diff --git a/telegram/_utils/argumentparsing.py b/src/telegram/_utils/argumentparsing.py similarity index 100% rename from telegram/_utils/argumentparsing.py rename to src/telegram/_utils/argumentparsing.py diff --git a/telegram/_utils/datetime.py b/src/telegram/_utils/datetime.py similarity index 100% rename from telegram/_utils/datetime.py rename to src/telegram/_utils/datetime.py diff --git a/telegram/_utils/defaultvalue.py b/src/telegram/_utils/defaultvalue.py similarity index 100% rename from telegram/_utils/defaultvalue.py rename to src/telegram/_utils/defaultvalue.py diff --git a/telegram/_utils/entities.py b/src/telegram/_utils/entities.py similarity index 100% rename from telegram/_utils/entities.py rename to src/telegram/_utils/entities.py diff --git a/telegram/_utils/enum.py b/src/telegram/_utils/enum.py similarity index 100% rename from telegram/_utils/enum.py rename to src/telegram/_utils/enum.py diff --git a/telegram/_utils/files.py b/src/telegram/_utils/files.py similarity index 100% rename from telegram/_utils/files.py rename to src/telegram/_utils/files.py diff --git a/telegram/_utils/logging.py b/src/telegram/_utils/logging.py similarity index 100% rename from telegram/_utils/logging.py rename to src/telegram/_utils/logging.py diff --git a/telegram/_utils/markup.py b/src/telegram/_utils/markup.py similarity index 100% rename from telegram/_utils/markup.py rename to src/telegram/_utils/markup.py diff --git a/telegram/_utils/repr.py b/src/telegram/_utils/repr.py similarity index 100% rename from telegram/_utils/repr.py rename to src/telegram/_utils/repr.py diff --git a/telegram/_utils/strings.py b/src/telegram/_utils/strings.py similarity index 100% rename from telegram/_utils/strings.py rename to src/telegram/_utils/strings.py diff --git a/telegram/_utils/types.py b/src/telegram/_utils/types.py similarity index 100% rename from telegram/_utils/types.py rename to src/telegram/_utils/types.py diff --git a/telegram/_utils/warnings.py b/src/telegram/_utils/warnings.py similarity index 100% rename from telegram/_utils/warnings.py rename to src/telegram/_utils/warnings.py diff --git a/telegram/_utils/warnings_transition.py b/src/telegram/_utils/warnings_transition.py similarity index 100% rename from telegram/_utils/warnings_transition.py rename to src/telegram/_utils/warnings_transition.py diff --git a/telegram/_version.py b/src/telegram/_version.py similarity index 100% rename from telegram/_version.py rename to src/telegram/_version.py diff --git a/telegram/_videochat.py b/src/telegram/_videochat.py similarity index 100% rename from telegram/_videochat.py rename to src/telegram/_videochat.py diff --git a/telegram/_webappdata.py b/src/telegram/_webappdata.py similarity index 100% rename from telegram/_webappdata.py rename to src/telegram/_webappdata.py diff --git a/telegram/_webappinfo.py b/src/telegram/_webappinfo.py similarity index 100% rename from telegram/_webappinfo.py rename to src/telegram/_webappinfo.py diff --git a/telegram/_webhookinfo.py b/src/telegram/_webhookinfo.py similarity index 100% rename from telegram/_webhookinfo.py rename to src/telegram/_webhookinfo.py diff --git a/telegram/_writeaccessallowed.py b/src/telegram/_writeaccessallowed.py similarity index 100% rename from telegram/_writeaccessallowed.py rename to src/telegram/_writeaccessallowed.py diff --git a/telegram/constants.py b/src/telegram/constants.py similarity index 100% rename from telegram/constants.py rename to src/telegram/constants.py diff --git a/telegram/error.py b/src/telegram/error.py similarity index 100% rename from telegram/error.py rename to src/telegram/error.py diff --git a/telegram/ext/__init__.py b/src/telegram/ext/__init__.py similarity index 100% rename from telegram/ext/__init__.py rename to src/telegram/ext/__init__.py diff --git a/telegram/ext/_aioratelimiter.py b/src/telegram/ext/_aioratelimiter.py similarity index 100% rename from telegram/ext/_aioratelimiter.py rename to src/telegram/ext/_aioratelimiter.py diff --git a/telegram/ext/_application.py b/src/telegram/ext/_application.py similarity index 100% rename from telegram/ext/_application.py rename to src/telegram/ext/_application.py diff --git a/telegram/ext/_applicationbuilder.py b/src/telegram/ext/_applicationbuilder.py similarity index 100% rename from telegram/ext/_applicationbuilder.py rename to src/telegram/ext/_applicationbuilder.py diff --git a/telegram/ext/_basepersistence.py b/src/telegram/ext/_basepersistence.py similarity index 100% rename from telegram/ext/_basepersistence.py rename to src/telegram/ext/_basepersistence.py diff --git a/telegram/ext/_baseratelimiter.py b/src/telegram/ext/_baseratelimiter.py similarity index 100% rename from telegram/ext/_baseratelimiter.py rename to src/telegram/ext/_baseratelimiter.py diff --git a/telegram/ext/_baseupdateprocessor.py b/src/telegram/ext/_baseupdateprocessor.py similarity index 100% rename from telegram/ext/_baseupdateprocessor.py rename to src/telegram/ext/_baseupdateprocessor.py diff --git a/telegram/ext/_callbackcontext.py b/src/telegram/ext/_callbackcontext.py similarity index 100% rename from telegram/ext/_callbackcontext.py rename to src/telegram/ext/_callbackcontext.py diff --git a/telegram/ext/_callbackdatacache.py b/src/telegram/ext/_callbackdatacache.py similarity index 100% rename from telegram/ext/_callbackdatacache.py rename to src/telegram/ext/_callbackdatacache.py diff --git a/telegram/ext/_contexttypes.py b/src/telegram/ext/_contexttypes.py similarity index 100% rename from telegram/ext/_contexttypes.py rename to src/telegram/ext/_contexttypes.py diff --git a/telegram/ext/_defaults.py b/src/telegram/ext/_defaults.py similarity index 100% rename from telegram/ext/_defaults.py rename to src/telegram/ext/_defaults.py diff --git a/telegram/ext/_dictpersistence.py b/src/telegram/ext/_dictpersistence.py similarity index 100% rename from telegram/ext/_dictpersistence.py rename to src/telegram/ext/_dictpersistence.py diff --git a/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py similarity index 100% rename from telegram/ext/_extbot.py rename to src/telegram/ext/_extbot.py diff --git a/telegram/ext/_handlers/__init__.py b/src/telegram/ext/_handlers/__init__.py similarity index 100% rename from telegram/ext/_handlers/__init__.py rename to src/telegram/ext/_handlers/__init__.py diff --git a/telegram/ext/_handlers/basehandler.py b/src/telegram/ext/_handlers/basehandler.py similarity index 100% rename from telegram/ext/_handlers/basehandler.py rename to src/telegram/ext/_handlers/basehandler.py diff --git a/telegram/ext/_handlers/businessconnectionhandler.py b/src/telegram/ext/_handlers/businessconnectionhandler.py similarity index 100% rename from telegram/ext/_handlers/businessconnectionhandler.py rename to src/telegram/ext/_handlers/businessconnectionhandler.py diff --git a/telegram/ext/_handlers/businessmessagesdeletedhandler.py b/src/telegram/ext/_handlers/businessmessagesdeletedhandler.py similarity index 100% rename from telegram/ext/_handlers/businessmessagesdeletedhandler.py rename to src/telegram/ext/_handlers/businessmessagesdeletedhandler.py diff --git a/telegram/ext/_handlers/callbackqueryhandler.py b/src/telegram/ext/_handlers/callbackqueryhandler.py similarity index 100% rename from telegram/ext/_handlers/callbackqueryhandler.py rename to src/telegram/ext/_handlers/callbackqueryhandler.py diff --git a/telegram/ext/_handlers/chatboosthandler.py b/src/telegram/ext/_handlers/chatboosthandler.py similarity index 100% rename from telegram/ext/_handlers/chatboosthandler.py rename to src/telegram/ext/_handlers/chatboosthandler.py diff --git a/telegram/ext/_handlers/chatjoinrequesthandler.py b/src/telegram/ext/_handlers/chatjoinrequesthandler.py similarity index 100% rename from telegram/ext/_handlers/chatjoinrequesthandler.py rename to src/telegram/ext/_handlers/chatjoinrequesthandler.py diff --git a/telegram/ext/_handlers/chatmemberhandler.py b/src/telegram/ext/_handlers/chatmemberhandler.py similarity index 100% rename from telegram/ext/_handlers/chatmemberhandler.py rename to src/telegram/ext/_handlers/chatmemberhandler.py diff --git a/telegram/ext/_handlers/choseninlineresulthandler.py b/src/telegram/ext/_handlers/choseninlineresulthandler.py similarity index 100% rename from telegram/ext/_handlers/choseninlineresulthandler.py rename to src/telegram/ext/_handlers/choseninlineresulthandler.py diff --git a/telegram/ext/_handlers/commandhandler.py b/src/telegram/ext/_handlers/commandhandler.py similarity index 100% rename from telegram/ext/_handlers/commandhandler.py rename to src/telegram/ext/_handlers/commandhandler.py diff --git a/telegram/ext/_handlers/conversationhandler.py b/src/telegram/ext/_handlers/conversationhandler.py similarity index 100% rename from telegram/ext/_handlers/conversationhandler.py rename to src/telegram/ext/_handlers/conversationhandler.py diff --git a/telegram/ext/_handlers/inlinequeryhandler.py b/src/telegram/ext/_handlers/inlinequeryhandler.py similarity index 100% rename from telegram/ext/_handlers/inlinequeryhandler.py rename to src/telegram/ext/_handlers/inlinequeryhandler.py diff --git a/telegram/ext/_handlers/messagehandler.py b/src/telegram/ext/_handlers/messagehandler.py similarity index 100% rename from telegram/ext/_handlers/messagehandler.py rename to src/telegram/ext/_handlers/messagehandler.py diff --git a/telegram/ext/_handlers/messagereactionhandler.py b/src/telegram/ext/_handlers/messagereactionhandler.py similarity index 100% rename from telegram/ext/_handlers/messagereactionhandler.py rename to src/telegram/ext/_handlers/messagereactionhandler.py diff --git a/telegram/ext/_handlers/paidmediapurchasedhandler.py b/src/telegram/ext/_handlers/paidmediapurchasedhandler.py similarity index 100% rename from telegram/ext/_handlers/paidmediapurchasedhandler.py rename to src/telegram/ext/_handlers/paidmediapurchasedhandler.py diff --git a/telegram/ext/_handlers/pollanswerhandler.py b/src/telegram/ext/_handlers/pollanswerhandler.py similarity index 100% rename from telegram/ext/_handlers/pollanswerhandler.py rename to src/telegram/ext/_handlers/pollanswerhandler.py diff --git a/telegram/ext/_handlers/pollhandler.py b/src/telegram/ext/_handlers/pollhandler.py similarity index 100% rename from telegram/ext/_handlers/pollhandler.py rename to src/telegram/ext/_handlers/pollhandler.py diff --git a/telegram/ext/_handlers/precheckoutqueryhandler.py b/src/telegram/ext/_handlers/precheckoutqueryhandler.py similarity index 100% rename from telegram/ext/_handlers/precheckoutqueryhandler.py rename to src/telegram/ext/_handlers/precheckoutqueryhandler.py diff --git a/telegram/ext/_handlers/prefixhandler.py b/src/telegram/ext/_handlers/prefixhandler.py similarity index 100% rename from telegram/ext/_handlers/prefixhandler.py rename to src/telegram/ext/_handlers/prefixhandler.py diff --git a/telegram/ext/_handlers/shippingqueryhandler.py b/src/telegram/ext/_handlers/shippingqueryhandler.py similarity index 100% rename from telegram/ext/_handlers/shippingqueryhandler.py rename to src/telegram/ext/_handlers/shippingqueryhandler.py diff --git a/telegram/ext/_handlers/stringcommandhandler.py b/src/telegram/ext/_handlers/stringcommandhandler.py similarity index 100% rename from telegram/ext/_handlers/stringcommandhandler.py rename to src/telegram/ext/_handlers/stringcommandhandler.py diff --git a/telegram/ext/_handlers/stringregexhandler.py b/src/telegram/ext/_handlers/stringregexhandler.py similarity index 100% rename from telegram/ext/_handlers/stringregexhandler.py rename to src/telegram/ext/_handlers/stringregexhandler.py diff --git a/telegram/ext/_handlers/typehandler.py b/src/telegram/ext/_handlers/typehandler.py similarity index 100% rename from telegram/ext/_handlers/typehandler.py rename to src/telegram/ext/_handlers/typehandler.py diff --git a/telegram/ext/_jobqueue.py b/src/telegram/ext/_jobqueue.py similarity index 100% rename from telegram/ext/_jobqueue.py rename to src/telegram/ext/_jobqueue.py diff --git a/telegram/ext/_picklepersistence.py b/src/telegram/ext/_picklepersistence.py similarity index 100% rename from telegram/ext/_picklepersistence.py rename to src/telegram/ext/_picklepersistence.py diff --git a/telegram/ext/_updater.py b/src/telegram/ext/_updater.py similarity index 100% rename from telegram/ext/_updater.py rename to src/telegram/ext/_updater.py diff --git a/telegram/ext/_utils/__init__.py b/src/telegram/ext/_utils/__init__.py similarity index 100% rename from telegram/ext/_utils/__init__.py rename to src/telegram/ext/_utils/__init__.py diff --git a/telegram/ext/_utils/_update_parsing.py b/src/telegram/ext/_utils/_update_parsing.py similarity index 100% rename from telegram/ext/_utils/_update_parsing.py rename to src/telegram/ext/_utils/_update_parsing.py diff --git a/telegram/ext/_utils/asyncio.py b/src/telegram/ext/_utils/asyncio.py similarity index 100% rename from telegram/ext/_utils/asyncio.py rename to src/telegram/ext/_utils/asyncio.py diff --git a/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py similarity index 100% rename from telegram/ext/_utils/networkloop.py rename to src/telegram/ext/_utils/networkloop.py diff --git a/telegram/ext/_utils/stack.py b/src/telegram/ext/_utils/stack.py similarity index 100% rename from telegram/ext/_utils/stack.py rename to src/telegram/ext/_utils/stack.py diff --git a/telegram/ext/_utils/trackingdict.py b/src/telegram/ext/_utils/trackingdict.py similarity index 100% rename from telegram/ext/_utils/trackingdict.py rename to src/telegram/ext/_utils/trackingdict.py diff --git a/telegram/ext/_utils/types.py b/src/telegram/ext/_utils/types.py similarity index 100% rename from telegram/ext/_utils/types.py rename to src/telegram/ext/_utils/types.py diff --git a/telegram/ext/_utils/webhookhandler.py b/src/telegram/ext/_utils/webhookhandler.py similarity index 100% rename from telegram/ext/_utils/webhookhandler.py rename to src/telegram/ext/_utils/webhookhandler.py diff --git a/telegram/ext/filters.py b/src/telegram/ext/filters.py similarity index 100% rename from telegram/ext/filters.py rename to src/telegram/ext/filters.py diff --git a/telegram/helpers.py b/src/telegram/helpers.py similarity index 100% rename from telegram/helpers.py rename to src/telegram/helpers.py diff --git a/telegram/py.typed b/src/telegram/py.typed similarity index 100% rename from telegram/py.typed rename to src/telegram/py.typed diff --git a/telegram/request/__init__.py b/src/telegram/request/__init__.py similarity index 100% rename from telegram/request/__init__.py rename to src/telegram/request/__init__.py diff --git a/telegram/request/_baserequest.py b/src/telegram/request/_baserequest.py similarity index 100% rename from telegram/request/_baserequest.py rename to src/telegram/request/_baserequest.py diff --git a/telegram/request/_httpxrequest.py b/src/telegram/request/_httpxrequest.py similarity index 100% rename from telegram/request/_httpxrequest.py rename to src/telegram/request/_httpxrequest.py diff --git a/telegram/request/_requestdata.py b/src/telegram/request/_requestdata.py similarity index 100% rename from telegram/request/_requestdata.py rename to src/telegram/request/_requestdata.py diff --git a/telegram/request/_requestparameter.py b/src/telegram/request/_requestparameter.py similarity index 100% rename from telegram/request/_requestparameter.py rename to src/telegram/request/_requestparameter.py diff --git a/telegram/warnings.py b/src/telegram/warnings.py similarity index 100% rename from telegram/warnings.py rename to src/telegram/warnings.py diff --git a/tests/README.rst b/tests/README.rst index a6724558041..822c90d35c7 100644 --- a/tests/README.rst +++ b/tests/README.rst @@ -6,6 +6,12 @@ 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 ``pyproject.toml`` file in the root of the repository. +Since PTB uses a src-based layout, make sure you have installed the package in development mode before running the tests: + +.. code-block:: bash + + $ pip install -e . + Running tests ============= diff --git a/tests/auxil/files.py b/tests/auxil/files.py index 21571b1988a..583ae615cef 100644 --- a/tests/auxil/files.py +++ b/tests/auxil/files.py @@ -19,6 +19,7 @@ from pathlib import Path PROJECT_ROOT_PATH = Path(__file__).parent.parent.parent.resolve() +SOURCE_ROOT_PATH = PROJECT_ROOT_PATH / "src" / "telegram" TEST_DATA_PATH = PROJECT_ROOT_PATH / "tests" / "data" diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index fd96aa99e1f..f375559eb3a 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -58,7 +58,7 @@ from telegram.warnings import PTBDeprecationWarning, PTBUserWarning from tests.auxil.asyncio_helpers import call_after from tests.auxil.build_messages import make_message_update -from tests.auxil.files import PROJECT_ROOT_PATH +from tests.auxil.files import SOURCE_ROOT_PATH from tests.auxil.monkeypatch import empty_get_updates, return_true from tests.auxil.networking import send_webhook_message from tests.auxil.pytest_classes import PytestApplication, PytestUpdater, make_bot @@ -1006,7 +1006,7 @@ async def callback(update, context): == "ApplicationHandlerStop is not supported with handlers running non-blocking." ) assert ( - Path(recwarn[0].filename) == PROJECT_ROOT_PATH / "telegram" / "ext" / "_application.py" + Path(recwarn[0].filename) == SOURCE_ROOT_PATH / "ext" / "_application.py" ), "incorrect stacklevel!" async def test_non_blocking_no_error_handler(self, app, caplog): @@ -1079,7 +1079,7 @@ async def error_handler(update, context): == "ApplicationHandlerStop is not supported with handlers running non-blocking." ) assert ( - Path(recwarn[0].filename) == PROJECT_ROOT_PATH / "telegram" / "ext" / "_application.py" + Path(recwarn[0].filename) == SOURCE_ROOT_PATH / "ext" / "_application.py" ), "incorrect stacklevel!" @pytest.mark.parametrize(("block", "expected_output"), [(False, 0), (True, 5)]) diff --git a/tests/ext/test_conversationhandler.py b/tests/ext/test_conversationhandler.py index e57c1faa373..64959f47f47 100644 --- a/tests/ext/test_conversationhandler.py +++ b/tests/ext/test_conversationhandler.py @@ -60,7 +60,7 @@ ) from telegram.warnings import PTBUserWarning from tests.auxil.build_messages import make_command_message -from tests.auxil.files import PROJECT_ROOT_PATH +from tests.auxil.files import SOURCE_ROOT_PATH from tests.auxil.pytest_classes import PytestBot, make_bot from tests.auxil.slots import mro_slots @@ -725,7 +725,7 @@ async def callback(_, __): assert recwarn[0].category is PTBUserWarning assert ( Path(recwarn[0].filename) - == PROJECT_ROOT_PATH / "telegram" / "ext" / "_handlers" / "conversationhandler.py" + == SOURCE_ROOT_PATH / "ext" / "_handlers" / "conversationhandler.py" ), "wrong stacklevel!" assert ( str(recwarn[0].message) @@ -1105,11 +1105,7 @@ async def test_no_running_job_queue_warning(self, app, bot, user1, recwarn, jq): assert warning.category is PTBUserWarning assert ( Path(warning.filename) - == PROJECT_ROOT_PATH - / "telegram" - / "ext" - / "_handlers" - / "conversationhandler.py" + == SOURCE_ROOT_PATH / "ext" / "_handlers" / "conversationhandler.py" ), "wrong stacklevel!" # now set app.job_queue back to it's original value @@ -1428,8 +1424,7 @@ def timeout(*args, **kwargs): assert str(recwarn[0].message).startswith("ApplicationHandlerStop in TIMEOUT") assert recwarn[0].category is PTBUserWarning assert ( - Path(recwarn[0].filename) - == PROJECT_ROOT_PATH / "telegram" / "ext" / "_jobqueue.py" + Path(recwarn[0].filename) == SOURCE_ROOT_PATH / "ext" / "_jobqueue.py" ), "wrong stacklevel!" await app.stop() diff --git a/tests/ext/test_picklepersistence.py b/tests/ext/test_picklepersistence.py index 5ce998c9018..f5c15c5cb9b 100644 --- a/tests/ext/test_picklepersistence.py +++ b/tests/ext/test_picklepersistence.py @@ -28,7 +28,7 @@ from telegram import Chat, Message, TelegramObject, Update, User from telegram.ext import ContextTypes, PersistenceInput, PicklePersistence from telegram.warnings import PTBUserWarning -from tests.auxil.files import PROJECT_ROOT_PATH +from tests.auxil.files import SOURCE_ROOT_PATH from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots @@ -899,8 +899,7 @@ async def test_custom_pickler_unpickler_simple( assert recwarn[-1].category is PTBUserWarning assert str(recwarn[-1].message).startswith("Unknown bot instance found.") assert ( - Path(recwarn[-1].filename) - == PROJECT_ROOT_PATH / "telegram" / "ext" / "_picklepersistence.py" + Path(recwarn[-1].filename) == SOURCE_ROOT_PATH / "ext" / "_picklepersistence.py" ), "wrong stacklevel!" pp = PicklePersistence("pickletest", single_file=False, on_flush=False) pp.set_bot(bot) diff --git a/tests/test_modules.py b/tests/test_modules.py index 086e7fe5a8f..eead2b183f4 100644 --- a/tests/test_modules.py +++ b/tests/test_modules.py @@ -23,9 +23,11 @@ import os from pathlib import Path +from tests.auxil.files import SOURCE_ROOT_PATH + def test_public_submodules_dunder_all(): - modules_to_search = list(Path("telegram").rglob("*.py")) + modules_to_search = list(SOURCE_ROOT_PATH.rglob("*.py")) if not modules_to_search: raise AssertionError("No modules found to search through, please modify this test.") @@ -52,6 +54,7 @@ def test_public_submodules_dunder_all(): def load_module(path: Path): + path = path.relative_to(SOURCE_ROOT_PATH.parent) if path.name == "__init__.py": mod_name = str(path.parent).replace(os.sep, ".") # telegram(.ext) format else: diff --git a/tests/test_warnings.py b/tests/test_warnings.py index 18be85689ed..555d5dcb132 100644 --- a/tests/test_warnings.py +++ b/tests/test_warnings.py @@ -23,7 +23,7 @@ from telegram._utils.warnings import warn from telegram.warnings import PTBDeprecationWarning, PTBRuntimeWarning, PTBUserWarning -from tests.auxil.files import PROJECT_ROOT_PATH +from tests.auxil.files import SOURCE_ROOT_PATH from tests.auxil.slots import mro_slots @@ -66,7 +66,7 @@ def make_assertion(cls): make_assertion(PTBUserWarning) def test_warn(self, recwarn): - expected_file = PROJECT_ROOT_PATH / "telegram" / "_utils" / "warnings.py" + expected_file = SOURCE_ROOT_PATH / "_utils" / "warnings.py" warn("test message") assert len(recwarn) == 1