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 01/26] 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 02/26] 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 03/26] 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 04/26] 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 05/26] 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 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 From d533ea2a72c1d2dce2c0b8bfbfee3934ca245186 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 21:00:57 +0200 Subject: [PATCH 26/26] Update `cachetools` requirement from <5.6.0,>=5.3.3 to >=5.3.3,<6.1.0 (#4801) 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> --- README.rst | 2 +- changes/unreleased/4801.feKaYKKZTZq2KBjhyxVVAM.toml | 5 +++++ pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changes/unreleased/4801.feKaYKKZTZq2KBjhyxVVAM.toml diff --git a/README.rst b/README.rst index 633dc383ad7..63e0d8d54e7 100644 --- a/README.rst +++ b/README.rst @@ -157,7 +157,7 @@ PTB can be installed with optional dependencies: * ``pip install "python-telegram-bot[http2]"`` installs `httpx[http2] `_. Use this, if you want to use HTTP/2. * ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1,<1.3 `_. Use this, if you want to use ``telegram.ext.AIORateLimiter``. * ``pip install "python-telegram-bot[webhooks]"`` installs the `tornado~=6.4 `_ library. Use this, if you want to use ``telegram.ext.Updater.start_webhook``/``telegram.ext.Application.run_webhook``. -* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools>=5.3.3,<5.6.0 `_ library. Use this, if you want to use `arbitrary callback_data `_. +* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools>=5.3.3,<6.1.0 `_ library. Use this, if you want to use `arbitrary callback_data `_. * ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler>=3.10.4,<3.12.0 `_ library. Use this, if you want to use the ``telegram.ext.JobQueue``. To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot[socks,webhooks]"``. diff --git a/changes/unreleased/4801.feKaYKKZTZq2KBjhyxVVAM.toml b/changes/unreleased/4801.feKaYKKZTZq2KBjhyxVVAM.toml new file mode 100644 index 00000000000..6d4e67483c3 --- /dev/null +++ b/changes/unreleased/4801.feKaYKKZTZq2KBjhyxVVAM.toml @@ -0,0 +1,5 @@ +dependencies = "Update cachetools requirement from <5.6.0,>=5.3.3 to >=5.3.3,<6.1.0" +[[pull_requests]] +uid = "4801" +author_uid = "dependabot[bot]" +closes_threads = [] diff --git a/pyproject.toml b/pyproject.toml index d1f35f0591d..d28d0e73c14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ all = [ ] callback-data = [ # Cachetools doesn't have a strict stability policy. Let's be cautious for now. - "cachetools>=5.3.3,<5.6.0", + "cachetools>=5.3.3,<6.1.0", ] ext = [ "python-telegram-bot[callback-data,job-queue,rate-limiter,webhooks]",