diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index cba6d41dc3c..9dc97e078c1 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -31,10 +31,10 @@ jobs: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: # Install a specific version of uv. - version: "0.10.10" + version: "0.11.8" # Install 3.13: python-version: 3.13 diff --git a/.github/workflows/docs-linkcheck.yml b/.github/workflows/docs-linkcheck.yml index da816bfb2d7..02df1034ebd 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: linkcheck-output path: docs/build/html/output.* diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml index 5807b28be53..113e3fcff7f 100644 --- a/.github/workflows/gha_security.yml +++ b/.github/workflows/gha_security.yml @@ -21,13 +21,13 @@ jobs: with: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - name: Run zizmor run: uvx zizmor --persona=pedantic --format sarif . > results.sarif env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: sarif_file: results.sarif category: zizmor diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index d394e462866..a6a42d10dde 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.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 d7376a50535..5a9661de79d 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: python-package-distributions-and-signatures path: dist/ diff --git a/.github/workflows/type_completeness_monthly.yml b/.github/workflows/type_completeness_monthly.yml index 30a8a1c8a3b..f8711035915 100644 --- a/.github/workflows/type_completeness_monthly.yml +++ b/.github/workflows/type_completeness_monthly.yml @@ -18,7 +18,7 @@ jobs: python-version: 3.12 pyright-version: ~=1.1.367 - name: Check Output - uses: jannekem/run-python-script-action@bbfca66c612a28f3eeca0ae40e1f810265e2ea68 # v1.7 + uses: jannekem/run-python-script-action@9d8e2e0878d575fb6073277f38ce3f10ebf4f059 # v1.8 env: TYPE_COMPLETENESS: ${{ steps.pyright-type-completeness.outputs.base-completeness-score }} with: diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 16965b643d8..55fe104e4a5 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -87,15 +87,16 @@ jobs: .test_report_optionals_junit.xml - name: Submit coverage - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 with: env_vars: OS,PYTHON name: ${{ matrix.os }}-${{ matrix.python-version }} fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1 + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 if: ${{ !cancelled() }} with: files: .test_report_no_optionals_junit.xml,.test_report_optionals_junit.xml token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81aba867ac4..10e04d2940c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.15.6' + rev: 'v0.15.12' hooks: # Run the linter: - id: ruff-check @@ -23,7 +23,7 @@ repos: - . # this basically does `pip install -e .` priority: 10 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 + rev: v1.20.2 hooks: - id: mypy name: mypy-ptb diff --git a/AUTHORS.rst b/AUTHORS.rst index 8851cee1668..299f88ad3ee 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -79,6 +79,7 @@ The following wonderful people contributed directly or indirectly to this projec - `kennethcheo `_ - `Kirill Vasin `_ - `Kjwon15 `_ +- `Krishna Chaitanya Balusu `_ - `Li-aung Yip `_ - `locobott `_ - `Loo Zheng Yuan `_ diff --git a/README.rst b/README.rst index bfdedc88a0b..84f80c9bb4f 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-9.5-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-9.6-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -77,7 +77,7 @@ After installing_ the library, be sure to check out the section on `working with Telegram API support ~~~~~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **9.5** are natively supported by this library. +All types and methods of the Telegram Bot API **9.6** 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/5125.filter-fix.toml b/changes/unreleased/5125.filter-fix.toml new file mode 100644 index 00000000000..b76b3683e66 --- /dev/null +++ b/changes/unreleased/5125.filter-fix.toml @@ -0,0 +1,5 @@ +bugfixes = "Fixed incorrect isinstance check in class telegram.ext.filters._MergedFilter for or_filter." +[[pull_requests]] +uid = "5125" +author_uids = ["gistrec"] +closes_threads = [] diff --git a/changes/unreleased/5177.fg9Y3ZSboi94mttv3eas2L.toml b/changes/unreleased/5177.fg9Y3ZSboi94mttv3eas2L.toml new file mode 100644 index 00000000000..c89b0dd3f90 --- /dev/null +++ b/changes/unreleased/5177.fg9Y3ZSboi94mttv3eas2L.toml @@ -0,0 +1,5 @@ +internal = "Update Ruff to v0.15.7" +[[pull_requests]] +uid = "5177" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5183.Enoyw5yoT4S9uRxn7v5ud5.toml b/changes/unreleased/5183.Enoyw5yoT4S9uRxn7v5ud5.toml new file mode 100644 index 00000000000..f5eb4f4671c --- /dev/null +++ b/changes/unreleased/5183.Enoyw5yoT4S9uRxn7v5ud5.toml @@ -0,0 +1,5 @@ +internal = "Migrate from deprecated test-results-action to codecov-action" +[[pull_requests]] +uid = "5183" +author_uids = ["Krishnachaitanyakc"] +closes_threads = ["5158"] diff --git a/changes/unreleased/5184.YKdeCfWDDXk8v3DJx8nTeg.toml b/changes/unreleased/5184.YKdeCfWDDXk8v3DJx8nTeg.toml new file mode 100644 index 00000000000..fe9d3496f80 --- /dev/null +++ b/changes/unreleased/5184.YKdeCfWDDXk8v3DJx8nTeg.toml @@ -0,0 +1,5 @@ +internal = "Update Ruff to v0.15.8" +[[pull_requests]] +uid = "5184" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5185.VTFw6hNJ4A9mGayu2rduUt.toml b/changes/unreleased/5185.VTFw6hNJ4A9mGayu2rduUt.toml new file mode 100644 index 00000000000..4b2a625b5e8 --- /dev/null +++ b/changes/unreleased/5185.VTFw6hNJ4A9mGayu2rduUt.toml @@ -0,0 +1,5 @@ +dependencies = "Update dependency cryptography to v46.0.6 [SECURITY]" +[[pull_requests]] +uid = "5185" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5190.5mL9K6trXT6Y4vLgqxy9jE.toml b/changes/unreleased/5190.5mL9K6trXT6Y4vLgqxy9jE.toml new file mode 100644 index 00000000000..f7f2aa88f99 --- /dev/null +++ b/changes/unreleased/5190.5mL9K6trXT6Y4vLgqxy9jE.toml @@ -0,0 +1,5 @@ +other = "Update codecov/codecov-action action to v5.5.4" +[[pull_requests]] +uid = "5190" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5191.B6JK8KhgoqqawRrrWaSCDH.toml b/changes/unreleased/5191.B6JK8KhgoqqawRrrWaSCDH.toml new file mode 100644 index 00000000000..af1b24d818d --- /dev/null +++ b/changes/unreleased/5191.B6JK8KhgoqqawRrrWaSCDH.toml @@ -0,0 +1,5 @@ +internal = "Update astral-sh/setup-uv action to v7.6.0" +[[pull_requests]] +uid = "5191" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5192.VYDBGMwXeTVyvXESmEnfFa.toml b/changes/unreleased/5192.VYDBGMwXeTVyvXESmEnfFa.toml new file mode 100644 index 00000000000..c8585ba7989 --- /dev/null +++ b/changes/unreleased/5192.VYDBGMwXeTVyvXESmEnfFa.toml @@ -0,0 +1,5 @@ +internal = "Update dependency astral-sh/uv to v0.11.2" +[[pull_requests]] +uid = "5192" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5193.GqEK23Erwr49GQ79uJho38.toml b/changes/unreleased/5193.GqEK23Erwr49GQ79uJho38.toml new file mode 100644 index 00000000000..26e3560e0a5 --- /dev/null +++ b/changes/unreleased/5193.GqEK23Erwr49GQ79uJho38.toml @@ -0,0 +1,5 @@ +internal = "Update dependency mypy to v1.20.0" +[[pull_requests]] +uid = "5193" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5194.bnCTjH5QVYuyFDp4iCwJoV.toml b/changes/unreleased/5194.bnCTjH5QVYuyFDp4iCwJoV.toml new file mode 100644 index 00000000000..928e99daa28 --- /dev/null +++ b/changes/unreleased/5194.bnCTjH5QVYuyFDp4iCwJoV.toml @@ -0,0 +1,5 @@ +internal = "Update github/codeql-action action to v4.35.1" +[[pull_requests]] +uid = "5194" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5196.7keq7yJhXbMb9RyShLHz4D.toml b/changes/unreleased/5196.7keq7yJhXbMb9RyShLHz4D.toml new file mode 100644 index 00000000000..9a1ba1501e1 --- /dev/null +++ b/changes/unreleased/5196.7keq7yJhXbMb9RyShLHz4D.toml @@ -0,0 +1,7 @@ +features = "Full Support for Bot API 9.6" + +pull_requests = [ + { uid = "5196", author_uid = "harshil21" }, + { uid = "5202", author_uid = "ouyooung" }, + { uid = "5197", author_uid = "harshil21" }, +] diff --git a/changes/unreleased/5198.haofa4LoE6k2WQoAUSizM8.toml b/changes/unreleased/5198.haofa4LoE6k2WQoAUSizM8.toml new file mode 100644 index 00000000000..4d894b19706 --- /dev/null +++ b/changes/unreleased/5198.haofa4LoE6k2WQoAUSizM8.toml @@ -0,0 +1,5 @@ +internal = "Update Ruff to v0.15.9" +[[pull_requests]] +uid = "5198" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5199.4qywU9Faav8YhWivBBykyJ.toml b/changes/unreleased/5199.4qywU9Faav8YhWivBBykyJ.toml new file mode 100644 index 00000000000..dc85c2b2c54 --- /dev/null +++ b/changes/unreleased/5199.4qywU9Faav8YhWivBBykyJ.toml @@ -0,0 +1,5 @@ +internal = "Update Mypy to v1.20.1" +[[pull_requests]] +uid = "5199" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5203.jxMZ7aSe7eDKXTvfEdZGzw.toml b/changes/unreleased/5203.jxMZ7aSe7eDKXTvfEdZGzw.toml new file mode 100644 index 00000000000..2534d84cd65 --- /dev/null +++ b/changes/unreleased/5203.jxMZ7aSe7eDKXTvfEdZGzw.toml @@ -0,0 +1,5 @@ +dependencies = "Update dependency cryptography to v46.0.7 [SECURITY]" +[[pull_requests]] +uid = "5203" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5205.KBqhJA9kouNAJmQYNwZHT2.toml b/changes/unreleased/5205.KBqhJA9kouNAJmQYNwZHT2.toml new file mode 100644 index 00000000000..544c91e6df5 --- /dev/null +++ b/changes/unreleased/5205.KBqhJA9kouNAJmQYNwZHT2.toml @@ -0,0 +1,5 @@ +internal = "Update Ruff to v0.15.10" +[[pull_requests]] +uid = "5205" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5206.nXEjjZXrYZiWroBfBeQR3N.toml b/changes/unreleased/5206.nXEjjZXrYZiWroBfBeQR3N.toml new file mode 100644 index 00000000000..2cbdb4d446d --- /dev/null +++ b/changes/unreleased/5206.nXEjjZXrYZiWroBfBeQR3N.toml @@ -0,0 +1,5 @@ +internal = "Update dependency pytest to v9.0.3 [SECURITY]" +[[pull_requests]] +uid = "5206" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5209.msPrg8bYnmr89qXR4fr5bG.toml b/changes/unreleased/5209.msPrg8bYnmr89qXR4fr5bG.toml new file mode 100644 index 00000000000..ee623902d8f --- /dev/null +++ b/changes/unreleased/5209.msPrg8bYnmr89qXR4fr5bG.toml @@ -0,0 +1,5 @@ +internal = "Update Ruff to v0.15.11" +[[pull_requests]] +uid = "5209" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5217.5AVeFbQnudaXWyZ3TM7E5B.toml b/changes/unreleased/5217.5AVeFbQnudaXWyZ3TM7E5B.toml new file mode 100644 index 00000000000..81f26445f4b --- /dev/null +++ b/changes/unreleased/5217.5AVeFbQnudaXWyZ3TM7E5B.toml @@ -0,0 +1,5 @@ +internal = "Update Ruff to v0.15.12" +[[pull_requests]] +uid = "5217" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5220.iAgLEYgfuW46wtvCUx3dhM.toml b/changes/unreleased/5220.iAgLEYgfuW46wtvCUx3dhM.toml new file mode 100644 index 00000000000..81e1d3d1a1f --- /dev/null +++ b/changes/unreleased/5220.iAgLEYgfuW46wtvCUx3dhM.toml @@ -0,0 +1,5 @@ +internal = "Update actions/upload-artifact action to v7.0.1" +[[pull_requests]] +uid = "5220" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5221.UEBLb7KFNwZkZ5fC3CyDBD.toml b/changes/unreleased/5221.UEBLb7KFNwZkZ5fC3CyDBD.toml new file mode 100644 index 00000000000..10aa76f8085 --- /dev/null +++ b/changes/unreleased/5221.UEBLb7KFNwZkZ5fC3CyDBD.toml @@ -0,0 +1,5 @@ +internal = "Update dependency astral-sh/uv to v0.11.8" +[[pull_requests]] +uid = "5221" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5222.dGHpTKrbVb9VQV8pVv9Lii.toml b/changes/unreleased/5222.dGHpTKrbVb9VQV8pVv9Lii.toml new file mode 100644 index 00000000000..b6f8325899e --- /dev/null +++ b/changes/unreleased/5222.dGHpTKrbVb9VQV8pVv9Lii.toml @@ -0,0 +1,5 @@ +internal = "Update github/codeql-action action to v4.35.2" +[[pull_requests]] +uid = "5222" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5223.6Ttu6UoyzkVQddzKrz947W.toml b/changes/unreleased/5223.6Ttu6UoyzkVQddzKrz947W.toml new file mode 100644 index 00000000000..e58e4ffd443 --- /dev/null +++ b/changes/unreleased/5223.6Ttu6UoyzkVQddzKrz947W.toml @@ -0,0 +1,5 @@ +internal = "Update Mypy to v1.20.2" +[[pull_requests]] +uid = "5223" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/changes/unreleased/5224.YVKcje5xUdXTjJJurmAVVD.toml b/changes/unreleased/5224.YVKcje5xUdXTjJJurmAVVD.toml new file mode 100644 index 00000000000..9a5abdba578 --- /dev/null +++ b/changes/unreleased/5224.YVKcje5xUdXTjJJurmAVVD.toml @@ -0,0 +1,5 @@ +internal = "Update jannekem/run-python-script-action action to v1.8" +[[pull_requests]] +uid = "5224" +author_uids = ["renovate[bot]"] +closes_threads = [] diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index c7b178dd4e9..72f58dea7bd 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -496,12 +496,19 @@ - Used for getting information about gifts available for sending * - :meth:`~telegram.Bot.get_chat_gifts` - Used for getting information about gifts owned and hosted by a chat + * - :meth:`~telegram.Bot.get_managed_bot_token` + - Used for getting the token of a managed bot + * - :meth:`~telegram.Bot.replace_managed_bot_token` + - Used for replacing the token of a managed bot * - :meth:`~telegram.Bot.get_me` - Used for getting basic information about the bot * - :meth:`~telegram.Bot.get_user_gifts` - Used for getting information about gifts owned and hosted by a user * - :meth:`~telegram.Bot.save_prepared_inline_message` - Used for storing a message to be sent by a user of a Mini App + * - :meth:`~telegram.Bot.save_prepared_keyboard_button` + - Used for saving a keyboard button to be used in a Mini App + .. raw:: html diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index fb960954fd3..25d9c269838 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -116,11 +116,14 @@ Available Types telegram.keyboardbutton telegram.keyboardbuttonpolltype telegram.keyboardbuttonrequestchat + telegram.keyboardbuttonrequestmanagedbot telegram.keyboardbuttonrequestusers telegram.linkpreviewoptions telegram.location telegram.locationaddress telegram.loginurl + telegram.managedbotcreated + telegram.managedbotupdated telegram.maybeinaccessiblemessage telegram.menubutton telegram.menubuttoncommands @@ -151,7 +154,9 @@ Available Types telegram.photosize telegram.poll telegram.pollanswer - telegram.polloption + telegram.polloptionadded + telegram.polloptiondeleted + telegram.preparedkeyboardbutton telegram.proximityalerttriggered telegram.reactioncount telegram.reactiontype diff --git a/docs/source/telegram.ext.handlers-tree.rst b/docs/source/telegram.ext.handlers-tree.rst index 72e0d824c53..690aca1c537 100644 --- a/docs/source/telegram.ext.handlers-tree.rst +++ b/docs/source/telegram.ext.handlers-tree.rst @@ -16,6 +16,7 @@ Handlers telegram.ext.conversationhandler telegram.ext.filters telegram.ext.inlinequeryhandler + telegram.ext.managedbotupdatedhandler telegram.ext.messagehandler telegram.ext.messagereactionhandler telegram.ext.paidmediapurchasedhandler diff --git a/docs/source/telegram.ext.managedbotupdatedhandler.rst b/docs/source/telegram.ext.managedbotupdatedhandler.rst new file mode 100644 index 00000000000..c59d8080389 --- /dev/null +++ b/docs/source/telegram.ext.managedbotupdatedhandler.rst @@ -0,0 +1,6 @@ +ManagedBotUpdatedHandler +======================== + +.. autoclass:: telegram.ext.ManagedBotUpdatedHandler + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.keyboardbuttonrequestmanagedbot.rst b/docs/source/telegram.keyboardbuttonrequestmanagedbot.rst new file mode 100644 index 00000000000..2840249e15d --- /dev/null +++ b/docs/source/telegram.keyboardbuttonrequestmanagedbot.rst @@ -0,0 +1,6 @@ +KeyboardButtonRequestManagedBot +=============================== + +.. autoclass:: telegram.KeyboardButtonRequestManagedBot + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.managedbotcreated.rst b/docs/source/telegram.managedbotcreated.rst new file mode 100644 index 00000000000..e137de86830 --- /dev/null +++ b/docs/source/telegram.managedbotcreated.rst @@ -0,0 +1,6 @@ +ManagedBotCreated +================= + +.. autoclass:: telegram.ManagedBotCreated + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.managedbotupdated.rst b/docs/source/telegram.managedbotupdated.rst new file mode 100644 index 00000000000..f47bd6b60cc --- /dev/null +++ b/docs/source/telegram.managedbotupdated.rst @@ -0,0 +1,6 @@ +ManagedBotUpdated +================= + +.. autoclass:: telegram.ManagedBotUpdated + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.polloptionadded.rst b/docs/source/telegram.polloptionadded.rst new file mode 100644 index 00000000000..fa4638c38f0 --- /dev/null +++ b/docs/source/telegram.polloptionadded.rst @@ -0,0 +1,6 @@ +PollOptionAdded +=============== + +.. autoclass:: telegram.PollOptionAdded + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.polloptiondeleted.rst b/docs/source/telegram.polloptiondeleted.rst new file mode 100644 index 00000000000..74f317f5604 --- /dev/null +++ b/docs/source/telegram.polloptiondeleted.rst @@ -0,0 +1,6 @@ +PollOptionDeleted +================= + +.. autoclass:: telegram.PollOptionDeleted + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.preparedkeyboardbutton.rst b/docs/source/telegram.preparedkeyboardbutton.rst new file mode 100644 index 00000000000..7a1f40481b5 --- /dev/null +++ b/docs/source/telegram.preparedkeyboardbutton.rst @@ -0,0 +1,6 @@ +PreparedKeyboardButton +====================== + +.. autoclass:: telegram.PreparedKeyboardButton + :members: + :show-inheritance: \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2c9909e04ec..c5a1377164d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ tests = [ # required for building the wheels for releases "build", # For the test suite - "pytest==9.0.2", + "pytest==9.0.3", # needed because pytest doesn't come with native support for coroutines as tests "pytest-asyncio==0.21.2", # xdist runs tests in parallel @@ -133,8 +133,8 @@ docs = [ ] linting = [ "prek", - "ruff==0.15.6", - "mypy==1.19.1", + "ruff==0.15.12", + "mypy==1.20.2", "pylint==4.0.5" ] all = [{ include-group = "tests" }, { include-group = "docs" }, { include-group = "linting"}] diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index fad336e0549..362f3253198 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -177,12 +177,15 @@ "KeyboardButton", "KeyboardButtonPollType", "KeyboardButtonRequestChat", + "KeyboardButtonRequestManagedBot", "KeyboardButtonRequestUsers", "LabeledPrice", "LinkPreviewOptions", "Location", "LocationAddress", "LoginUrl", + "ManagedBotCreated", + "ManagedBotUpdated", "MaskPosition", "MaybeInaccessibleMessage", "MenuButton", @@ -229,8 +232,11 @@ "Poll", "PollAnswer", "PollOption", + "PollOptionAdded", + "PollOptionDeleted", "PreCheckoutQuery", "PreparedInlineMessage", + "PreparedKeyboardButton", "ProximityAlertTriggered", "ReactionCount", "ReactionType", @@ -498,9 +504,14 @@ from ._inline.preparedinlinemessage import PreparedInlineMessage from ._keyboardbutton import KeyboardButton from ._keyboardbuttonpolltype import KeyboardButtonPollType -from ._keyboardbuttonrequest import KeyboardButtonRequestChat, KeyboardButtonRequestUsers +from ._keyboardbuttonrequest import ( + KeyboardButtonRequestChat, + KeyboardButtonRequestManagedBot, + KeyboardButtonRequestUsers, +) from ._linkpreviewoptions import LinkPreviewOptions from ._loginurl import LoginUrl +from ._managedbot import ManagedBotCreated, ManagedBotUpdated from ._menubutton import MenuButton, MenuButtonCommands, MenuButtonDefault, MenuButtonWebApp from ._message import InaccessibleMessage, MaybeInaccessibleMessage, Message from ._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged @@ -564,7 +575,15 @@ RevenueWithdrawalStateSucceeded, ) from ._payment.successfulpayment import SuccessfulPayment -from ._poll import InputPollOption, Poll, PollAnswer, PollOption +from ._poll import ( + InputPollOption, + Poll, + PollAnswer, + PollOption, + PollOptionAdded, + PollOptionDeleted, +) +from ._preparedkeyboardbutton import PreparedKeyboardButton from ._proximityalerttriggered import ProximityAlertTriggered from ._reaction import ( ReactionCount, diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 1050f05baf7..d9d1f83b069 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -78,6 +78,7 @@ from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._inline.preparedinlinemessage import PreparedInlineMessage from telegram._inputchecklist import InputChecklist +from telegram._keyboardbutton import KeyboardButton from telegram._menubutton import MenuButton from telegram._message import Message from telegram._messageid import MessageId @@ -85,6 +86,7 @@ from telegram._payment.stars.staramount import StarAmount from telegram._payment.stars.startransactions import StarTransactions from telegram._poll import InputPollOption, Poll +from telegram._preparedkeyboardbutton import PreparedKeyboardButton from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji from telegram._reply import ReplyParameters from telegram._sentwebappmessage import SentWebAppMessage @@ -102,6 +104,7 @@ from telegram._utils.types import ( BaseUrl, CorrectOptionID, + CorrectOptionIds, FileInput, JSONDict, ODVInput, @@ -114,7 +117,7 @@ from telegram.request import BaseRequest, RequestData from telegram.request._httpxrequest import HTTPXRequest from telegram.request._requestparameter import RequestParameter -from telegram.warnings import PTBUserWarning +from telegram.warnings import PTBDeprecationWarning, PTBUserWarning if TYPE_CHECKING: from telegram import ( @@ -7597,6 +7600,8 @@ async def send_poll( is_anonymous: bool | None = None, type: str | None = None, # pylint: disable=redefined-builtin allows_multiple_answers: bool | None = None, + # tags: deprecated in NEXT.VERSION, to be removed + # replaced by `correct_option_ids` correct_option_id: CorrectOptionID | None = None, is_closed: bool | None = None, disable_notification: ODVInput[bool] = DEFAULT_NONE, @@ -7614,6 +7619,14 @@ async def send_poll( question_entities: Sequence["MessageEntity"] | None = None, message_effect_id: str | None = None, allow_paid_broadcast: bool | None = None, + allows_revoting: bool | None = None, + allow_adding_options: bool | None = None, + hide_results_until_closes: bool | None = None, + correct_option_ids: CorrectOptionIds | None = None, + description: str | None = None, + description_parse_mode: str | None = None, + description_entities: Sequence["MessageEntity"] | None = None, + shuffle_options: bool | None = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int | None = None, @@ -7649,9 +7662,13 @@ async def send_poll( type (:obj:`str`, optional): Poll type, :tg-const:`telegram.Poll.QUIZ` or :tg-const:`telegram.Poll.REGULAR`, defaults to :tg-const:`telegram.Poll.REGULAR`. allows_multiple_answers (:obj:`bool`, optional): :obj:`True`, if the poll allows - multiple answers, ignored for polls in quiz mode, defaults to :obj:`False`. + multiple answers, defaults to :obj:`False`. correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer option, required for polls in quiz mode. + + .. deprecated:: NEXT.VERSION + Bot API 9.6 replaces this with :paramref:`correct_option_ids` instead. + explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters with at most @@ -7716,6 +7733,43 @@ async def send_poll( allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast| .. versionadded:: 21.7 + allows_revoting (:obj:`bool`, optional): :obj:`True`, if the poll allows to + change the chosen answer options, defaults to :obj:`False` + for quizzes and to :obj:`True` for regular polls + + .. versionadded:: NEXT.VERSION + allow_adding_options (:obj:`bool`, optional): :obj:`True`, if answer options can be + added to the poll after creation; not supported for anonymous polls and quizzes + + .. versionadded:: NEXT.VERSION + hide_results_until_closes (:obj:`bool`, optional): :obj:`True`, if poll results + must be shown only after the poll closes + + .. versionadded:: NEXT.VERSION + correct_option_ids (Sequence[:class:`int`], optional): A list of monotonically + increasing 0-based identifiers of the correct answer options, + required for polls in quiz mode. + + .. versionadded:: NEXT.VERSION + description (:obj:`str`, optional): Description of the poll to be sent, + 0-:tg-const:`telegram.Poll.MAX_DESCRIPTION_CHARACTERS` characters + after entities parsing. + + .. versionadded:: NEXT.VERSION + description_parse_mode (:obj:`str`, optional): Mode for parsing entities + in the poll description. See the constants + in :class:`telegram.constants.ParseMode` + + .. versionadded:: NEXT.VERSION + description_entities (Sequence[:class:`telegram.MessageEntity`], optional): A + JSON-serialized list of special entities that appear in the poll description, + which can be specified instead of :paramref:`description_parse_mode` + + .. versionadded:: NEXT.VERSION + shuffle_options (:obj:`bool`, optional): :obj:`True`, if the poll options must be + shown in random order + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -7744,6 +7798,19 @@ async def send_poll( :class:`telegram.error.TelegramError` """ + + if correct_option_id is not None: + warn( + PTBDeprecationWarning( + version="NEXT.VERSION", + message="Bot API 9.6 deprecated `correct_option_id` in favour of " + "`correct_option_ids`, please use that.", + ), + stacklevel=2, + ) + if correct_option_ids is None: + correct_option_ids = [correct_option_id] + data: JSONDict = { "chat_id": chat_id, "question": question, @@ -7755,8 +7822,15 @@ async def send_poll( "is_anonymous": is_anonymous, "type": type, "allows_multiple_answers": allows_multiple_answers, - "correct_option_id": correct_option_id, + "allow_adding_options": allow_adding_options, + "allows_revoting": allows_revoting, + "shuffle_options": shuffle_options, + "hide_results_until_closes": hide_results_until_closes, + "correct_option_ids": correct_option_ids, "is_closed": is_closed, + "description": description, + "description_parse_mode": description_parse_mode, + "description_entities": description_entities, "explanation": explanation, "explanation_entities": explanation_entities, "open_period": open_period, @@ -9874,14 +9948,16 @@ async def gift_premium_subscription( `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. + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER` + :attr:`~MessageEntity.CUSTOM_EMOJI`, and :attr:`~MessageEntity.DATE_TIME` 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. + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, + :attr:`~MessageEntity.CUSTOM_EMOJI`, and :attr:`~MessageEntity.DATE_TIME` are + ignored. Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -11362,14 +11438,16 @@ async def send_gift( `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. + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, + :attr:`~MessageEntity.CUSTOM_EMOJI`, and :attr:`~MessageEntity.DATE_TIME` 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. + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, + :attr:`~MessageEntity.CUSTOM_EMOJI`, and :attr:`~MessageEntity.DATE_TIME` are + ignored. pay_for_upgrade (:obj:`bool`, optional): Pass :obj:`True` to pay for the gift upgrade from the bot's balance, thereby making the upgrade free for the receiver. @@ -12081,6 +12159,130 @@ async def set_chat_member_tag( api_kwargs=api_kwargs, ) + async def get_managed_bot_token( + self, + user_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: JSONDict | None = None, + ) -> str: + """ + Use this method to get the token of a managed bot. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): User identifier of the managed bot whose token will be returned. + + Returns: + :obj:`str`: The token of the managed bot. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + } + + return await self._post( + "getManagedBotToken", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def replace_managed_bot_token( + self, + user_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: JSONDict | None = None, + ) -> str: + """ + Use this method to revoke the current token of a managed bot and generate a new one. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): User identifier of the managed bot whose token will be replaced. + + Returns: + :obj:`str`: The new token of the managed bot. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + } + + return await self._post( + "replaceManagedBotToken", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def save_prepared_keyboard_button( + self, + user_id: int, + button: KeyboardButton, + *, + 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: JSONDict | None = None, + ) -> PreparedKeyboardButton: + """ + Stores a keyboard button that can be used by a user within a Mini App. + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): Unique identifier of the target user that can use the button. + button (:obj:`telegram.KeyboardButton`): An object describing the + button to be saved. The button must be of the type + :attr:`~telegram.KeyboardButton.request_users`, + :attr:`~telegram.KeyboardButton.request_chat`, or + :attr:`~telegram.KeyboardButton.request_managed_bot`. + + Returns: + :class:`telegram.PreparedKeyboardButton` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + "button": button, + } + + return PreparedKeyboardButton.de_json( + await self._post( + "savePreparedKeyboardButton", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + self, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -12421,3 +12623,9 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`get_user_profile_audios`""" setChatMemberTag = set_chat_member_tag """Alias for :meth:`set_chat_member_tag`""" + getManagedBotToken = get_managed_bot_token + """Alias for :meth:`get_managed_bot_token`""" + replaceManagedBotToken = replace_managed_bot_token + """Alias for :meth:`replace_managed_bot_token`""" + savePreparedKeyboardButton = save_prepared_keyboard_button + """Alias for :meth:`save_prepared_keyboard_button`""" diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index 1230bf48277..764da868e7e 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -34,6 +34,7 @@ from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ( CorrectOptionID, + CorrectOptionIds, FileInput, JSONDict, ODVInput, @@ -2296,6 +2297,14 @@ async def send_poll( question_entities: Sequence["MessageEntity"] | None = None, message_effect_id: str | None = None, allow_paid_broadcast: bool | None = None, + shuffle_options: bool | None = None, + allows_revoting: bool | None = None, + correct_option_ids: CorrectOptionIds | None = None, + allow_adding_options: bool | None = None, + hide_results_until_closes: bool | None = None, + description: str | None = None, + description_parse_mode: str | None = None, + description_entities: Sequence["MessageEntity"] | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2323,6 +2332,9 @@ async def send_poll( type=type, # pylint=pylint, allows_multiple_answers=allows_multiple_answers, correct_option_id=correct_option_id, + allows_revoting=allows_revoting, + shuffle_options=shuffle_options, + correct_option_ids=correct_option_ids, is_closed=is_closed, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, @@ -2346,6 +2358,11 @@ async def send_poll( business_connection_id=business_connection_id, question_parse_mode=question_parse_mode, question_entities=question_entities, + description=description, + description_parse_mode=description_parse_mode, + description_entities=description_entities, + hide_results_until_closes=hide_results_until_closes, + allow_adding_options=allow_adding_options, ) async def send_copy( diff --git a/src/telegram/_files/sticker.py b/src/telegram/_files/sticker.py index e7854b53ab9..9ab6bd09aa5 100644 --- a/src/telegram/_files/sticker.py +++ b/src/telegram/_files/sticker.py @@ -311,7 +311,7 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "StickerSet": data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot) data["stickers"] = de_list_optional(data.get("stickers"), Sticker, bot) - api_kwargs = {} + api_kwargs: JSONDict = {} # These are deprecated fields that TG still returns for backwards compatibility # Let's filter them out to speed up the de-json process for deprecated_field in ("contains_masks", "thumb", "is_animated", "is_video"): diff --git a/src/telegram/_inputchecklist.py b/src/telegram/_inputchecklist.py index d8fd06258ca..21ae2f491f6 100644 --- a/src/telegram/_inputchecklist.py +++ b/src/telegram/_inputchecklist.py @@ -49,8 +49,8 @@ class InputChecklistTask(TelegramObject): |parse_mode| text_entities (Sequence[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the text, which can be specified instead of - parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler, and - custom_emoji entities are allowed. + parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler, + custom_emoji, and date_time entities are allowed. Attributes: id (:obj:`int`): @@ -66,7 +66,7 @@ class InputChecklistTask(TelegramObject): text_entities (Sequence[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the text, which can be specified instead of parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler, - and custom_emoji entities are allowed. + custom_emoji, and date_time entities are allowed. """ @@ -117,7 +117,7 @@ class InputChecklist(TelegramObject): title_entities (Sequence[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the title, which can be specified instead of :paramref:`parse_mode`. Currently, only bold, italic, - underline, strikethrough, spoiler, and custom_emoji entities are allowed. + underline, strikethrough, spoiler, and custom_emoji, and date_time entities are allowed tasks (Sequence[:class:`telegram.InputChecklistTask`]): List of :tg-const:`telegram.constants.InputChecklistLimit.MIN_TASK_NUMBER`\ @@ -139,7 +139,7 @@ class InputChecklist(TelegramObject): title_entities (Sequence[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the title, which can be specified instead of :paramref:`parse_mode`. Currently, only bold, italic, - underline, strikethrough, spoiler, and custom_emoji entities are allowed. + underline, strikethrough, spoiler, and custom_emoji, and date_time entities are allowed tasks (Sequence[:class:`telegram.InputChecklistTask`]): List of :tg-const:`telegram.constants.InputChecklistLimit.MIN_TASK_NUMBER`\ diff --git a/src/telegram/_keyboardbutton.py b/src/telegram/_keyboardbutton.py index 750062bf516..98aeeaeea35 100644 --- a/src/telegram/_keyboardbutton.py +++ b/src/telegram/_keyboardbutton.py @@ -21,7 +21,11 @@ from typing import TYPE_CHECKING from telegram._keyboardbuttonpolltype import KeyboardButtonPollType -from telegram._keyboardbuttonrequest import KeyboardButtonRequestChat, KeyboardButtonRequestUsers +from telegram._keyboardbuttonrequest import ( + KeyboardButtonRequestChat, + KeyboardButtonRequestManagedBot, + KeyboardButtonRequestUsers, +) from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict @@ -120,6 +124,14 @@ class KeyboardButton(TelegramObject): Premium subscription. .. versionadded:: 22.7 + request_managed_bot (:obj:`telegram.KeyboardButtonRequestManagedBot`, optional): If + specified, pressing the button will ask the user to create and share a bot that will + be managed by the current bot. Available for bots that enabled management of other bots + in the `@BotFather ` Mini App. Available in private + chats only. + + .. versionadded:: NEXT.VERSION + Attributes: text (:obj:`str`): Text of the button. If none of the fields other than :attr:`text`, @@ -167,6 +179,13 @@ class KeyboardButton(TelegramObject): Premium subscription. .. versionadded:: 22.7 + request_managed_bot (:obj:`telegram.KeyboardButtonRequestManagedBot`): Optional. If + specified, pressing the button will ask the user to create and share a bot that will + be managed by the current bot. Available for bots that enabled management of other bots + in the `@BotFather ` Mini App. Available in private + chats only. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -174,6 +193,7 @@ class KeyboardButton(TelegramObject): "request_chat", "request_contact", "request_location", + "request_managed_bot", "request_poll", "request_users", "style", @@ -192,6 +212,7 @@ def __init__( request_users: KeyboardButtonRequestUsers | None = None, style: str | None = None, icon_custom_emoji_id: str | None = None, + request_managed_bot: KeyboardButtonRequestManagedBot | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -208,6 +229,7 @@ def __init__( self.request_chat: KeyboardButtonRequestChat | None = request_chat self.style: str | None = style self.icon_custom_emoji_id: str | None = icon_custom_emoji_id + self.request_managed_bot: KeyboardButtonRequestManagedBot | None = request_managed_bot self._id_attrs = ( self.text, @@ -238,6 +260,9 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "KeyboardButton": data.get("request_chat"), KeyboardButtonRequestChat, bot ) data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot) + data["request_managed_bot"] = de_json_optional( + data.get("request_managed_bot"), KeyboardButtonRequestManagedBot, bot + ) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility diff --git a/src/telegram/_keyboardbuttonrequest.py b/src/telegram/_keyboardbuttonrequest.py index b5e0b050436..fa8e3513c1c 100644 --- a/src/telegram/_keyboardbuttonrequest.py +++ b/src/telegram/_keyboardbuttonrequest.py @@ -268,3 +268,53 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "KeyboardButtonReq ) return super().de_json(data=data, bot=bot) + + +class KeyboardButtonRequestManagedBot(TelegramObject): + """ + This object defines the parameters for the creation of a managed bot. + Information about the created bot will be shared with the bot using the update + managed_bot and a :obj:`telegram.Message` with the field + :attr:`telegram.Message.managed_bot_created`. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`request_id` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + request_id (:obj:`int`): Signed 32-bit identifier of the request. Must be unique + within the message. + suggested_name (:obj:`str`, optional): Suggested name for the bot. + suggested_username (:obj:`str`, optional): Suggested username for the bot. + + Attributes: + request_id (:obj:`int`): Signed 32-bit identifier of the request. Must be unique + within the message. + suggested_name (:obj:`str`): Optional. Suggested name for the bot. + suggested_username (:obj:`str`): Optional. Suggested username for the bot. + """ + + __slots__ = ( + "request_id", + "suggested_name", + "suggested_username", + ) + + def __init__( + self, + request_id: int, + suggested_name: str | None = None, + suggested_username: str | None = None, + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.request_id: int = request_id + # Optional + self.suggested_name: str | None = suggested_name + self.suggested_username: str | None = suggested_username + + self._id_attrs = (self.request_id,) + self._freeze() diff --git a/src/telegram/_managedbot.py b/src/telegram/_managedbot.py new file mode 100644 index 00000000000..109983c656e --- /dev/null +++ b/src/telegram/_managedbot.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# 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 managed bots in the Telegram Bot API.""" + +from typing import TYPE_CHECKING + +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.types import ( + JSONDict, +) + +if TYPE_CHECKING: + from telegram import Bot + + +class ManagedBotCreated(TelegramObject): + """ + This object contains information about the bot that was created to be managed by the current + bot. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`bot` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + bot (:class:`telegram.User`): Information about the bot. The bot's token can be fetched + using the method :meth:`~telegram.Bot.get_managed_bot_token`. + Attributes: + bot (:class:`telegram.User`): Information about the bot. The bot's token can be fetched + using the method :meth:`~telegram.Bot.get_managed_bot_token`. + """ + + __slots__ = ("bot",) + + def __init__( + self, + bot: User, + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.bot: User = bot + self._id_attrs = (self.bot,) + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ManagedBotCreated": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data=data) + + data["bot"] = User.de_json(data=data["bot"], bot=bot) + + return super().de_json(data=data, bot=bot) + + +class ManagedBotUpdated(TelegramObject): + """ + This object contains information about the creation, token update, or owner update of a bot + that is managed by the current bot. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`user` and :attr:`bot` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + user (:class:`telegram.User`): User that created the bot. + bot (:class:`telegram.User`): Information about the bot. Token of the bot can be fetched + using the method :meth:`~telegram.Bot.get_managed_bot_token`. + + Attributes: + user (:class:`telegram.User`): User that created the bot. + bot (:class:`telegram.User`): Information about the bot. Token of the bot can be fetched + using the method :meth:`~telegram.Bot.get_managed_bot_token`. + """ + + __slots__ = ("bot", "user") + + def __init__( + self, + user: User, + bot: User, + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.user: User = user + self.bot: User = bot + + self._id_attrs = ( + self.user, + self.bot, + ) + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ManagedBotUpdated": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data=data) + + data["user"] = User.de_json(data=data["user"], bot=bot) + data["bot"] = User.de_json(data=data["bot"], bot=bot) + + return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_message.py b/src/telegram/_message.py index c0d018ec406..8c6626f50a6 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -57,6 +57,7 @@ from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inputchecklist import InputChecklist from telegram._linkpreviewoptions import LinkPreviewOptions +from telegram._managedbot import ManagedBotCreated from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from telegram._messageentity import MessageEntity from telegram._paidmedia import PaidMediaInfo @@ -65,7 +66,7 @@ from telegram._payment.invoice import Invoice from telegram._payment.refundedpayment import RefundedPayment from telegram._payment.successfulpayment import SuccessfulPayment -from telegram._poll import Poll +from telegram._poll import Poll, PollOptionAdded, PollOptionDeleted from telegram._proximityalerttriggered import ProximityAlertTriggered from telegram._reply import ReplyParameters from telegram._shared import ChatShared, UsersShared @@ -80,6 +81,7 @@ from telegram._utils.strings import TextEncoding from telegram._utils.types import ( CorrectOptionID, + CorrectOptionIds, JSONDict, MarkdownVersion, ODVInput, @@ -689,6 +691,23 @@ class Message(MaybeInaccessibleMessage): supergroups only .. versionadded:: 22.7 + poll_option_added (:class:`telegram.PollOptionAdded`, optional): Service message: + answer option was added to a poll. + + .. versionadded:: NEXT.VERSION + poll_option_deleted (:class:`telegram.PollOptionDeleted`, optional): Service message: + answer option was deleted from a poll. + + .. versionadded:: NEXT.VERSION + reply_to_poll_option_id (:obj:`str`, optional): Persistent + identifier of the specific poll option that is being replied to. + + .. versionadded:: NEXT.VERSION + + managed_bot_created (:class:`telegram.ManagedBotCreated`, optional): Service message: user + created a bot that will be managed by the current bot. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. In specific instances @@ -1105,6 +1124,22 @@ class Message(MaybeInaccessibleMessage): supergroups only .. versionadded:: 22.7 + poll_option_added (:class:`telegram.PollOptionAdded`): Optional. Service message: + answer option was added to a poll. + + .. versionadded:: NEXT.VERSION + poll_option_deleted (:class:`telegram.PollOptionDeleted`): Optional. Service message: + answer option was deleted from a poll. + + .. versionadded:: NEXT.VERSION + reply_to_poll_option_id (:obj:`str`): Optional. Persistent + identifier of the specific poll option that is being replied to. + + .. versionadded:: NEXT.VERSION + managed_bot_created (:class:`telegram.ManagedBotCreated`): Optional. Service message: user + created a bot that will be managed by the current bot. + + .. versionadded:: NEXT.VERSION .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a @@ -1176,6 +1211,7 @@ class Message(MaybeInaccessibleMessage): "left_chat_member", "link_preview_options", "location", + "managed_bot_created", "media_group_id", "message_auto_delete_timer_changed", "message_thread_id", @@ -1191,12 +1227,15 @@ class Message(MaybeInaccessibleMessage): "photo", "pinned_message", "poll", + "poll_option_added", + "poll_option_deleted", "proximity_alert_triggered", "quote", "refunded_payment", "reply_markup", "reply_to_checklist_task_id", "reply_to_message", + "reply_to_poll_option_id", "reply_to_story", "sender_boost_count", "sender_business_bot", @@ -1337,6 +1376,10 @@ def __init__( chat_owner_changed: ChatOwnerChanged | None = None, chat_owner_left: ChatOwnerLeft | None = None, sender_tag: str | None = None, + poll_option_added: PollOptionAdded | None = None, + poll_option_deleted: PollOptionDeleted | None = None, + reply_to_poll_option_id: str | None = None, + managed_bot_created: ManagedBotCreated | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -1467,6 +1510,10 @@ def __init__( self.chat_owner_changed: ChatOwnerChanged | None = chat_owner_changed self.chat_owner_left: ChatOwnerLeft | None = chat_owner_left self.sender_tag: str | None = sender_tag + self.poll_option_added: PollOptionAdded | None = poll_option_added + self.poll_option_deleted: PollOptionDeleted | None = poll_option_deleted + self.reply_to_poll_option_id: str | None = reply_to_poll_option_id + self.managed_bot_created: ManagedBotCreated | None = managed_bot_created self._effective_attachment = DEFAULT_NONE @@ -1687,6 +1734,15 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Message": data.get("chat_owner_changed"), ChatOwnerChanged, bot ) data["chat_owner_left"] = de_json_optional(data.get("chat_owner_left"), ChatOwnerLeft, bot) + data["poll_option_added"] = de_json_optional( + data.get("poll_option_added"), PollOptionAdded, bot + ) + data["poll_option_deleted"] = de_json_optional( + data.get("poll_option_deleted"), PollOptionDeleted, bot + ) + data["managed_bot_created"] = de_json_optional( + data.get("managed_bot_created"), ManagedBotCreated, bot + ) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility @@ -3473,6 +3529,14 @@ async def reply_poll( question_entities: Sequence["MessageEntity"] | None = None, message_effect_id: str | None = None, allow_paid_broadcast: bool | None = None, + shuffle_options: bool | None = None, + allows_revoting: bool | None = None, + correct_option_ids: CorrectOptionIds | None = None, + allow_adding_options: bool | None = None, + hide_results_until_closes: bool | None = None, + description: str | None = None, + description_parse_mode: str | None = None, + description_entities: Sequence["MessageEntity"] | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3522,6 +3586,9 @@ async def reply_poll( type=type, allows_multiple_answers=allows_multiple_answers, correct_option_id=correct_option_id, + allows_revoting=allows_revoting, + shuffle_options=shuffle_options, + correct_option_ids=correct_option_ids, is_closed=is_closed, disable_notification=disable_notification, reply_parameters=effective_reply_parameters, @@ -3543,6 +3610,11 @@ async def reply_poll( question_entities=question_entities, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + description=description, + description_parse_mode=description_parse_mode, + description_entities=description_entities, + hide_results_until_closes=hide_results_until_closes, + allow_adding_options=allow_adding_options, ) async def reply_dice( diff --git a/src/telegram/_poll.py b/src/telegram/_poll.py index 744edd22eff..ba02062b33a 100644 --- a/src/telegram/_poll.py +++ b/src/telegram/_poll.py @@ -42,9 +42,11 @@ from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict, ODVInput, TimePeriod +from telegram._utils.warnings import warn +from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: - from telegram import Bot + from telegram import Bot, MaybeInaccessibleMessage class InputPollOption(TelegramObject): @@ -118,6 +120,10 @@ class PollOption(TelegramObject): considered equal, if their :attr:`text` and :attr:`voter_count` are equal. Args: + persistent_id (:obj:`str`): Unique identifier of the option, persistent on option addition + and deletion. + + .. versionadded:: NEXT.VERSION text (:obj:`str`): Option text, :tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH` characters. @@ -127,8 +133,24 @@ class PollOption(TelegramObject): poll option texts. .. versionadded:: 21.2 + added_by_user (:class:`telegram.User`, optional): User who added the option; + omitted if the option wasn't added by a user after poll creation. + + .. versionadded:: NEXT.VERSION + added_by_chat (:class:`telegram.Chat`, optional): Chat that added the option; + omitted if the option wasn't added by a chat after poll creation. + + .. versionadded:: NEXT.VERSION + addition_date (:obj:`datetime.datetime`, optional): Point in time + when the option was added; omitted if the option existed in the original poll. + + .. versionadded:: NEXT.VERSION Attributes: + persistent_id (:obj:`str`): Unique identifier of the option, persistent on option addition + and deletion. + + .. versionadded:: NEXT.VERSION text (:obj:`str`): Option text, :tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH` characters. @@ -139,22 +161,52 @@ class PollOption(TelegramObject): This list is empty if the question does not contain entities. .. versionadded:: 21.2 + added_by_user (:class:`telegram.User`): Optional. User who added the option; + omitted if the option wasn't added by a user after poll creation. + + .. versionadded:: NEXT.VERSION + added_by_chat (:class:`telegram.Chat`): Optional. Chat that added the option; + omitted if the option wasn't added by a chat after poll creation. + + .. versionadded:: NEXT.VERSION + addition_date (:obj:`datetime.datetime`): Optional. Point in time + when the option was added; omitted if the option existed in the original poll. + .. versionadded:: NEXT.VERSION """ - __slots__ = ("text", "text_entities", "voter_count") + __slots__ = ( + "added_by_chat", + "added_by_user", + "addition_date", + "persistent_id", + "text", + "text_entities", + "voter_count", + ) def __init__( self, text: str, voter_count: int, text_entities: Sequence[MessageEntity] | None = None, + added_by_user: User | None = None, + added_by_chat: Chat | None = None, + addition_date: dtm.datetime | None = None, + # tags: required in NEXT.VERSION, bot api 9.6 + # temporarily optional to avoid breaking changes + persistent_id: str | None = None, *, api_kwargs: JSONDict | None = None, ): super().__init__(api_kwargs=api_kwargs) self.text: str = text self.voter_count: int = voter_count + self.added_by_user: User | None = added_by_user + self.added_by_chat: Chat | None = added_by_chat + self.addition_date: dtm.datetime | None = addition_date + self.persistent_id: str | None = persistent_id + self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) self._id_attrs = (self.text, self.voter_count) @@ -166,7 +218,13 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PollOption": """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot) + data["added_by_user"] = de_json_optional(data.get("added_by_user"), User, bot) + data["added_by_chat"] = de_json_optional(data.get("added_by_chat"), Chat, bot) + data["addition_date"] = from_timestamp(data.get("addition_date"), tzinfo=loc_tzinfo) return super().de_json(data=data, bot=bot) @@ -248,6 +306,10 @@ class PollAnswer(TelegramObject): .. versionchanged:: 20.0 |sequenceclassargs| + option_persistent_ids (Sequence[:obj:`str`]): Persistent identifiers of the + chosen answer options. May be empty if the vote was retracted. + + .. versionadded:: NEXT.VERSION user (:class:`telegram.User`, optional): The user that changed the answer to the poll, if the voter isn't anonymous. If the voter is anonymous, this field will contain the user :tg-const:`telegram.constants.ChatID.FAKE_CHANNEL` for backwards compatibility. @@ -266,6 +328,10 @@ class PollAnswer(TelegramObject): .. versionchanged:: 20.0 |tupleclassattrs| + option_persistent_ids (tuple[:obj:`str`]): Persistent identifiers of the + chosen answer options. May be empty if the vote was retracted. + + .. versionadded:: NEXT.VERSION user (:class:`telegram.User`): Optional. The user, who changed the answer to the poll, if the voter isn't anonymous. If the voter is anonymous, this field will contain the user :tg-const:`telegram.constants.ChatID.FAKE_CHANNEL` for backwards compatibility @@ -276,10 +342,9 @@ class PollAnswer(TelegramObject): poll, if the voter is anonymous. .. versionadded:: 20.5 - """ - __slots__ = ("option_ids", "poll_id", "user", "voter_chat") + __slots__ = ("option_ids", "option_persistent_ids", "poll_id", "user", "voter_chat") def __init__( self, @@ -287,6 +352,9 @@ def __init__( option_ids: Sequence[int], user: User | None = None, voter_chat: Chat | None = None, + # tags: required in NEXT.VERSION, bot api 9.6 + # temporarily optional to avoid breaking changes + option_persistent_ids: Sequence[str] | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -295,6 +363,7 @@ def __init__( self.voter_chat: Chat | None = voter_chat self.option_ids: tuple[int, ...] = parse_sequence_arg(option_ids) self.user: User | None = user + self.option_persistent_ids: tuple[str, ...] = parse_sequence_arg(option_persistent_ids) self._id_attrs = ( self.poll_id, @@ -316,6 +385,236 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PollAnswer": return super().de_json(data=data, bot=bot) +class PollOptionAdded(TelegramObject): + """ + Describes a service message about an option added to a poll. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`option_persistent_id`, and :attr:`option_text` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + option_persistent_id (:obj:`str`): Unique identifier of the added option. + option_text (:obj:`str`): Option text. + poll_message (:class:`telegram.MaybeInaccessibleMessage`, optional): Message + containing the poll to which the option was added, if known. + Note that the Message object in this field will not contain the + :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + option_text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special + entities that appear in the :paramref:`option_text`. + + Attributes: + option_persistent_id (:obj:`str`): Unique identifier of the added option. + option_text (:obj:`str`): Option text. + poll_message (:class:`telegram.MaybeInaccessibleMessage`): Optional. Message + containing the poll to which the option was added, if known. + Note that the Message object in this field will not contain the + :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + option_text_entities (tuple[:class:`telegram.MessageEntity`]): Optional. Special + entities that appear in the :paramref:`option_text`. + """ + + __slots__ = ("option_persistent_id", "option_text", "option_text_entities", "poll_message") + + def __init__( + self, + option_persistent_id: str, + option_text: str, + poll_message: "MaybeInaccessibleMessage | None" = None, + option_text_entities: Sequence[MessageEntity] | None = None, + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.option_persistent_id: str = option_persistent_id + self.option_text: str = option_text + self.poll_message: MaybeInaccessibleMessage | None = poll_message + + self.option_text_entities: tuple[MessageEntity, ...] = parse_sequence_arg( + option_text_entities + ) + + self._id_attrs = (self.option_persistent_id, self.option_text) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PollOptionAdded": + """See :meth:`telegram.TelegramObject.de_json`.""" + from telegram._message import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 + MaybeInaccessibleMessage, + ) + + data = cls._parse_data(data) + + data["poll_message"] = de_json_optional( + data.get("poll_message"), MaybeInaccessibleMessage, bot + ) + data["option_text_entities"] = de_list_optional( + data.get("option_text_entities"), MessageEntity, bot + ) + + return super().de_json(data=data, bot=bot) + + def parse_option_text_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`option_text` + from a given :class:`telegram.MessageEntity` of :attr:`option_text_entities`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``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:`option_text_entities`. + + Returns: + :obj:`str`: The text of the given entity. + """ + return parse_message_entity(self.option_text, entity) + + def parse_option_text_entities( + self, types: list[str] | None = None + ) -> dict[MessageEntity, str]: + """ + Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. + It contains entities from this polls option 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:`option_text_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_entity` for more info. + + Args: + types (list[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``type`` attribute of an entity is contained in this list, it will be returned. + Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`. + + Returns: + dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to + the text that belongs to them, calculated based on UTF-16 codepoints. + """ + return parse_message_entities(self.option_text, self.option_text_entities, types) + + +class PollOptionDeleted(TelegramObject): + """ + Describes a service message about an option deleted from a poll. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`option_persistent_id`, :attr:`option_text` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + option_persistent_id (:obj:`str`): Unique identifier of the deleted option. + option_text (:obj:`str`): Option text. + poll_message (:class:`telegram.MaybeInaccessibleMessage`, optional): Message + containing the poll to which the option was deleted, if known. + Note that the Message object in this field will not contain the + :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + option_text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special + entities that appear in the option_text. + + Attributes: + option_persistent_id (:obj:`str`): Unique identifier of the deleted option. + option_text (:obj:`str`): Option text. + poll_message (:class:`telegram.MaybeInaccessibleMessage`): Optional. Message + containing the poll to which the option was deleted, if known. + Note that the Message object in this field will not contain the + :attr:`~telegram.Message.reply_to_message` field even if it itself is a reply. + option_text_entities (tuple[:class:`telegram.MessageEntity`]): Optional. Special + entities that appear in the option_text. + """ + + __slots__ = ("option_persistent_id", "option_text", "option_text_entities", "poll_message") + + def __init__( + self, + option_persistent_id: str, + option_text: str, + poll_message: "MaybeInaccessibleMessage | None" = None, + option_text_entities: Sequence[MessageEntity] | None = None, + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.option_persistent_id: str = option_persistent_id + self.option_text: str = option_text + self.poll_message: MaybeInaccessibleMessage | None = poll_message + + self.option_text_entities: tuple[MessageEntity, ...] = parse_sequence_arg( + option_text_entities + ) + + self._id_attrs = (self.option_persistent_id, self.option_text) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PollOptionDeleted": + """See :meth:`telegram.TelegramObject.de_json`.""" + from telegram._message import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 + MaybeInaccessibleMessage, + ) + + data = cls._parse_data(data) + + data["poll_message"] = de_json_optional( + data.get("poll_message"), MaybeInaccessibleMessage, bot + ) + data["option_text_entities"] = de_list_optional( + data.get("option_text_entities"), MessageEntity, bot + ) + + return super().de_json(data=data, bot=bot) + + def parse_option_text_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`option_text` + from a given :class:`telegram.MessageEntity` of :attr:`option_text_entities`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``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:`option_text_entities`. + + Returns: + :obj:`str`: The text of the given entity. + """ + return parse_message_entity(self.option_text, entity) + + def parse_option_text_entities( + self, types: list[str] | None = None + ) -> dict[MessageEntity, str]: + """ + Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. + It contains entities from this polls option 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:`option_text_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_entity` for more info. + + Args: + types (list[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``type`` attribute of an entity is contained in this list, it will be returned. + Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`. + + Returns: + dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to + the text that belongs to them, calculated based on UTF-16 codepoints. + """ + return parse_message_entities(self.option_text, self.option_text_entities, types) + + class Poll(TelegramObject): """ This object contains information about a poll. @@ -341,6 +640,9 @@ class Poll(TelegramObject): correct_option_id (:obj:`int`, optional): A zero based identifier of the correct answer option. Available only for closed polls in the quiz mode, which were sent (not forwarded), by the bot or to a private chat with the bot. + + .. deprecated:: NEXT.VERSION + Use :paramref:`correct_option_ids` instead. explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters. @@ -368,6 +670,23 @@ class Poll(TelegramObject): in poll questions. .. versionadded:: 21.2 + allows_revoting (:obj:`bool`, optional): :obj:`True`, if the poll allows to + change the chosenanswer options. + + .. versionadded:: NEXT.VERSION + correct_option_ids (Sequence[:class:`int`], optional): Array of 0-based identifiers of + the correct answer options. Available only for polls in quiz mode which are closed or + were sent (not forwarded) by the bot or to the private chat with the bot. + + .. versionadded:: NEXT.VERSION + description (:obj:`str`, optional): Description of the poll; + for polls inside the :class:`~telegram.Message` object only. + + .. versionadded:: NEXT.VERSION + description_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special + entities like usernames, URLs, bot commands, etc. that appear in the description + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`str`): Unique poll identifier. @@ -385,6 +704,9 @@ class Poll(TelegramObject): correct_option_id (:obj:`int`): Optional. A zero based identifier of the correct answer option. Available only for closed polls in the quiz mode, which were sent (not forwarded), by the bot or to a private chat with the bot. + + .. deprecated:: NEXT.VERSION + Use :attr:`correct_option_ids` instead. explanation (:obj:`str`): Optional. Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters. @@ -413,14 +735,34 @@ class Poll(TelegramObject): This list is empty if the question does not contain entities. .. versionadded:: 21.2 + allows_revoting (:obj:`bool`): Optional. :obj:`True`, if the poll + allows to change the chosenanswer options + + .. versionadded:: NEXT.VERSION + correct_option_ids (tuple[:class:`int`]): Array of 0-based identifiers of the + correct answer options. Available only for polls in quiz mode which are closed or were + sent (not forwarded) by the bot or to the private chat with the bot. + + .. versionadded:: NEXT.VERSION + description (:obj:`str`): Optional. Description of the poll; + for polls inside the Message object only + + .. versionadded:: NEXT.VERSION + description_entities (tuple[:class:`telegram.MessageEntity`]): Special + entities like usernames, URLs, bot commands, etc. that appear in the description + + .. versionadded:: NEXT.VERSION """ __slots__ = ( "_open_period", "allows_multiple_answers", + "allows_revoting", "close_date", - "correct_option_id", + "correct_option_ids", + "description", + "description_entities", "explanation", "explanation_entities", "id", @@ -443,12 +785,20 @@ def __init__( is_anonymous: bool, type: str, # pylint: disable=redefined-builtin allows_multiple_answers: bool, + # tags: deprecated NEXT.VERSION + # Removed in bot api 9.6: correct_option_id: int | None = None, explanation: str | None = None, explanation_entities: Sequence[MessageEntity] | None = None, open_period: TimePeriod | None = None, close_date: dtm.datetime | None = None, question_entities: Sequence[MessageEntity] | None = None, + # tags: required in NEXT.VERSION + # temporarily optional to avoid breaking changes + allows_revoting: bool | None = None, + correct_option_ids: Sequence[int] | None = None, + description: str | None = None, + description_entities: Sequence[MessageEntity] | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -461,7 +811,26 @@ def __init__( self.is_anonymous: bool = is_anonymous self.type: str = enum.get_member(constants.PollType, type, type) self.allows_multiple_answers: bool = allows_multiple_answers - self.correct_option_id: int | None = correct_option_id + self.allows_revoting: bool | None = allows_revoting + + # tag: deprecated NEXT.VERSION + if correct_option_id is not None: + warn( + PTBDeprecationWarning( + "NEXT.VERSION", + "The parameter `correct_option_id` is deprecated. " + "Use `correct_option_ids` instead.", + ), + stacklevel=2, + ) + if correct_option_ids is None: + correct_option_ids = [correct_option_id] + + self.correct_option_ids: tuple[int, ...] = parse_sequence_arg(correct_option_ids) + self.description: str | None = description + self.description_entities: tuple[MessageEntity, ...] = parse_sequence_arg( + description_entities + ) self.explanation: str | None = explanation self.explanation_entities: tuple[MessageEntity, ...] = parse_sequence_arg( explanation_entities @@ -494,6 +863,9 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Poll": data["question_entities"] = de_list_optional( data.get("question_entities"), MessageEntity, bot ) + data["description_entities"] = de_list_optional( + data.get("description_entities"), MessageEntity, bot + ) return super().de_json(data=data, bot=bot) @@ -598,6 +970,82 @@ def parse_question_entities(self, types: list[str] | None = None) -> dict[Messag """ return parse_message_entities(self.question, self.question_entities, types) + def parse_description_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`description` from a given :class:`telegram.MessageEntity` of + :attr:`description_entities`. + + .. versionadded:: NEXT.VERSION + + 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:`description_entities`. + + Returns: + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the poll has no description. + + """ + if not self.description: + raise RuntimeError("This Poll has no 'description'.") + + return parse_message_entity(self.description, entity) + + def parse_description_entities( + self, types: list[str] | None = None + ) -> dict[MessageEntity, str]: + """ + Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. + It contains entities from this polls description filtered by their ``type`` attribute as + the key, and the text that each entity belongs to as the value of the :obj:`dict`. + + .. versionadded:: NEXT.VERSION + + Note: + This method should always be used instead of the :attr:`description_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_description_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 poll has no description. + """ + if not self.description: + raise RuntimeError("This Poll has no 'description'.") + + return parse_message_entities(self.description, self.description_entities, types) + + @property + def correct_option_id(self) -> int | None: + """A zero based identifier of the correct answer + option. Available only for closed polls in the quiz mode, which were sent + (not forwarded), by the bot or to a private chat with the bot. + + .. deprecated:: NEXT.VERSION + Use :attr:`correct_option_ids` instead. + """ + warn( + PTBDeprecationWarning( + "NEXT.VERSION", + "The attribute `correct_option_id` is deprecated. " + "Use `correct_option_ids` instead.", + ), + stacklevel=2, + ) + return self.correct_option_ids[0] if self.correct_option_ids else None + REGULAR: Final[str] = constants.PollType.REGULAR """:const:`telegram.constants.PollType.REGULAR`""" QUIZ: Final[str] = constants.PollType.QUIZ @@ -652,3 +1100,8 @@ def parse_question_entities(self, types: list[str] | None = None) -> dict[Messag .. versionadded:: 20.0 """ + MAX_DESCRIPTION_CHARACTERS: Final[int] = constants.PollLimit.MAX_DESCRIPTION_CHARACTERS + """:const:`telegram.constants.PollLimit.MAX_DESCRIPTION_CHARACTERS` + + .. versionadded:: NEXT.VERSION + """ diff --git a/src/telegram/_preparedkeyboardbutton.py b/src/telegram/_preparedkeyboardbutton.py new file mode 100644 index 00000000000..0a650f9469e --- /dev/null +++ b/src/telegram/_preparedkeyboardbutton.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram PreparedKeyboardButton.""" + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import ( + JSONDict, +) + + +class PreparedKeyboardButton(TelegramObject): + """ + Describes a keyboard button to be used by a user of a Mini App. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + id (:obj:`str`): Unique identifier of the keyboard button. + + Attributes: + id (:obj:`str`): Unique identifier of the keyboard button. + """ + + __slots__ = ("id",) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.id: str = id + self._id_attrs = (self.id,) + self._freeze() diff --git a/src/telegram/_reply.py b/src/telegram/_reply.py index 367d5aad7a0..6f1edfadeb8 100644 --- a/src/telegram/_reply.py +++ b/src/telegram/_reply.py @@ -311,8 +311,8 @@ class TextQuote(TelegramObject): units as specified by the sender. entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities that appear - in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and - custom_emoji entities are kept in quotes. + in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, + custom_emoji, and date_time entities are kept in quotes. is_manual (:obj:`bool`, optional): :obj:`True`, if the quote was chosen manually by the message sender. Otherwise, the quote was added automatically by the server. @@ -322,8 +322,8 @@ class TextQuote(TelegramObject): position (:obj:`int`): Approximate quote position in the original message in UTF-16 code units as specified by the sender. entities (tuple[:class:`telegram.MessageEntity`]): Optional. Special entities that appear - in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and - custom_emoji entities are kept in quotes. + in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, + custom_emoji, and date_time entities are kept in quotes. is_manual (:obj:`bool`): Optional. :obj:`True`, if the quote was chosen manually by the message sender. Otherwise, the quote was added automatically by the server. """ @@ -394,9 +394,9 @@ class ReplyParameters(TelegramObject): used only for replies in the same chat and forum topic. quote (:obj:`str`, optional): Quoted part of the message to be replied to; 0-1024 characters after entities parsing. The quote must be an exact substring of the message - to be replied to, including bold, italic, underline, strikethrough, spoiler, and - custom_emoji entities. The message will fail to send if the quote isn't found in the - original message. + to be replied to, including bold, italic, underline, strikethrough, spoiler, + custom_emoji, and date_time entities. The message will fail to send if the quote isn't + found in the original message. quote_parse_mode (:obj:`str`, optional): Mode for parsing entities in the quote. See :wiki:`formatting options ` for more details. @@ -410,6 +410,10 @@ class ReplyParameters(TelegramObject): replied to. .. versionadded:: 22.4 + poll_option_id (:obj:`str`, optional): Persistent + identifier of the specific poll option to be replied to. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Identifier of the message that will be replied to in the current @@ -422,9 +426,9 @@ class ReplyParameters(TelegramObject): used only for replies in the same chat and forum topic. quote (:obj:`str`): Optional. Quoted part of the message to be replied to; 0-1024 characters after entities parsing. The quote must be an exact substring of the message - to be replied to, including bold, italic, underline, strikethrough, spoiler, and - custom_emoji entities. The message will fail to send if the quote isn't found in the - original message. + to be replied to, including bold, italic, underline, strikethrough, spoiler, + custom_emoji, and date_time entities. The message will fail to send if the quote isn't + found in the original message. quote_parse_mode (:obj:`str`): Optional. Mode for parsing entities in the quote. See :wiki:`formatting options ` for more details. @@ -437,6 +441,10 @@ class ReplyParameters(TelegramObject): replied to. .. versionadded:: 22.4 + poll_option_id (:obj:`str`): Optional. Persistent + identifier of the specific poll option to be replied to. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -444,6 +452,7 @@ class ReplyParameters(TelegramObject): "chat_id", "checklist_task_id", "message_id", + "poll_option_id", "quote", "quote_entities", "quote_parse_mode", @@ -460,6 +469,7 @@ def __init__( quote_entities: Sequence[MessageEntity] | None = None, quote_position: int | None = None, checklist_task_id: int | None = None, + poll_option_id: str | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -473,6 +483,7 @@ def __init__( self.quote_entities: tuple[MessageEntity, ...] | None = parse_sequence_arg(quote_entities) self.quote_position: int | None = quote_position self.checklist_task_id: int | None = checklist_task_id + self.poll_option_id: str | None = poll_option_id self._id_attrs = (self.message_id,) diff --git a/src/telegram/_update.py b/src/telegram/_update.py index 9e018e4cb0f..a8b8668ad17 100644 --- a/src/telegram/_update.py +++ b/src/telegram/_update.py @@ -28,6 +28,7 @@ from telegram._chatmemberupdated import ChatMemberUpdated from telegram._choseninlineresult import ChosenInlineResult from telegram._inline.inlinequery import InlineQuery +from telegram._managedbot import ManagedBotUpdated from telegram._message import Message from telegram._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated from telegram._paidmedia import PaidMediaPurchased @@ -163,6 +164,11 @@ class Update(TelegramObject): .. versionadded:: 21.6 + managed_bot (:class:`telegram.ManagedBotUpdated`, optional): A new bot was created to be + managed by the bot, or token or owner of a managed bot was changed. + + .. versionadded:: NEXT.VERSION + Attributes: update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a @@ -275,6 +281,10 @@ class Update(TelegramObject): paid media with a non-empty payload sent by the bot in a non-channel chat. .. versionadded:: 21.6 + managed_bot (:class:`telegram.ManagedBotUpdated`): Optional. A new bot was created to be + managed by the bot, or token or owner of a managed bot was changed. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( @@ -295,6 +305,7 @@ class Update(TelegramObject): "edited_channel_post", "edited_message", "inline_query", + "managed_bot", "message", "message_reaction", "message_reaction_count", @@ -403,6 +414,12 @@ class Update(TelegramObject): .. versionadded:: 21.6 """ + MANAGED_BOT: Final[str] = constants.UpdateType.MANAGED_BOT + """:const:`telegram.constants.UpdateType.MANAGED_BOT` + + .. versionadded:: NEXT.VERSION + """ + ALL_TYPES: Final[list[str]] = list(constants.UpdateType) """list[:obj:`str`]: A list of all available update types. @@ -434,6 +451,7 @@ def __init__( edited_business_message: Message | None = None, deleted_business_messages: BusinessMessagesDeleted | None = None, purchased_paid_media: PaidMediaPurchased | None = None, + managed_bot: ManagedBotUpdated | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -464,6 +482,7 @@ def __init__( self.edited_business_message: Message | None = edited_business_message self.deleted_business_messages: BusinessMessagesDeleted | None = deleted_business_messages self.purchased_paid_media: PaidMediaPurchased | None = purchased_paid_media + self.managed_bot: ManagedBotUpdated | None = managed_bot self._effective_user: User | None = None self._effective_sender: User | Chat | None = None @@ -498,6 +517,9 @@ def effective_user(self) -> "User | None": .. versionchanged:: 21.6 This property now also considers :attr:`purchased_paid_media`. + .. versionchanged:: NEXT.VERSION + This property now also considers :attr:`managed_bot`. + Example: * If :attr:`message` is present, this will give :attr:`telegram.Message.from_user`. @@ -557,6 +579,9 @@ def effective_user(self) -> "User | None": elif self.purchased_paid_media: user = self.purchased_paid_media.from_user + elif self.managed_bot: + user = self.managed_bot.user + self._effective_user = user return user @@ -627,8 +652,8 @@ def effective_chat(self) -> "Chat | None": This is the case, if :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, - :attr:`poll_answer`, :attr:`business_connection`, or :attr:`purchased_paid_media` - is present. + :attr:`poll_answer`, :attr:`business_connection`, :attr:`purchased_paid_media`, + or :attr:`managed_bot` is present. .. versionchanged:: 21.1 This property now also considers :attr:`business_message`, @@ -807,5 +832,6 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Update": data["purchased_paid_media"] = de_json_optional( data.get("purchased_paid_media"), PaidMediaPurchased, bot ) + data["managed_bot"] = de_json_optional(data.get("managed_bot"), ManagedBotUpdated, bot) return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_user.py b/src/telegram/_user.py index 89dae50d147..821f230adb6 100644 --- a/src/telegram/_user.py +++ b/src/telegram/_user.py @@ -29,6 +29,7 @@ from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ( CorrectOptionID, + CorrectOptionIds, JSONDict, ODVInput, TimePeriod, @@ -124,6 +125,10 @@ class User(TelegramObject): :meth:`telegram.Bot.get_me`. .. versionadded:: 22.7 + can_manage_bots (:obj:`bool`, optional): :obj:`True`, if other bots can be created to be + controlled by the bot. Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this user or bot. @@ -164,6 +169,10 @@ class User(TelegramObject): :meth:`telegram.Bot.get_me`. .. versionadded:: 22.7 + can_manage_bots (:obj:`bool`): Optional. :obj:`True`, if other bots can be created to be + controlled by the bot. Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION .. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id` coincides with the :attr:`Chat.id` of the private chat with the user. This has been the @@ -175,6 +184,7 @@ class User(TelegramObject): "allows_users_to_create_topics", "can_connect_to_business", "can_join_groups", + "can_manage_bots", "can_read_all_group_messages", "first_name", "has_main_web_app", @@ -205,6 +215,7 @@ def __init__( has_main_web_app: bool | None = None, has_topics_enabled: bool | None = None, allows_users_to_create_topics: bool | None = None, + can_manage_bots: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -226,6 +237,7 @@ def __init__( self.has_main_web_app: bool | None = has_main_web_app self.has_topics_enabled: bool | None = has_topics_enabled self.allows_users_to_create_topics: bool | None = allows_users_to_create_topics + self.can_manage_bots: bool | None = can_manage_bots self._id_attrs = (self.id,) @@ -1730,6 +1742,14 @@ async def send_poll( question_entities: Sequence["MessageEntity"] | None = None, message_effect_id: str | None = None, allow_paid_broadcast: bool | None = None, + shuffle_options: bool | None = None, + allows_revoting: bool | None = None, + correct_option_ids: CorrectOptionIds | None = None, + allow_adding_options: bool | None = None, + hide_results_until_closes: bool | None = None, + description: str | None = None, + description_parse_mode: str | None = None, + description_entities: Sequence["MessageEntity"] | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1760,6 +1780,9 @@ async def send_poll( type=type, # pylint=pylint, allows_multiple_answers=allows_multiple_answers, correct_option_id=correct_option_id, + allows_revoting=allows_revoting, + shuffle_options=shuffle_options, + correct_option_ids=correct_option_ids, is_closed=is_closed, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, @@ -1769,6 +1792,8 @@ async def send_poll( write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, + message_effect_id=message_effect_id, + allow_paid_broadcast=allow_paid_broadcast, explanation=explanation, explanation_parse_mode=explanation_parse_mode, open_period=open_period, @@ -1781,8 +1806,11 @@ async def send_poll( business_connection_id=business_connection_id, question_parse_mode=question_parse_mode, question_entities=question_entities, - message_effect_id=message_effect_id, - allow_paid_broadcast=allow_paid_broadcast, + description=description, + description_parse_mode=description_parse_mode, + description_entities=description_entities, + hide_results_until_closes=hide_results_until_closes, + allow_adding_options=allow_adding_options, ) async def send_gift( @@ -2712,3 +2740,34 @@ async def set_chat_member_tag( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) + + async def replace_token( + 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: JSONDict | None = None, + ) -> str: + """ + Shortcut for:: + + await bot.replace_managed_bot_token(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.replace_managed_bot_token`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`str` is returned. + """ + return await self.get_bot().replace_managed_bot_token( + user_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) diff --git a/src/telegram/_utils/types.py b/src/telegram/_utils/types.py index 1b29e2e8de6..0684219ad01 100644 --- a/src/telegram/_utils/types.py +++ b/src/telegram/_utils/types.py @@ -25,7 +25,7 @@ """ import datetime as dtm -from collections.abc import Callable, Collection +from collections.abc import Callable, Collection, Sequence from pathlib import Path from typing import IO, TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar, Union @@ -93,6 +93,11 @@ CorrectOptionID: TypeAlias = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # pylint: disable=invalid-name +CorrectOptionIds: TypeAlias = Sequence[Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]] +""" +.. versionadded:: NEXT.VERSION +""" + MarkdownVersion: TypeAlias = Literal[1, 2] SocketOpt: TypeAlias = ( diff --git a/src/telegram/constants.py b/src/telegram/constants.py index 3ae66ff0b4b..144e6763892 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -181,7 +181,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=5) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=6) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -2347,6 +2347,8 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.left_chat_member`.""" LOCATION = "location" """:obj:`str`: Messages with :attr:`telegram.Message.location`.""" + MANAGED_BOT_CREATED = "managed_bot_created" + """:obj:`str`: Messages with :attr:`telegram.Message.managed_bot_created`.""" MESSAGE_AUTO_DELETE_TIMER_CHANGED = "message_auto_delete_timer_changed" """:obj:`str`: Messages with :attr:`telegram.Message.message_auto_delete_timer_changed`.""" MIGRATE_TO_CHAT_ID = "migrate_to_chat_id" @@ -2367,6 +2369,16 @@ class MessageType(StringEnum): .. versionadded:: v22.2 """ + POLL_OPTION_ADDED = "poll_option_added" + """:obj:`str`: Messages with :attr:`telegram.Message.poll_option_added`. + + .. versionadded:: NEXT.VERSION + """ + POLL_OPTION_DELETED = "poll_option_deleted" + """:obj:`str`: Messages with :attr:`telegram.Message.poll_option_deleted`. + + .. versionadded:: NEXT.VERSION + """ SUGGESTED_POST_APPROVAL_FAILED = "suggested_post_approval_failed" """:obj:`str`: Messages with :attr:`telegram.Message.suggested_post_approval_failed`. @@ -3439,11 +3451,20 @@ class PollLimit(IntEnum): Also used in the :paramref:`~telegram.Bot.send_poll.close_date` parameter of :meth:`telegram.Bot.send_poll`. """ - MAX_OPEN_PERIOD = 600 + MAX_OPEN_PERIOD = 2628000 """:obj:`int`: Maximum value allowed for the :paramref:`~telegram.Bot.send_poll.open_period` parameter of :meth:`telegram.Bot.send_poll`. Also used in the :paramref:`~telegram.Bot.send_poll.close_date` parameter of :meth:`telegram.Bot.send_poll`. + + .. versionchanged:: NEXT.VERSION + Changed from ``600`` to ``2628000`` since Bot API 9.6. + """ + MAX_DESCRIPTION_CHARACTERS = 1024 + """:obj:`int`: Maximum value allowed for the + :paramref:`~telegram.Bot.send_poll.description` parameter of :meth:`telegram.Bot.send_poll`. + + .. versionadded:: NEXT.VERSION """ @@ -3594,6 +3615,11 @@ class UpdateType(StringEnum): .. versionadded:: 21.6 """ + MANAGED_BOT = "managed_bot" + """:obj:`str`: Updates with :attr:`telegram.Update.managed_bot`. + + .. versionadded:: NEXT.VERSION + """ class InvoiceLimit(IntEnum): diff --git a/src/telegram/ext/__init__.py b/src/telegram/ext/__init__.py index 4c7fa9f5a8c..d286722b594 100644 --- a/src/telegram/ext/__init__.py +++ b/src/telegram/ext/__init__.py @@ -46,6 +46,7 @@ "InvalidCallbackData", "Job", "JobQueue", + "ManagedBotUpdatedHandler", "MessageHandler", "MessageReactionHandler", "PaidMediaPurchasedHandler", @@ -88,6 +89,7 @@ from ._handlers.commandhandler import CommandHandler from ._handlers.conversationhandler import ConversationHandler from ._handlers.inlinequeryhandler import InlineQueryHandler +from ._handlers.managedbotupdatedhandler import ManagedBotUpdatedHandler from ._handlers.messagehandler import MessageHandler from ._handlers.messagereactionhandler import MessageReactionHandler from ._handlers.paidmediapurchasedhandler import PaidMediaPurchasedHandler diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index c09b1a5f6d4..931115f7beb 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -63,6 +63,7 @@ InputPaidMedia, InputPollOption, InputProfilePhoto, + KeyboardButton, LinkPreviewOptions, MaskPosition, MenuButton, @@ -72,6 +73,7 @@ PhotoSize, Poll, PreparedInlineMessage, + PreparedKeyboardButton, ReactionType, ReplyParameters, SentWebAppMessage, @@ -98,6 +100,7 @@ from telegram._utils.types import ( BaseUrl, CorrectOptionID, + CorrectOptionIds, FileInput, JSONDict, ODVInput, @@ -3254,6 +3257,14 @@ async def send_poll( question_entities: Sequence["MessageEntity"] | None = None, message_effect_id: str | None = None, allow_paid_broadcast: bool | None = None, + allows_revoting: bool | None = None, + allow_adding_options: bool | None = None, + hide_results_until_closes: bool | None = None, + correct_option_ids: CorrectOptionIds | None = None, + description: str | None = None, + description_parse_mode: str | None = None, + description_entities: Sequence["MessageEntity"] | None = None, + shuffle_options: bool | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3295,6 +3306,14 @@ async def send_poll( question_entities=question_entities, message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + allows_revoting=allows_revoting, + shuffle_options=shuffle_options, + correct_option_ids=correct_option_ids, + description=description, + description_parse_mode=description_parse_mode, + description_entities=description_entities, + hide_results_until_closes=hide_results_until_closes, + allow_adding_options=allow_adding_options, ) async def send_sticker( @@ -5511,6 +5530,69 @@ async def set_chat_member_tag( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_managed_bot_token( + self, + user_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: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> str: + return await super().get_managed_bot_token( + user_id=user_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 replace_managed_bot_token( + self, + user_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: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> str: + return await super().replace_managed_bot_token( + user_id=user_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 save_prepared_keyboard_button( + self, + user_id: int, + button: KeyboardButton, + *, + 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: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> PreparedKeyboardButton: + + return await super().save_prepared_keyboard_button( + user_id=user_id, + button=button, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message @@ -5677,3 +5759,6 @@ async def set_chat_member_tag( removeMyProfilePhoto = remove_my_profile_photo getUserProfileAudios = get_user_profile_audios setChatMemberTag = set_chat_member_tag + getManagedBotToken = get_managed_bot_token + replaceManagedBotToken = replace_managed_bot_token + savePreparedKeyboardButton = save_prepared_keyboard_button diff --git a/src/telegram/ext/_handlers/managedbotupdatedhandler.py b/src/telegram/ext/_handlers/managedbotupdatedhandler.py new file mode 100644 index 00000000000..2715f06c21e --- /dev/null +++ b/src/telegram/ext/_handlers/managedbotupdatedhandler.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# 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 the ManagedBotUpdatedHandler class.""" + +from typing import TypeVar + +from telegram import Update +from telegram._utils.defaultvalue import DEFAULT_TRUE +from telegram._utils.types import SCT, DVType +from telegram.ext._handlers.basehandler import BaseHandler +from telegram.ext._utils._update_parsing import parse_chat_id, parse_username +from telegram.ext._utils.types import CCT, HandlerCallback + +RT = TypeVar("RT") + + +class ManagedBotUpdatedHandler(BaseHandler[Update, CCT, RT]): + """Handler class to handle + :attr:`updated Telegram Managed Bots `. + + .. versionadded:: NEXT.VERSION + + Args: + callback (:term:`coroutine function`): The callback function for this handler. Will be + called when :meth:`check_update` has determined that an update should be processed by + this handler. Callback signature:: + + async def callback(update: Update, context: CallbackContext) + user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only + those which are from the specified user ID(s). The user ID(s) can be either the + creator of the managed bot or the bot itself. + + username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only + those which are from the specified username(s). The username(s) can be either the + creator of the managed bot or the bot itself. + + block (:obj:`bool`, optional): Determines whether the return value of the callback should + be awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. + + .. seealso:: :wiki:`Concurrency` + Attributes: + callback (:term:`coroutine function`): The callback function for this handler. + block (:obj:`bool`): Determines whether the return value of the callback should be + awaited before processing the next handler in + :meth:`telegram.ext.Application.process_update`. + """ + + __slots__ = ( + "_user_ids", + "_usernames", + ) + + def __init__( + self: "ManagedBotUpdatedHandler[CCT, RT]", + callback: HandlerCallback[Update, CCT, RT], + user_id: SCT[int] | None = None, + username: SCT[str] | None = None, + block: DVType[bool] = DEFAULT_TRUE, + ): + super().__init__(callback, block=block) + + self._user_ids = parse_chat_id(user_id) + self._usernames = parse_username(username) + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handler's :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + if isinstance(update, Update) and update.managed_bot: + if not self._user_ids and not self._usernames: + return True + if ( + update.managed_bot.user.id in self._user_ids + or update.managed_bot.bot.id in self._user_ids + ): + return True + return ( + update.managed_bot.user.username in self._usernames + or update.managed_bot.bot.username in self._usernames + ) + return False diff --git a/src/telegram/ext/_utils/trackingdict.py b/src/telegram/ext/_utils/trackingdict.py index bf7af4d5dab..04685efca23 100644 --- a/src/telegram/ext/_utils/trackingdict.py +++ b/src/telegram/ext/_utils/trackingdict.py @@ -109,7 +109,7 @@ def pop( # type: ignore[override] self.__track_write(key) if isinstance(default, DefaultValue): return super().pop(key) - return super().pop(key, default=default) + return super().pop(key, default) def clear(self) -> None: self.__track_write(set(super().keys())) diff --git a/src/telegram/ext/filters.py b/src/telegram/ext/filters.py index d099b0c5313..d8fa167e7b0 100644 --- a/src/telegram/ext/filters.py +++ b/src/telegram/ext/filters.py @@ -430,7 +430,7 @@ def __init__( ): self.data_filter = True self.or_filter = or_filter - if self.or_filter and not isinstance(self.and_filter, bool) and self.or_filter.data_filter: + if self.or_filter and not isinstance(self.or_filter, bool) and self.or_filter.data_filter: self.data_filter = True @staticmethod @@ -1984,6 +1984,7 @@ def filter(self, update: Update) -> bool: or StatusUpdate.GIVEAWAY_COMPLETED.check_update(update) or StatusUpdate.GIVEAWAY_CREATED.check_update(update) or StatusUpdate.LEFT_CHAT_MEMBER.check_update(update) + or StatusUpdate.MANAGED_BOT_CREATED.check_update(update) or StatusUpdate.MESSAGE_AUTO_DELETE_TIMER_CHANGED.check_update(update) or StatusUpdate.MIGRATE.check_update(update) or StatusUpdate.NEW_CHAT_MEMBERS.check_update(update) @@ -1991,6 +1992,8 @@ def filter(self, update: Update) -> bool: 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.POLL_OPTION_ADDED.check_update(update) + or StatusUpdate.POLL_OPTION_DELETED.check_update(update) or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) or StatusUpdate.REFUNDED_PAYMENT.check_update(update) or StatusUpdate.SUGGESTED_POST_APPROVAL_FAILED.check_update(update) @@ -2259,6 +2262,15 @@ def filter(self, message: Message) -> bool: LEFT_CHAT_MEMBER = _LeftChatMember(name="filters.StatusUpdate.LEFT_CHAT_MEMBER") """Messages that contain :attr:`telegram.Message.left_chat_member`.""" + class _ManagedBotCreated(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.managed_bot_created) + + MANAGED_BOT_CREATED = _ManagedBotCreated(name="filters.StatusUpdate.MANAGED_BOT_CREATED") + """Messages that contain :attr:`telegram.Message.managed_bot_created`.""" + class _MessageAutoDeleteTimerChanged(MessageFilter): __slots__ = () @@ -2333,6 +2345,28 @@ def filter(self, message: Message) -> bool: PINNED_MESSAGE = _PinnedMessage(name="filters.StatusUpdate.PINNED_MESSAGE") """Messages that contain :attr:`telegram.Message.pinned_message`.""" + class _PollOptionAdded(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.poll_option_added) + + POLL_OPTION_ADDED = _PollOptionAdded(name="filters.StatusUpdate.POLL_OPTION_ADDED") + """Messages that contain :attr:`telegram.Message.poll_option_added`. + .. versionadded:: NEXT.VERSION + """ + + class _PollOptionDeleted(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.poll_option_deleted) + + POLL_OPTION_DELETED = _PollOptionDeleted(name="filters.StatusUpdate.POLL_OPTION_DELETED") + """Messages that contain :attr:`telegram.Message.poll_option_deleted`. + .. versionadded:: NEXT.VERSION + """ + class _ProximityAlertTriggered(MessageFilter): __slots__ = () diff --git a/tests/_files/test_audio.py b/tests/_files/test_audio.py index 0bd9b2e6fd8..dd832966fde 100644 --- a/tests/_files/test_audio.py +++ b/tests/_files/test_audio.py @@ -263,7 +263,7 @@ async def test_send_all_args(self, bot, chat_id, audio_file, thumb_file, duratio assert message.audio.file_name == self.file_name assert message.audio.mime_type == self.mime_type assert message.audio.file_size == self.file_size - assert message.audio.thumbnail.file_size == self.thumb_file_size + assert message.audio.thumbnail.file_size in [self.thumb_file_size, 1395] assert message.audio.thumbnail.width == self.thumb_width assert message.audio.thumbnail.height == self.thumb_height assert message.has_protected_content diff --git a/tests/_files/test_photo.py b/tests/_files/test_photo.py index 83ba28ea701..93e0ac40ea1 100644 --- a/tests/_files/test_photo.py +++ b/tests/_files/test_photo.py @@ -75,7 +75,7 @@ def test_expected_values(self, photo, thumb): assert thumb.height == 90 # File sizes don't seem to be consistent, so we use the values that we have observed # so far - assert thumb.file_size in [1475, 1477] + assert thumb.file_size in [1474, 1475, 1477] def test_de_json(self, offline_bot, photo): json_dict = { diff --git a/tests/_files/test_video.py b/tests/_files/test_video.py index 8be6d9df2cf..4a34e126668 100644 --- a/tests/_files/test_video.py +++ b/tests/_files/test_video.py @@ -319,7 +319,7 @@ async def test_send_all_args( assert message.caption == self.caption.replace("*", "") - assert message.video.thumbnail.file_size == self.thumb_file_size + assert message.video.thumbnail.file_size in [self.thumb_file_size, 1769] assert message.video.thumbnail.width == self.thumb_width assert message.video.thumbnail.height == self.thumb_height diff --git a/tests/_files/test_videonote.py b/tests/_files/test_videonote.py index 3af79baf497..c2993021883 100644 --- a/tests/_files/test_videonote.py +++ b/tests/_files/test_videonote.py @@ -263,7 +263,7 @@ async def test_send_all_args( assert message.video_note.duration == video_note.duration assert message.video_note.file_size == video_note.file_size - assert message.video_note.thumbnail.file_size == self.thumb_file_size + assert message.video_note.thumbnail.file_size in [self.thumb_file_size, 11631] assert message.video_note.thumbnail.width == self.thumb_width assert message.video_note.thumbnail.height == self.thumb_height assert message.has_protected_content diff --git a/tests/auxil/dummy_objects.py b/tests/auxil/dummy_objects.py index 611e19a67f0..c0ff0ce0cf5 100644 --- a/tests/auxil/dummy_objects.py +++ b/tests/auxil/dummy_objects.py @@ -29,6 +29,7 @@ Poll, PollOption, PreparedInlineMessage, + PreparedKeyboardButton, SentWebAppMessage, StarAmount, StarTransaction, @@ -133,6 +134,7 @@ type="dummy_type", allows_multiple_answers=False, ), + "PreparedKeyboardButton": PreparedKeyboardButton(id=1234), "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), diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index a045d2a6dd7..e68fb0dbc09 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -276,6 +276,18 @@ def test_filters_merged_with_regex(self, update): result = (filters.COMMAND | filters.Regex(r"linked param")).check_update(update) assert result is True + def test_merged_filter_or_data_filter(self, update): + sre_type = type(re.match("", "")) + update.message.text = "deep-linked param" + update.message.entities = [] + # COMMAND doesn't match; or_filter (Regex, a data filter) should return match data + result = (filters.COMMAND | filters.Regex(r"linked param")).check_update(update) + assert result + assert isinstance(result, dict) + matches = result["matches"] + assert isinstance(matches, list) + assert all(type(res) is sre_type for res in matches) + def test_regex_complex_merges(self, update, message_origin_user): sre_type = type(re.match("", "")) update.message.text = "test it out" @@ -1171,6 +1183,21 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.CHAT_OWNER_LEFT.check_update(update) update.message.chat_owner_left = None + update.message.poll_option_added = "poll_option_added" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.POLL_OPTION_ADDED.check_update(update) + update.message.poll_option_added = None + + update.message.poll_option_deleted = "poll_option_deleted" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.POLL_OPTION_DELETED.check_update(update) + update.message.poll_option_deleted = None + + update.message.managed_bot_created = "test" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.MANAGED_BOT_CREATED.check_update(update) + update.message.managed_bot_created = None + def test_filters_forwarded(self, update): assert filters.FORWARDED.check_update(update) update.message.forward_origin = MessageOriginHiddenUser(dtm.datetime.utcnow(), 1) diff --git a/tests/ext/test_managedbotupdatedhandler.py b/tests/ext/test_managedbotupdatedhandler.py new file mode 100644 index 00000000000..9fe5781d1d2 --- /dev/null +++ b/tests/ext/test_managedbotupdatedhandler.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# 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 asyncio +import datetime as dtm + +import pytest + +from telegram import ( + Bot, + CallbackQuery, + Chat, + ChosenInlineResult, + ManagedBotUpdated, + Message, + PreCheckoutQuery, + ShippingQuery, + Update, + User, +) +from telegram._utils.datetime import UTC +from telegram.ext import CallbackContext, JobQueue, ManagedBotUpdatedHandler +from tests.auxil.slots import mro_slots + +message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") + +params = [ + {"message": message}, + {"edited_message": message}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, + {"channel_post": message}, + {"edited_channel_post": message}, + {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, + {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, + {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, +] + +ids = ( + "message", + "edited_message", + "callback_query", + "channel_post", + "edited_channel_post", + "chosen_inline_result", + "shipping_query", + "pre_checkout_query", + "callback_query_without_message", +) + + +@pytest.fixture(scope="class", params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope="class") +def time(): + return dtm.datetime.now(tz=UTC) + + +@pytest.fixture(scope="class") +def managed_bot_updated(bot): + bmd = ManagedBotUpdated( + user=User(1, "", False, username="user_a"), + bot=User(2, "", True, username="test_bot"), + ) + bmd.set_bot(bot) + return bmd + + +@pytest.fixture +def managed_bot_updated_update(bot, managed_bot_updated): + return Update(0, managed_bot=managed_bot_updated) + + +class TestManagedBotUpdatedHandler: + test_flag = False + + def test_slot_behaviour(self): + action = ManagedBotUpdatedHandler(self.callback) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + @pytest.fixture(autouse=True) + def _reset(self): + self.test_flag = False + + async def callback(self, update, context): + self.test_flag = ( + isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, asyncio.Queue) + and isinstance(context.job_queue, JobQueue) + and isinstance(context.user_data, dict) + and isinstance(context.bot_data, dict) + and isinstance( + update.managed_bot, + ManagedBotUpdated, + ) + ) + + def test_with_user_id(self, managed_bot_updated_update): + handler = ManagedBotUpdatedHandler(self.callback, user_id=1) + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, user_id=[1]) + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, user_id=2) + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, user_id=2, username="@user_a") + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, user_id=[1, 2]) + assert handler.check_update(managed_bot_updated_update) + + handler = ManagedBotUpdatedHandler(self.callback, user_id=3) + assert not handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, user_id=[3]) + assert not handler.check_update(managed_bot_updated_update) + + def test_with_username(self, managed_bot_updated_update): + handler = ManagedBotUpdatedHandler(self.callback, username="user_a") + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, username="@user_a") + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, username=["user_a"]) + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, username=["@user_a"]) + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, username=["test_bot"]) + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, username=["@test_bot"]) + assert handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, user_id=[2], username="@user_b") + assert handler.check_update(managed_bot_updated_update) + + handler = ManagedBotUpdatedHandler(self.callback, username="user_b") + assert not handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, username="@user_b") + assert not handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, username=["user_b"]) + assert not handler.check_update(managed_bot_updated_update) + handler = ManagedBotUpdatedHandler(self.callback, username=["@user_b"]) + assert not handler.check_update(managed_bot_updated_update) + + managed_bot_updated_update.managed_bot.user._unfreeze() + managed_bot_updated_update.managed_bot.user.username = None + assert not handler.check_update(managed_bot_updated_update) + + def test_other_update_types(self, false_update): + handler = ManagedBotUpdatedHandler(self.callback) + assert not handler.check_update(false_update) + assert not handler.check_update(True) + + async def test_context(self, app, managed_bot_updated_update): + handler = ManagedBotUpdatedHandler(callback=self.callback) + app.add_handler(handler) + + async with app: + await app.process_update(managed_bot_updated_update) + assert self.test_flag diff --git a/tests/test_bot.py b/tests/test_bot.py index 1585c581aa1..2fd77550ecb 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -58,6 +58,8 @@ InputPollOption, InputProfilePhotoStatic, InputTextMessageContent, + KeyboardButton, + KeyboardButtonRequestManagedBot, LabeledPrice, LinkPreviewOptions, MenuButton, @@ -70,6 +72,7 @@ Poll, PollOption, PreparedInlineMessage, + PreparedKeyboardButton, ReactionTypeCustomEmoji, ReactionTypeEmoji, ReplyParameters, @@ -99,7 +102,7 @@ from telegram.ext import ExtBot, InvalidCallbackData from telegram.helpers import escape_markdown from telegram.request import BaseRequest, HTTPXRequest, RequestData -from telegram.warnings import PTBUserWarning +from telegram.warnings import PTBDeprecationWarning, PTBUserWarning from tests.auxil.bot_method_checks import check_defaults_handling from tests.auxil.ci_bots import FALLBACKS from tests.auxil.envvars import GITHUB_ACTIONS @@ -2847,6 +2850,76 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): await offline_bot.set_chat_member_tag(1234, 5678, "This is a tag") + async def test_send_poll_warn_correct_option_id(self, offline_bot, monkeypatch, recwarn): + async def make_first_assert(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("correct_option_ids") == [1] + assert request_data.parameters.get("correct_option_id") is None + return make_message("dummy reply").to_dict() + + async def make_second_assert(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("correct_option_ids") == [1, 2] + assert request_data.parameters.get("correct_option_id") is None + return make_message("dummy reply").to_dict() + + monkeypatch.setattr(offline_bot.request, "post", make_first_assert) + + await offline_bot.send_poll( + 1, + question="question", + options=["option1", "option2"], + correct_option_id=1, + ) + + w = recwarn.pop() + assert issubclass(w.category, PTBDeprecationWarning) + assert "correct_option_id" in str(w.message) + + # Test that correct_option_ids takes priority when both correct_option_id(s) are given + monkeypatch.setattr(offline_bot.request, "post", make_second_assert) + assert await offline_bot.send_poll( + 1, + question="question", + options=["option1", "option2"], + correct_option_id=1, + correct_option_ids=[1, 2], + ) + + # TODO: If we create a managed bot, we could test this for real + async def test_get_managed_bot_token(self, offline_bot, monkeypatch): + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("user_id") == 1234 + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + await offline_bot.get_managed_bot_token(1234) + + async def test_replace_managed_bot_token(self, offline_bot, monkeypatch): + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("user_id") == 1234 + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + await offline_bot.replace_managed_bot_token(1234) + + # Need real user id to test with request. Otherwise returns User_id_invalid + async def test_save_prepared_keyboard_button(self, offline_bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("user_id") == 1234 + assert ( + request_data.parameters.get("button") + == KeyboardButton( + text="this", request_managed_bot=KeyboardButtonRequestManagedBot(1234) + ).to_dict() + ) + return PreparedKeyboardButton(234).to_dict() + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + inst = await offline_bot.save_prepared_keyboard_button( + 1234, + KeyboardButton(text="this", request_managed_bot=KeyboardButtonRequestManagedBot(1234)), + ) + assert isinstance(inst, PreparedKeyboardButton) + class TestBotWithRequest: """ diff --git a/tests/test_constants.py b/tests/test_constants.py index 40fd2d41814..c5e7ff000bf 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -208,6 +208,7 @@ def is_type_attribute(name: str) -> bool: "paid_star_count", "is_paid_post", "reply_to_checklist_task_id", + "reply_to_poll_option_id", } @pytest.mark.parametrize( diff --git a/tests/test_keyboardbutton.py b/tests/test_keyboardbutton.py index 22434be3efb..a664b81cd8e 100644 --- a/tests/test_keyboardbutton.py +++ b/tests/test_keyboardbutton.py @@ -23,6 +23,7 @@ KeyboardButton, KeyboardButtonPollType, KeyboardButtonRequestChat, + KeyboardButtonRequestManagedBot, KeyboardButtonRequestUsers, WebAppInfo, ) @@ -41,6 +42,7 @@ def keyboard_button(): request_users=KeyboardButtonTestBase.request_users, style=KeyboardButtonTestBase.style, icon_custom_emoji_id=KeyboardButtonTestBase.icon_custom_emoji_id, + request_managed_bot=KeyboardButtonTestBase.request_managed_bot, ) @@ -54,6 +56,7 @@ class KeyboardButtonTestBase: request_users = KeyboardButtonRequestUsers(2) style = "primary" icon_custom_emoji_id = "5237829955978547322" + request_managed_bot = KeyboardButtonRequestManagedBot(4, "suggested_name", "username") class TestKeyboardButtonWithoutRequest(KeyboardButtonTestBase): @@ -73,6 +76,7 @@ def test_expected_values(self, keyboard_button): assert keyboard_button.request_users == self.request_users assert keyboard_button.style == self.style assert keyboard_button.icon_custom_emoji_id == self.icon_custom_emoji_id + assert keyboard_button.request_managed_bot == self.request_managed_bot def test_to_dict(self, keyboard_button): keyboard_button_dict = keyboard_button.to_dict() @@ -87,6 +91,10 @@ def test_to_dict(self, keyboard_button): assert keyboard_button_dict["request_users"] == keyboard_button.request_users.to_dict() assert keyboard_button_dict["style"] == keyboard_button.style assert keyboard_button_dict["icon_custom_emoji_id"] == keyboard_button.icon_custom_emoji_id + assert ( + keyboard_button_dict["request_managed_bot"] + == keyboard_button.request_managed_bot.to_dict() + ) @pytest.mark.parametrize("request_user", [True, False]) def test_de_json(self, request_user): @@ -100,6 +108,7 @@ def test_de_json(self, request_user): "request_users": self.request_users.to_dict(), "style": self.style, "icon_custom_emoji_id": self.icon_custom_emoji_id, + "request_managed_bot": self.request_managed_bot.to_dict(), } if request_user: json_dict["request_user"] = {"request_id": 2} @@ -119,6 +128,7 @@ def test_de_json(self, request_user): assert keyboard_button.request_users == self.request_users assert keyboard_button.style == self.style assert keyboard_button.icon_custom_emoji_id == self.icon_custom_emoji_id + assert keyboard_button.request_managed_bot == self.request_managed_bot def test_equality(self): a = KeyboardButton("test", request_contact=True) diff --git a/tests/test_keyboardbuttonrequestmanagedbot.py b/tests/test_keyboardbuttonrequestmanagedbot.py new file mode 100644 index 00000000000..4f3fa95b73e --- /dev/null +++ b/tests/test_keyboardbuttonrequestmanagedbot.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# 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 KeyboardButtonRequestManagedBot +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def keyboard_button_request_managed_bot(): + return KeyboardButtonRequestManagedBot( + KeyboardButtonRequestManagedBotTestBase.request_id, + KeyboardButtonRequestManagedBotTestBase.suggested_name, + KeyboardButtonRequestManagedBotTestBase.suggested_username, + ) + + +class KeyboardButtonRequestManagedBotTestBase: + request_id = 4 + suggested_name = "suggested_name" + suggested_username = "username" + + +class TestKeyboardButtonRequestManagedBotWithoutRequest(KeyboardButtonRequestManagedBotTestBase): + def test_slot_behaviour(self, keyboard_button_request_managed_bot): + inst = keyboard_button_request_managed_bot + 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 = { + "request_id": self.request_id, + "suggested_name": self.suggested_name, + "suggested_username": self.suggested_username, + } + keyboard_button_request_managed_bot = KeyboardButtonRequestManagedBot.de_json( + json_dict, offline_bot + ) + assert keyboard_button_request_managed_bot.api_kwargs == {} + assert keyboard_button_request_managed_bot.request_id == self.request_id + assert keyboard_button_request_managed_bot.suggested_name == self.suggested_name + assert keyboard_button_request_managed_bot.suggested_username == self.suggested_username + + def test_to_dict(self): + keyboard_button_request_managed_bot = KeyboardButtonRequestManagedBot( + self.request_id, + self.suggested_name, + self.suggested_username, + ) + keyboard_button_request_managed_bot_dict = keyboard_button_request_managed_bot.to_dict() + assert ( + keyboard_button_request_managed_bot_dict["request_id"] + == keyboard_button_request_managed_bot.request_id + ) + assert ( + keyboard_button_request_managed_bot_dict["suggested_name"] + == keyboard_button_request_managed_bot.suggested_name + ) + assert ( + keyboard_button_request_managed_bot_dict["suggested_username"] + == keyboard_button_request_managed_bot.suggested_username + ) + + def test_equality(self): + a = KeyboardButtonRequestManagedBot( + self.request_id, self.suggested_name, self.suggested_username + ) + b = KeyboardButtonRequestManagedBot( + self.request_id, self.suggested_name, self.suggested_username + ) + c = KeyboardButtonRequestManagedBot(1, self.suggested_name, self.suggested_username) + d = "not a KeyboardButtonRequestManagedBot" + + 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_managedbot.py b/tests/test_managedbot.py new file mode 100644 index 00000000000..9ec374c5894 --- /dev/null +++ b/tests/test_managedbot.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# 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 ManagedBotCreated, ManagedBotUpdated, User +from tests.auxil.slots import mro_slots + + +@pytest.fixture +def managed_bot_created(): + return ManagedBotCreated( + bot=User( + id=ManagedBotTestBase.bot_id, + is_bot=ManagedBotTestBase.is_bot, + first_name=ManagedBotTestBase.bot_first_name, + username=ManagedBotTestBase.bot_username, + ), + ) + + +@pytest.fixture +def managed_bot_updated(): + return ManagedBotUpdated( + user=User( + id=ManagedBotTestBase.bot_creator_id, + is_bot=ManagedBotTestBase.is_bot, + first_name=ManagedBotTestBase.bot_creator_first_name, + username=ManagedBotTestBase.bot_creator_username, + ), + bot=User( + id=ManagedBotTestBase.bot_id, + is_bot=ManagedBotTestBase.is_bot, + first_name=ManagedBotTestBase.bot_first_name, + username=ManagedBotTestBase.bot_username, + ), + ) + + +class ManagedBotTestBase: + bot_creator_id = 123 + bot_creator_first_name = "TestBotManager" + bot_creator_username = "test_bot_manager" + bot_id = 321 + bot_first_name = "TestBot" + bot_username = "test_bot" + is_bot = True + + +class TestManagedBotCreatedWithoutRequest(ManagedBotTestBase): + def test_slot_behaviour(self, managed_bot_created): + inst = managed_bot_created + 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, managed_bot_created): + json_dict = { + "bot": managed_bot_created.bot.to_dict(), + } + managed_bot_created = ManagedBotCreated.de_json(json_dict, offline_bot) + assert managed_bot_created.api_kwargs == {} + assert managed_bot_created.bot.id == self.bot_id + assert managed_bot_created.bot.is_bot is True + assert managed_bot_created.bot.first_name == self.bot_first_name + assert managed_bot_created.bot.username == self.bot_username + + def test_to_dict(self, managed_bot_created): + managed_bot_created_dict = managed_bot_created.to_dict() + assert managed_bot_created_dict["bot"]["id"] == self.bot_id + assert managed_bot_created_dict["bot"]["is_bot"] is True + assert managed_bot_created_dict["bot"]["first_name"] == self.bot_first_name + assert managed_bot_created_dict["bot"]["username"] == self.bot_username + + def test_equality(self, managed_bot_created): + managed_bot_created_2 = ManagedBotCreated( + bot=User( + id=self.bot_id, + is_bot=True, + first_name=self.bot_first_name, + username=self.bot_username, + ), + ) + managed_bot_created_3 = ManagedBotCreated( + bot=User( + id=4534, is_bot=True, first_name=self.bot_first_name, username=self.bot_username + ), + ) + not_a_managed_bot_created = ManagedBotUpdated( + user=User( + id=self.bot_creator_id, + is_bot=True, + first_name=self.bot_creator_first_name, + username=self.bot_creator_username, + ), + bot=User( + id=self.bot_id, + is_bot=True, + first_name=self.bot_first_name, + username=self.bot_username, + ), + ) + + assert managed_bot_created == managed_bot_created_2 + assert hash(managed_bot_created) == hash(managed_bot_created_2) + + assert managed_bot_created != managed_bot_created_3 + assert hash(managed_bot_created) != hash(managed_bot_created_3) + + assert managed_bot_created != not_a_managed_bot_created + assert hash(managed_bot_created) != hash(not_a_managed_bot_created) + + +class TestManagedBotUpdatedWithoutRequest(ManagedBotTestBase): + def test_slot_behaviour(self, managed_bot_updated): + inst = managed_bot_updated + 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, managed_bot_updated): + json_dict = { + "user": managed_bot_updated.user.to_dict(), + "bot": managed_bot_updated.bot.to_dict(), + } + managed_bot_updated_new = ManagedBotUpdated.de_json(json_dict, offline_bot) + assert managed_bot_updated_new.api_kwargs == {} + assert managed_bot_updated_new.user == managed_bot_updated.user + assert managed_bot_updated_new.bot == managed_bot_updated.bot + + def test_to_dict(self, managed_bot_updated): + managed_bot_updated_dict = managed_bot_updated.to_dict() + assert managed_bot_updated_dict["user"] == managed_bot_updated.user.to_dict() + assert managed_bot_updated_dict["bot"] == managed_bot_updated.bot.to_dict() + + def test_equality(self, managed_bot_updated): + managed_bot_updated_2 = ManagedBotUpdated( + user=User( + id=self.bot_creator_id, + is_bot=True, + first_name=self.bot_creator_first_name, + username=self.bot_creator_username, + ), + bot=User( + id=self.bot_id, + is_bot=True, + first_name=self.bot_first_name, + username=self.bot_username, + ), + ) + managed_bot_updated_3 = ManagedBotUpdated( + user=User( + id=4534, + is_bot=True, + first_name=self.bot_creator_first_name, + username=self.bot_creator_username, + ), + bot=User( + id=self.bot_id, + is_bot=True, + first_name=self.bot_first_name, + username=self.bot_username, + ), + ) + not_a_managed_bot_updated = ManagedBotCreated( + bot=User( + id=self.bot_id, + is_bot=True, + first_name=self.bot_first_name, + username=self.bot_username, + ), + ) + + assert managed_bot_updated == managed_bot_updated_2 + assert hash(managed_bot_updated) == hash(managed_bot_updated_2) + + assert managed_bot_updated != managed_bot_updated_3 + assert hash(managed_bot_updated) != hash(managed_bot_updated_3) + + assert managed_bot_updated != not_a_managed_bot_updated + assert hash(managed_bot_updated) != hash(not_a_managed_bot_updated) diff --git a/tests/test_message.py b/tests/test_message.py index 2873f17414c..958c59b3109 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -56,6 +56,7 @@ Invoice, LinkPreviewOptions, Location, + ManagedBotCreated, Message, MessageAutoDeleteTimerChanged, MessageEntity, @@ -67,6 +68,7 @@ PhotoSize, Poll, PollOption, + PollOptionAdded, ProximityAlertTriggered, RefundedPayment, ReplyParameters, @@ -102,6 +104,7 @@ WebAppData, ) from telegram._directmessagestopic import DirectMessagesTopic +from telegram._poll import PollOptionDeleted from telegram._utils.datetime import UTC from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ODVInput @@ -436,6 +439,10 @@ def message(bot): {"chat_owner_changed": ChatOwnerChanged(new_owner=User(4, "Snow", False))}, {"chat_owner_left": ChatOwnerLeft(new_owner=User(5, "Crash", False))}, {"sender_tag": "This is a tag"}, + {"poll_option_added": PollOptionAdded(option_persistent_id="abc", option_text="this")}, + {"poll_option_deleted": PollOptionDeleted(option_persistent_id="abc", option_text="this")}, + {"reply_to_poll_option_id": "3123"}, + {"managed_bot_created": ManagedBotCreated(bot=User(6, "ManagedBot", True))}, ], ids=[ "reply", @@ -530,6 +537,10 @@ def message(bot): "chat_owner_changed", "chat_owner_left", "sender_tag", + "poll_option_added", + "poll_option_deleted", + "reply_to_poll_option_id", + "managed_bot_created", ], ) def message_params(bot, request): diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 1f7a86f0ebc..f62ef853990 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -18,6 +18,8 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains exceptions to our API compared to the official API.""" +from collections.abc import Sequence + from telegram import Animation, Audio, Document, Gift, PhotoSize, Sticker, Video, VideoNote, Voice from tests.test_official.helpers import _get_params_base @@ -71,7 +73,10 @@ class ParamTypeCheckingExceptions: # too complex to compare/predict with official API # structure: class/method_name: {param_name: reduced form of annotation} COMPLEX_TYPES = { - "send_poll": {"correct_option_id": int}, # actual: Literal + "send_poll": { + # "correct_option_id": int, + "correct_option_ids": Sequence[int] + }, "get_file": { "file_id": str, # actual: Union[str, objs_with_file_id_attr] }, @@ -214,7 +219,11 @@ def ignored_param_requirements(object_name: str) -> set[str]: # Arguments that are optional arguments for now for backwards compatibility -BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {} +BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = { + "PollOption": {"persistent_id"}, + "PollAnswer": {"option_persistent_ids"}, + "Poll": {"allows_revoting"}, +} def backwards_compat_kwargs(object_name: str) -> set[str]: diff --git a/tests/test_poll.py b/tests/test_poll.py index 2edc93d3d9f..0f1998ed17d 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -19,7 +19,17 @@ import pytest -from telegram import Chat, InputPollOption, MessageEntity, Poll, PollAnswer, PollOption, User +from telegram import ( + Chat, + InputPollOption, + MaybeInaccessibleMessage, + MessageEntity, + Poll, + PollAnswer, + PollOption, + User, +) +from telegram._poll import PollOptionAdded, PollOptionDeleted from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import PollType from telegram.warnings import PTBDeprecationWarning @@ -108,6 +118,9 @@ def poll_option(): text=PollOptionTestBase.text, voter_count=PollOptionTestBase.voter_count, text_entities=PollOptionTestBase.text_entities, + added_by_user=PollOptionTestBase.added_by_user, + added_by_chat=PollOptionTestBase.added_by_chat, + addition_date=PollOptionTestBase.addition_date, ) out._unfreeze() return out @@ -120,6 +133,9 @@ class PollOptionTestBase: MessageEntity(MessageEntity.BOLD, 0, 4), MessageEntity(MessageEntity.ITALIC, 5, 6), ] + added_by_user = User(1, "test_user", False) + added_by_chat = Chat(1, "test_chat") + addition_date = dtm.datetime.now(dtm.timezone.utc) class TestPollOptionWithoutRequest(PollOptionTestBase): @@ -129,26 +145,23 @@ def test_slot_behaviour(self, poll_option): assert len(mro_slots(poll_option)) == len(set(mro_slots(poll_option))), "duplicate slot" def test_de_json(self): - json_dict = {"text": self.text, "voter_count": self.voter_count} - poll_option = PollOption.de_json(json_dict, None) - assert poll_option.api_kwargs == {} - - assert poll_option.text == self.text - assert poll_option.voter_count == self.voter_count - - def test_de_json_all(self): json_dict = { "text": self.text, "voter_count": self.voter_count, "text_entities": [e.to_dict() for e in self.text_entities], + "added_by_user": self.added_by_user.to_dict(), + "added_by_chat": self.added_by_chat.to_dict(), + "addition_date": to_timestamp(self.addition_date), } poll_option = PollOption.de_json(json_dict, None) - assert poll_option.api_kwargs == {} assert poll_option.text == self.text assert poll_option.voter_count == self.voter_count assert poll_option.text_entities == tuple(self.text_entities) + assert poll_option.added_by_user == self.added_by_user + assert poll_option.added_by_chat == self.added_by_chat + assert abs((poll_option.addition_date - self.addition_date).total_seconds()) < 1 def test_to_dict(self, poll_option): poll_option_dict = poll_option.to_dict() @@ -159,6 +172,9 @@ def test_to_dict(self, poll_option): assert poll_option_dict["text_entities"] == [ e.to_dict() for e in poll_option.text_entities ] + assert poll_option_dict["added_by_user"] == poll_option.added_by_user.to_dict() + assert poll_option_dict["added_by_chat"] == poll_option.added_by_chat.to_dict() + assert poll_option_dict["addition_date"] == to_timestamp(poll_option.addition_date) def test_parse_entity(self, poll_option): entity = MessageEntity(MessageEntity.BOLD, 0, 4) @@ -201,6 +217,7 @@ def poll_answer(): PollAnswerTestBase.option_ids, PollAnswerTestBase.user, PollAnswerTestBase.voter_chat, + PollAnswerTestBase.option_persistent_ids, ) @@ -209,6 +226,7 @@ class PollAnswerTestBase: option_ids = [2] user = User(1, "", False) voter_chat = Chat(1, "") + option_persistent_ids = ["123"] class TestPollAnswerWithoutRequest(PollAnswerTestBase): @@ -218,6 +236,7 @@ def test_de_json(self): "option_ids": self.option_ids, "user": self.user.to_dict(), "voter_chat": self.voter_chat.to_dict(), + "option_persistent_ids": self.option_persistent_ids, } poll_answer = PollAnswer.de_json(json_dict, None) assert poll_answer.api_kwargs == {} @@ -226,6 +245,7 @@ def test_de_json(self): assert poll_answer.option_ids == tuple(self.option_ids) assert poll_answer.user == self.user assert poll_answer.voter_chat == self.voter_chat + assert poll_answer.option_persistent_ids == tuple(self.option_persistent_ids) def test_to_dict(self, poll_answer): poll_answer_dict = poll_answer.to_dict() @@ -235,6 +255,7 @@ def test_to_dict(self, poll_answer): assert poll_answer_dict["option_ids"] == list(poll_answer.option_ids) assert poll_answer_dict["user"] == poll_answer.user.to_dict() assert poll_answer_dict["voter_chat"] == poll_answer.voter_chat.to_dict() + assert poll_answer_dict["option_persistent_ids"] == list(poll_answer.option_persistent_ids) def test_equality(self): a = PollAnswer(123, [2], self.user, self.voter_chat) @@ -276,6 +297,10 @@ def poll(): open_period=PollTestBase.open_period, close_date=PollTestBase.close_date, question_entities=PollTestBase.question_entities, + allows_revoting=PollTestBase.allows_revoting, + correct_option_ids=PollTestBase.correct_option_ids, + description=PollTestBase.description, + description_entities=PollTestBase.description_entities, ) poll._unfreeze() return poll @@ -301,6 +326,10 @@ class PollTestBase: MessageEntity(MessageEntity.BOLD, 0, 4), MessageEntity(MessageEntity.ITALIC, 5, 8), ] + allows_revoting = True + correct_option_ids = [1, 2] + description = "description" + description_entities = [MessageEntity(MessageEntity.ITALIC, 0, 11)] class TestPollWithoutRequest(PollTestBase): @@ -319,6 +348,10 @@ def test_de_json(self, offline_bot): "open_period": int(self.open_period.total_seconds()), "close_date": to_timestamp(self.close_date), "question_entities": [e.to_dict() for e in self.question_entities], + "allows_revoting": self.allows_revoting, + "correct_option_ids": self.correct_option_ids, + "description": self.description, + "description_entities": [e.to_dict() for e in self.description_entities], } poll = Poll.de_json(json_dict, offline_bot) assert poll.api_kwargs == {} @@ -341,6 +374,10 @@ def test_de_json(self, offline_bot): assert abs(poll.close_date - self.close_date) < dtm.timedelta(seconds=1) assert to_timestamp(poll.close_date) == to_timestamp(self.close_date) assert poll.question_entities == tuple(self.question_entities) + assert poll.allows_revoting == self.allows_revoting + assert poll.correct_option_ids == tuple(self.correct_option_ids) + assert poll.description == self.description + assert poll.description_entities == tuple(self.description_entities) def test_de_json_localization(self, tz_bot, offline_bot, raw_bot): json_dict = { @@ -357,6 +394,10 @@ def test_de_json_localization(self, tz_bot, offline_bot, raw_bot): "open_period": int(self.open_period.total_seconds()), "close_date": to_timestamp(self.close_date), "question_entities": [e.to_dict() for e in self.question_entities], + "allows_revoting": self.allows_revoting, + "correct_option_ids": self.correct_option_ids, + "description": self.description, + "description_entities": [e.to_dict() for e in self.description_entities], } poll_raw = Poll.de_json(json_dict, raw_bot) @@ -390,6 +431,12 @@ def test_to_dict(self, poll): assert poll_dict["open_period"] == int(self.open_period.total_seconds()) assert poll_dict["close_date"] == to_timestamp(poll.close_date) assert poll_dict["question_entities"] == [e.to_dict() for e in poll.question_entities] + assert poll_dict["allows_revoting"] == poll.allows_revoting + assert poll_dict["correct_option_ids"] == list(poll.correct_option_ids) + assert poll_dict["description"] == poll.description + assert poll_dict["description_entities"] == [ + e.to_dict() for e in poll.description_entities + ] def test_time_period_properties(self, PTB_TIMEDELTA, poll): if PTB_TIMEDELTA: @@ -409,6 +456,26 @@ def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, poll): assert "`open_period` will be of type `datetime.timedelta`" in str(recwarn[0].message) assert recwarn[0].category is PTBDeprecationWarning + def test_correct_option_id_deprecated(self, recwarn, poll): + poll.correct_option_id + + assert len(recwarn) == 1 + assert "The attribute `correct_option_id` is deprecated" in str(recwarn[0].message) + assert recwarn[0].category is PTBDeprecationWarning + + poll = Poll( + PollTestBase.id_, + PollTestBase.question, + PollTestBase.options, + PollTestBase.total_voter_count, + PollTestBase.is_closed, + PollTestBase.is_anonymous, + PollTestBase.type, + PollTestBase.allows_multiple_answers, + correct_option_id=1, + ) + assert poll.correct_option_ids == (1,) + def test_equality(self): a = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True) b = Poll(123, "question", ["o1", "o2"], 1, True, False, Poll.REGULAR, True) @@ -499,3 +566,281 @@ def test_parse_question_entities(self, poll): assert poll.parse_question_entities(MessageEntity.ITALIC) == {entity: "Question"} assert poll.parse_question_entities() == {entity: "Question", entity_2: "Test"} + + def test_parse_description_entity(self, poll): + entity = MessageEntity(MessageEntity.ITALIC, 0, 11) + poll.description_entities = [entity] + + assert poll.parse_description_entity(entity) == "description" + with pytest.raises(RuntimeError, match="Poll has no"): + Poll( + "id", + "question", + [PollOption("text", voter_count=0)], + total_voter_count=0, + is_closed=False, + is_anonymous=False, + type=Poll.QUIZ, + allows_multiple_answers=False, + ).parse_description_entity(entity) + + def test_parse_description_entities(self, poll): + entity = MessageEntity(MessageEntity.ITALIC, 0, 11) + entity_2 = MessageEntity(MessageEntity.BOLD, 0, 4) + poll.description_entities = [entity_2, entity] + + assert poll.parse_description_entities(MessageEntity.ITALIC) == {entity: "description"} + assert poll.parse_description_entities() == {entity: "description", entity_2: "desc"} + with pytest.raises(RuntimeError, match="Poll has no"): + Poll( + "id", + "question", + [PollOption("text", voter_count=0)], + total_voter_count=0, + is_closed=False, + is_anonymous=False, + type=Poll.QUIZ, + allows_multiple_answers=False, + ).parse_description_entities() + + +@pytest.fixture(scope="module") +def poll_option_added(): + p = PollOptionAdded( + poll_message=PollOptionAddedTestBase.poll_message, + option_persistent_id=PollOptionAddedTestBase.option_persistent_id, + option_text=PollOptionAddedTestBase.option_text, + option_text_entities=PollOptionAddedTestBase.option_text_entities, + ) + p._unfreeze() + return p + + +class PollOptionAddedTestBase: + poll_message = MaybeInaccessibleMessage( + message_id=1, + date=dtm.datetime.now(dtm.timezone.utc), + chat=Chat(1, "test_chat"), + ) + option_persistent_id = "123" + option_text = "test option" + option_text_entities = [ + MessageEntity(MessageEntity.BOLD, 0, 4), + MessageEntity(MessageEntity.ITALIC, 5, 6), + ] + + +class TestPollOptionAddedWithoutRequest(PollOptionAddedTestBase): + def test_slot_behaviour(self, poll_option_added): + for attr in poll_option_added.__slots__: + assert getattr(poll_option_added, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(poll_option_added)) == len(set(mro_slots(poll_option_added))), ( + "duplicate slot" + ) + + def test_de_json(self, offline_bot): + json_dict = { + "poll_message": self.poll_message.to_dict(), + "option_persistent_id": self.option_persistent_id, + "option_text": self.option_text, + "option_text_entities": [e.to_dict() for e in self.option_text_entities], + } + poll_option_added = PollOptionAdded.de_json(json_dict, offline_bot) + assert poll_option_added.api_kwargs == {} + + assert poll_option_added.poll_message == self.poll_message + assert poll_option_added.option_persistent_id == self.option_persistent_id + assert poll_option_added.option_text == self.option_text + assert poll_option_added.option_text_entities == tuple(self.option_text_entities) + + def test_to_dict(self, poll_option_added): + poll_option_added_dict = poll_option_added.to_dict() + + assert isinstance(poll_option_added_dict, dict) + assert poll_option_added_dict["poll_message"] == poll_option_added.poll_message.to_dict() + assert ( + poll_option_added_dict["option_persistent_id"] + == poll_option_added.option_persistent_id + ) + assert poll_option_added_dict["option_text"] == poll_option_added.option_text + assert poll_option_added_dict["option_text_entities"] == [ + e.to_dict() for e in poll_option_added.option_text_entities + ] + + def test_parse_option_text_entity(self, poll_option_added): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + poll_option_added.option_text_entities = [entity] + + assert poll_option_added.parse_option_text_entity(entity) == "test" + + def test_parse_option_text_entities(self, poll_option_added): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + entity_2 = MessageEntity(MessageEntity.ITALIC, 5, 6) + poll_option_added.option_text_entities = [entity, entity_2] + + assert poll_option_added.parse_option_text_entities(MessageEntity.BOLD) == {entity: "test"} + assert poll_option_added.parse_option_text_entities() == { + entity: "test", + entity_2: "option", + } + + def test_equality(self): + a = PollOptionAdded( + poll_message=self.poll_message, + option_persistent_id=self.option_persistent_id, + option_text=self.option_text, + option_text_entities=self.option_text_entities, + ) + b = PollOptionAdded( + poll_message=self.poll_message, + option_persistent_id=self.option_persistent_id, + option_text=self.option_text, + option_text_entities=self.option_text_entities, + ) + c = PollOptionAdded( + poll_message=MaybeInaccessibleMessage( + 2, dtm.datetime.now(dtm.timezone.utc), Chat(1, "test_chat") + ), + option_persistent_id=self.option_persistent_id, + option_text=self.option_text, + option_text_entities=self.option_text_entities, + ) + + d = PollOptionAdded( + poll_message=self.poll_message, + option_persistent_id="different_id", + option_text=self.option_text, + option_text_entities=self.option_text_entities, + ) + + assert a == b + assert hash(a) == hash(b) + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture(scope="module") +def poll_option_deleted(): + p = PollOptionDeleted( + poll_message=PollOptionDeletedTestBase.poll_message, + option_persistent_id=PollOptionDeletedTestBase.option_persistent_id, + option_text=PollOptionDeletedTestBase.option_text, + option_text_entities=PollOptionDeletedTestBase.option_text_entities, + ) + p._unfreeze() + return p + + +class PollOptionDeletedTestBase: + poll_message = MaybeInaccessibleMessage( + message_id=1, + date=dtm.datetime.now(dtm.timezone.utc), + chat=Chat(1, "test_chat"), + ) + option_persistent_id = "123" + option_text = "test option" + option_text_entities = [ + MessageEntity(MessageEntity.BOLD, 0, 4), + MessageEntity(MessageEntity.ITALIC, 5, 6), + ] + + +class TestPollOptionDeletedWithoutRequest(PollOptionDeletedTestBase): + def test_slot_behaviour(self, poll_option_deleted): + for attr in poll_option_deleted.__slots__: + assert getattr(poll_option_deleted, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(poll_option_deleted)) == len(set(mro_slots(poll_option_deleted))), ( + "duplicate slot" + ) + + def test_de_json(self, offline_bot): + json_dict = { + "poll_message": self.poll_message.to_dict(), + "option_persistent_id": self.option_persistent_id, + "option_text": self.option_text, + "option_text_entities": [e.to_dict() for e in self.option_text_entities], + } + poll_option_deleted = PollOptionDeleted.de_json(json_dict, offline_bot) + assert poll_option_deleted.api_kwargs == {} + + assert poll_option_deleted.poll_message == self.poll_message + assert poll_option_deleted.option_persistent_id == self.option_persistent_id + assert poll_option_deleted.option_text == self.option_text + assert poll_option_deleted.option_text_entities == tuple(self.option_text_entities) + + def test_to_dict(self, poll_option_deleted): + poll_option_deleted_dict = poll_option_deleted.to_dict() + + assert isinstance(poll_option_deleted_dict, dict) + assert ( + poll_option_deleted_dict["poll_message"] == poll_option_deleted.poll_message.to_dict() + ) + assert ( + poll_option_deleted_dict["option_persistent_id"] + == poll_option_deleted.option_persistent_id + ) + assert poll_option_deleted_dict["option_text"] == poll_option_deleted.option_text + assert poll_option_deleted_dict["option_text_entities"] == [ + e.to_dict() for e in poll_option_deleted.option_text_entities + ] + + def test_parse_option_text_entity(self, poll_option_deleted): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + poll_option_deleted.option_text_entities = [entity] + + assert poll_option_deleted.parse_option_text_entity(entity) == "test" + + def test_parse_option_text_entities(self, poll_option_deleted): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + entity_2 = MessageEntity(MessageEntity.ITALIC, 5, 6) + poll_option_deleted.option_text_entities = [entity, entity_2] + + assert poll_option_deleted.parse_option_text_entities(MessageEntity.BOLD) == { + entity: "test" + } + assert poll_option_deleted.parse_option_text_entities() == { + entity: "test", + entity_2: "option", + } + + def test_equality(self): + a = PollOptionDeleted( + poll_message=self.poll_message, + option_persistent_id=self.option_persistent_id, + option_text=self.option_text, + option_text_entities=self.option_text_entities, + ) + b = PollOptionDeleted( + poll_message=self.poll_message, + option_persistent_id=self.option_persistent_id, + option_text=self.option_text, + option_text_entities=self.option_text_entities, + ) + c = PollOptionDeleted( + poll_message=MaybeInaccessibleMessage( + 2, dtm.datetime.now(dtm.timezone.utc), Chat(1, "test_chat") + ), + option_persistent_id=self.option_persistent_id, + option_text=self.option_text, + option_text_entities=self.option_text_entities, + ) + + d = PollOptionDeleted( + poll_message=self.poll_message, + option_persistent_id="different_id", + option_text=self.option_text, + option_text_entities=self.option_text_entities, + ) + + 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_preparedkeyboardbutton.py b/tests/test_preparedkeyboardbutton.py new file mode 100644 index 00000000000..9b62be8445a --- /dev/null +++ b/tests/test_preparedkeyboardbutton.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# 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 PreparedKeyboardButton +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def prepared_keyboard_button(): + return PreparedKeyboardButton(PreparedKeyboardButtonTestBase.id) + + +class PreparedKeyboardButtonTestBase: + id = "4" + + +class TestPreparedKeyboardButtonWithoutRequest(PreparedKeyboardButtonTestBase): + def test_slot_behaviour(self, prepared_keyboard_button): + inst = prepared_keyboard_button + 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 = { + "id": self.id, + } + prepared_keyboard_button = PreparedKeyboardButton.de_json(json_dict, offline_bot) + assert prepared_keyboard_button.api_kwargs == {} + assert prepared_keyboard_button.id == self.id + + def test_to_dict(self, prepared_keyboard_button): + prepared_keyboard_button_dict = prepared_keyboard_button.to_dict() + assert prepared_keyboard_button_dict["id"] == self.id + + def test_equality(self): + a = PreparedKeyboardButton(self.id) + b = PreparedKeyboardButton(self.id) + c = PreparedKeyboardButton("5") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) diff --git a/tests/test_reply.py b/tests/test_reply.py index 16e9da49957..1822fff19f4 100644 --- a/tests/test_reply.py +++ b/tests/test_reply.py @@ -219,6 +219,7 @@ def reply_parameters(): quote_entities=ReplyParametersTestBase.quote_entities, quote_position=ReplyParametersTestBase.quote_position, checklist_task_id=ReplyParametersTestBase.checklist_task_id, + poll_option_id=ReplyParametersTestBase.poll_option_id, ) @@ -234,6 +235,7 @@ class ReplyParametersTestBase: ] quote_position = 5 checklist_task_id = 9 + poll_option_id = "213" class TestReplyParametersWithoutRequest(ReplyParametersTestBase): @@ -254,6 +256,7 @@ def test_de_json(self, offline_bot): "quote_entities": [entity.to_dict() for entity in self.quote_entities], "quote_position": self.quote_position, "checklist_task_id": self.checklist_task_id, + "poll_option_id": self.poll_option_id, } reply_parameters = ReplyParameters.de_json(json_dict, offline_bot) @@ -267,6 +270,7 @@ def test_de_json(self, offline_bot): assert reply_parameters.quote_entities == tuple(self.quote_entities) assert reply_parameters.quote_position == self.quote_position assert reply_parameters.checklist_task_id == self.checklist_task_id + assert reply_parameters.poll_option_id == self.poll_option_id def test_to_dict(self, reply_parameters): reply_parameters_dict = reply_parameters.to_dict() @@ -285,6 +289,7 @@ def test_to_dict(self, reply_parameters): ] assert reply_parameters_dict["quote_position"] == self.quote_position assert reply_parameters_dict["checklist_task_id"] == self.checklist_task_id + assert reply_parameters_dict["poll_option_id"] == self.poll_option_id def test_equality(self, reply_parameters): a = reply_parameters diff --git a/tests/test_update.py b/tests/test_update.py index b74234b9435..8a8e0c4853b 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -38,6 +38,7 @@ ChosenInlineResult, InaccessibleMessage, InlineQuery, + ManagedBotUpdated, Message, MessageReactionCountUpdated, MessageReactionUpdated, @@ -151,6 +152,10 @@ paid_media_payload="payload", ) +managed_bot = ManagedBotUpdated( + user=User(1, "creator", True), + bot=User(2, "bot", True), +) params = [ {"message": message}, @@ -191,6 +196,7 @@ {"business_message": business_message}, {"edited_business_message": business_message}, {"purchased_paid_media": purchased_paid_media}, + {"managed_bot": managed_bot}, # Must be last to conform with `ids` below! {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, ] @@ -219,6 +225,7 @@ "business_message", "edited_business_message", "purchased_paid_media", + "managed_bot", ) ids = (*all_types, "callback_query_without_message") @@ -300,6 +307,7 @@ def test_effective_chat(self, update): or update.poll_answer is not None or update.business_connection is not None or update.purchased_paid_media is not None + or update.managed_bot is not None ): assert chat.id == 1 else: @@ -414,6 +422,7 @@ def test_effective_message(self, update): or update.deleted_business_messages is not None or update.business_connection is not None or update.purchased_paid_media is not None + or update.managed_bot is not None ): assert eff_message.message_id == message.message_id else: diff --git a/tests/test_user.py b/tests/test_user.py index ba237ea476d..24f8ffe8c8d 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -46,6 +46,7 @@ def json_dict(): "has_main_web_app": UserTestBase.has_main_web_app, "has_topics_enabled": UserTestBase.has_topics_enabled, "allows_users_to_create_topics": UserTestBase.allows_users_to_create_topics, + "can_manage_bots": UserTestBase.can_manage_bots, } @@ -67,6 +68,7 @@ def user(bot): has_main_web_app=UserTestBase.has_main_web_app, has_topics_enabled=UserTestBase.has_topics_enabled, allows_users_to_create_topics=UserTestBase.allows_users_to_create_topics, + can_manage_bots=UserTestBase.can_manage_bots, ) user.set_bot(bot) user._unfreeze() @@ -89,6 +91,7 @@ class UserTestBase: has_main_web_app = False has_topics_enabled = False allows_users_to_create_topics = False + can_manage_bots = True class TestUserWithoutRequest(UserTestBase): @@ -116,6 +119,7 @@ def test_de_json(self, json_dict, offline_bot): assert user.has_main_web_app == self.has_main_web_app assert user.has_topics_enabled == self.has_topics_enabled assert user.allows_users_to_create_topics == self.allows_users_to_create_topics + assert user.can_manage_bots == self.can_manage_bots def test_to_dict(self, user): user_dict = user.to_dict() @@ -136,6 +140,7 @@ def test_to_dict(self, user): assert user_dict["has_main_web_app"] == user.has_main_web_app assert user_dict["has_topics_enabled"] == user.has_topics_enabled assert user_dict["allows_users_to_create_topics"] == user.allows_users_to_create_topics + assert user_dict["can_manage_bots"] == user.can_manage_bots def test_equality(self): a = User(self.id_, self.first_name, self.is_bot, self.last_name) @@ -906,3 +911,18 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(user.get_bot(), "set_chat_member_tag", make_assertion) assert await user.set_chat_member_tag(chat_id="chat_id", tag="tag") + + async def test_instance_method_replace_token(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["user_id"] == user.id + + assert check_shortcut_signature( + user.replace_token, Bot.replace_managed_bot_token, ["user_id"], [] + ) + assert await check_shortcut_call( + user.replace_token, user.get_bot(), "replace_managed_bot_token" + ) + assert await check_defaults_handling(user.replace_token, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "replace_managed_bot_token", make_assertion) + assert await user.replace_token() diff --git a/uv.lock b/uv.lock index 1dcaef13739..fceee73d550 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", @@ -495,62 +496,62 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] [[package]] @@ -579,7 +580,8 @@ name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", @@ -975,7 +977,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -984,39 +986,51 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/97/ce2502df2cecf2ef997b6c6527c4a223b92feb9e7b790cdc8dcd683f3a8a/mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", size = 14457059, upload-time = "2026-04-21T17:06:14.935Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/417ee60b822cc80c0f3dc9f495ad7fd8dbb8d8b2cf4baf22d4046d25d01d/mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", size = 13346816, upload-time = "2026-04-21T17:10:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/e20951978702df58379d0bcc2e8f7ccdca4e78cd7dc66dd3ddbf9b29d517/mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", size = 13772593, upload-time = "2026-04-21T17:08:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/63/a5/5441a13259ec516c56fd5de0fd96a69a9590ae6c5e5d3e5174aa84b97973/mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", size = 14656635, upload-time = "2026-04-21T17:09:54.042Z" }, + { url = "https://files.pythonhosted.org/packages/3b/51/b89c69157c5e1f19fd125a65d991166a26906e7902f026f00feebbcfa2b9/mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", size = 14943278, upload-time = "2026-04-21T17:09:15.599Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/6b0eeecfe96d7cce1d71c66b8e03cb304aa70ec11f1955dc1d6b46aca3c3/mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd", size = 10851915, upload-time = "2026-04-21T17:06:03.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/6593dc88545d75fb96416184be5392da5e2a8e8c2802a8597913e16ae25c/mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", size = 9786676, upload-time = "2026-04-21T17:07:02.035Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" }, + { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, ] [[package]] @@ -1121,7 +1135,8 @@ name = "pydantic" version = "2.13.0b2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", ] dependencies = [ { name = "annotated-types", marker = "python_full_version >= '3.14'" }, @@ -1261,7 +1276,8 @@ name = "pydantic-core" version = "2.42.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", ] dependencies = [ { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, @@ -1423,7 +1439,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1434,9 +1450,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -1622,16 +1638,16 @@ all = [ { name = "chango", marker = "python_full_version >= '3.12'", specifier = "~=0.6.0" }, { name = "flaky", specifier = ">=3.8.1" }, { name = "furo", specifier = "==2025.12.19" }, - { name = "mypy", specifier = "==1.19.1" }, + { name = "mypy", specifier = "==1.20.2" }, { name = "prek" }, { name = "pydantic", marker = "python_full_version >= '3.14'", specifier = ">=2.12.0a1" }, { name = "pylint", specifier = "==4.0.5" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==0.21.2" }, { name = "pytest-cov" }, { name = "pytest-xdist", specifier = "==3.8.0" }, { name = "pytz" }, - { name = "ruff", specifier = "==0.15.6" }, + { name = "ruff", specifier = "==0.15.12" }, { name = "sphinx", marker = "python_full_version >= '3.12'", specifier = "==9.1.0" }, { name = "sphinx-build-compatibility", git = "https://github.com/readthedocs/sphinx-build-compatibility.git?rev=58aabc5f207c6c2421f23d3578adc0b14af57047" }, { name = "sphinx-copybutton", specifier = "==0.5.2" }, @@ -1652,16 +1668,16 @@ docs = [ { name = "sphinxcontrib-mermaid", specifier = "==2.0.1" }, ] linting = [ - { name = "mypy", specifier = "==1.19.1" }, + { name = "mypy", specifier = "==1.20.2" }, { name = "prek" }, { name = "pylint", specifier = "==4.0.5" }, - { name = "ruff", specifier = "==0.15.6" }, + { name = "ruff", specifier = "==0.15.12" }, ] tests = [ { name = "beautifulsoup4" }, { name = "build" }, { name = "flaky", specifier = ">=3.8.1" }, - { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==0.21.2" }, { name = "pytest-cov" }, { name = "pytest-xdist", specifier = "==3.8.0" }, @@ -1781,27 +1797,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, - { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, - { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, - { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, - { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, - { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, - { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, - { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, - { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, - { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] @@ -1916,7 +1932,8 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", ]