Compare commits

...

24 Commits

Author SHA1 Message Date
Hinrich Mahler 2642ecc737 Bump version to v20.0a4 2022-08-27 13:25:57 +02:00
Bibo-Joshi abfcf72a56 Fix setup.py Regarding Optional Dependencies (#3209) 2022-08-27 13:23:17 +02:00
Hinrich Mahler 0e044804d2 Bump version to v20.0a3 2022-08-27 12:39:38 +02:00
Bibo-Joshi 5b9afd5329 Type Hinting Fixes (#3202) 2022-08-27 11:58:28 +02:00
Bibo-Joshi a983a89964 Documentation Improvements (#3139, #3153, #3135)
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
Co-authored-by: Poolitzer <github@poolitzer.eu>
Co-authored-by: Pawan <pawanrai9999@gmail.com>
Co-authored-by: Aditya Yadav <69784758+aditya-yadav-27@users.noreply.github.com>
2022-08-27 11:46:51 +02:00
Bibo-Joshi 741a50ab97 New Rate Limiting Mechanism (#3148) 2022-08-26 06:50:03 +02:00
Poolitzer cf6c298b82 API 6.2 (#3195)
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2022-08-25 19:36:55 +02:00
Harshil 90c0fe948b Add Python 3.11 to Test Suite & Adapt Enum Behaviour (#3168) 2022-08-17 18:24:50 +02:00
dependabot[bot] 2c84122654 Bump sphinx from 5.0.2 to 5.1.1 (#3177)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-03 22:03:00 +02:00
Harshil 143db5fc9d Drop Manual Token Validation (#3167) 2022-08-03 08:16:48 +02:00
pre-commit-ci[bot] c28ad86214 Update pre-commit Dependencies (#3085)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2022-08-02 08:26:36 +02:00
dependabot[bot] 15f153474a Bump pytest-asyncio from 0.18.3 to 0.19.0 (#3158)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-17 13:34:08 +02:00
Harshil 55d66a9ea3 Fix helpers.mention_markdown for Markdown V1 and Improve Related Unit Tests (#3155)
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2022-07-17 13:07:21 +02:00
dependabot[bot] 14c86daf23 Update tornado requirement from ~=6.1 to ~=6.2 (#3149)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2022-07-12 20:16:22 +02:00
Bibo-Joshi 460aaf8bb6 Make chat/user_data Available in Error Handler for Errors in Jobs (#3152) 2022-07-11 07:54:03 +02:00
Harshil 1e703a0be5 Simplify Unit Tests for Bot.send_chat_action (#3151) 2022-07-10 17:14:45 +02:00
Bibo-Joshi 142e3c0177 Add api_kwargs Paramater to Bot.log_out and Improve Related Unit Tests (#3147) 2022-07-10 15:37:12 +02:00
Bibo-Joshi d4b7a2b3e9 Drop pre-commit Dependencies from requirements-dev.txt (#3120)
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2022-07-09 23:12:07 +02:00
dependabot[bot] dac6d03666 Bump black from 22.3.0 to 22.6.0 (#3132)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2022-07-08 00:13:57 +02:00
dependabot[bot] 3bfd58dfd9 Bump actions/setup-python from 3 to 4 (#3131)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-07 23:21:20 +02:00
Bibo-Joshi 2d6459b290 Make Bot.delete_my_commands a Coroutine Function (#3136) 2022-07-04 19:33:45 +02:00
Alex 1f0f6a8d3d Add Application.post_shutdown (#3126)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2022-07-03 15:22:50 +02:00
Bibo-Joshi 2ecb8d5413 Fix ConversationHandler.check_update not respecting per_user (#3128) 2022-07-03 15:21:04 +02:00
Bibo-Joshi f1d03393de Change Default Values for concurrent_updates and connection_pool_size (#3127) 2022-06-29 21:38:03 +02:00
99 changed files with 5106 additions and 487 deletions
+3 -2
View File
@@ -26,7 +26,7 @@ Setting things up
.. code-block:: bash
$ pip install -r requirements.txt -r requirements-dev.txt
$ pip install -r requirements-all.txt
5. Install pre-commit hooks:
@@ -82,7 +82,7 @@ Here's how to make a one-off code change.
- Documenting types of global variables and complex types of class members can be done using the Sphinx docstring convention.
- In addition, PTB uses the `Black`_ coder formatting. Plugins for Black exist for some `popular editors`_. You can use those instead of manually formatting everything.
- In addition, PTB uses some formatting/styling and linting tools in the pre-commit setup. Some of those tools also have command line tools that can help to run these tools outside of the pre-commit step. If you'd like to leverage that, please have a look at the `pre-commit config file`_ for an overview of which tools (and which versions of them) are used. For example, we use `Black`_ for code formatting. Plugins for Black exist for some `popular editors`_. You can use those instead of manually formatting everything.
- Please ensure that the code you write is well-tested.
@@ -273,6 +273,7 @@ break the API classes. For example:
.. _AUTHORS.rst: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/AUTHORS.rst
.. _`MyPy`: https://mypy.readthedocs.io/en/stable/index.html
.. _`here`: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
.. _`pre-commit config file`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.pre-commit-config.yaml
.. _`Black`: https://black.readthedocs.io/en/stable/index.html
.. _`popular editors`: https://black.readthedocs.io/en/stable/integrations/editors.html
.. _`RTD`: https://docs.python-telegram-bot.org/
+2 -4
View File
@@ -16,14 +16,12 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -r requirements.txt
python -W ignore -m pip install -r requirements-dev.txt
python -W ignore -m pip install -r docs/requirements-docs.txt
python -W ignore -m pip install -r requirements-all.txt
- name: Check Links
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto -b linkcheck
+2 -4
View File
@@ -21,14 +21,12 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -r requirements.txt
python -W ignore -m pip install -r requirements-dev.txt
python -W ignore -m pip install -r docs/requirements-docs.txt
python -W ignore -m pip install -r requirements-all.txt
- name: Build docs
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto
@@ -3,7 +3,7 @@ on:
pull_request_target:
paths:
- requirements.txt
- requirements-dev.txt
- requirements-opts.txt
- .pre-commit-config.yaml
permissions:
pull-requests: write
@@ -15,5 +15,5 @@ jobs:
- name: running the check
uses: Poolitzer/notifier-action@master
with:
notify-message: Hey! Looks like you edited the (dev) requirements or the pre-commit hooks. I'm just a friendly reminder to keep the pre-commit hook versions in sync with the dev requirements and the additional dependencies for the hooks in sync with the requirements :)
notify-message: Hey! Looks like you edited the (optional) requirements or the pre-commit hooks. I'm just a friendly reminder to keep the additional dependencies for the hooks in sync with the requirements :)
repo-token: ${{ secrets.GITHUB_TOKEN }}
+31 -9
View File
@@ -4,7 +4,7 @@ on:
branches:
- master
push:
branches:
branches:
- master
schedule:
# Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions
@@ -14,15 +14,27 @@ jobs:
pytest:
name: pytest
runs-on: ${{matrix.os}}
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
os: [ubuntu-latest, windows-latest, macos-latest]
experimental: [false]
include:
- python-version: 3.11.0-rc.1
os: ubuntu-latest
experimental: true
- python-version: 3.11.0-rc.1
os: windows-latest
experimental: true
- python-version: 3.11.0-rc.1
os: macos-latest
experimental: true
fail-fast: False
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -30,32 +42,41 @@ jobs:
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -U codecov pytest-cov
python -W ignore -m pip install -r requirements.txt
python -W ignore -m pip install -r requirements-opts.txt
python -W ignore -m pip install -r requirements-dev.txt
- name: Test with pytest
# We run 3 different suites here
# We run 4 different suites here
# 1. Test just utils.datetime.py without pytz being installed
# 2. Test just test_no_passport.py without passport dependencies being installed
# 3. Test everything else
# 3. Test just test_rate_limiter.py without passport dependencies being installed
# 4. Test everything else
# The first & second one are achieved by mocking the corresponding import
# See test_helpers.py & test_no_passport.py for details
run: |
pytest -v --cov -k test_no_passport.py
no_passport_exit=$?
export TEST_NO_PASSPORT='false'
export TEST_PASSPORT='true'
pytest -v --cov --cov-append -k test_helpers.py
no_pytz_exit=$?
export TEST_NO_PYTZ='false'
export TEST_PYTZ='true'
pip uninstall aiolimiter -y
pytest -v --cov --cov-append -k test_ratelimiter.py
no_rate_limiter_exit=$?
export TEST_RATE_LIMITER='true'
pip install -r requirements-opts.txt
pytest -v --cov --cov-append
full_exit=$?
special_exit=$(( no_pytz_exit > no_passport_exit ? no_pytz_exit : no_passport_exit ))
special_exit=$(( special_exit > no_rate_limiter_exit ? special_exit : no_rate_limiter_exit ))
global_exit=$(( special_exit > full_exit ? special_exit : full_exit ))
exit ${global_exit}
env:
JOB_INDEX: ${{ strategy.job-index }}
BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzkwOTgzOTk3IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0OTY5MTc3NTAiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzMzODcxNDYxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzNjM5MzI1NzMiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDA3ODM2NjA1IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEyOTMwNzkxNjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9XQ==
TEST_NO_PYTZ : "true"
TEST_NO_PASSPORT: "true"
TEST_PYTZ : "false"
TEST_PASSPORT: "false"
TEST_RATE_LIMITER: "false"
TEST_BUILD: "true"
shell: bash --noprofile --norc {0}
@@ -76,13 +97,14 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -r requirements.txt
python -W ignore -m pip install -r requirements-opts.txt
python -W ignore -m pip install -r requirements-dev.txt
- name: Compare to official api
run: |
+11 -12
View File
@@ -1,6 +1,4 @@
# Make sure that
# * the revs specified here match requirements-dev.txt
# * the additional_dependencies here match requirements.txt
# Make sure that the additional_dependencies here match requirements.txt
ci:
autofix_prs: false
@@ -11,14 +9,14 @@ ci:
repos:
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 22.6.0
hooks:
- id: black
args:
- --diff
- --check
- repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1
- repo: https://github.com/PyCQA/flake8
rev: 5.0.2
hooks:
- id: flake8
- repo: https://github.com/PyCQA/pylint
@@ -34,25 +32,26 @@ repos:
additional_dependencies:
- httpx~=0.23.0
- tornado~=6.1
- tornado~=6.2
- APScheduler~=3.9.1
- cachetools~=5.2.0
- aiolimiter~=1.0.0
- . # this basically does `pip install -e .`
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.961
rev: v0.971
hooks:
- id: mypy
name: mypy-ptb
files: ^telegram/.*\.py$
additional_dependencies:
- types-ujson
- types-pytz
- types-cryptography
- types-cachetools
- httpx~=0.23.0
- tornado~=6.1
- tornado~=6.2
- APScheduler~=3.9.1
- cachetools~=5.2.0
- aiolimiter~=1.0.0
- . # this basically does `pip install -e .`
- id: mypy
name: mypy-examples
@@ -61,12 +60,12 @@ repos:
- --no-strict-optional
- --follow-imports=silent
additional_dependencies:
- tornado~=6.1
- tornado~=6.2
- APScheduler~=3.9.1
- cachetools~=5.2.0
- . # this basically does `pip install -e .`
- repo: https://github.com/asottile/pyupgrade
rev: v2.34.0
rev: v2.37.3
hooks:
- id: pyupgrade
files: ^(telegram|examples|tests)/.*\.py$
+1
View File
@@ -86,6 +86,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Paradox <https://github.com/paradox70>`_
- `Patrick Hofmann <https://github.com/PH89>`_
- `Paul Larsen <https://github.com/PaulSonOfLars>`_
- `Pawan <https://github.com/pawanrai9999>`_
- `Pieter Schutz <https://github.com/eldinnie>`_
- `Piraty <https://github.com/piraty>`_
- `Poolitzer <https://github.com/Poolitzer>`_
+84
View File
@@ -2,6 +2,90 @@
Changelog
=========
Version 20.0a4
==============
*Released 2022-08-27*
This is the technical changelog for version 20.0a4. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
Hot Fixes
---------
* Fix a Bug in ``setup.py`` Regarding Optional Dependencies (`#3209`_)
.. _`#3209`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3209
Version 20.0a3
==============
*Released 2022-08-27*
This is the technical changelog for version 20.0a3. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
Major Changes
-------------
- Full Support for API 6.2 (`#3195`_)
New Features
------------
- New Rate Limiting Mechanism (`#3148`_)
- Make ``chat/user_data`` Available in Error Handler for Errors in Jobs (`#3152`_)
- Add ``Application.post_shutdown`` (`#3126`_)
Bug Fixes
---------
- Fix ``helpers.mention_markdown`` for Markdown V1 and Improve Related Unit Tests (`#3155`_)
- Add ``api_kwargs`` Parameter to ``Bot.log_out`` and Improve Related Unit Tests (`#3147`_)
- Make ``Bot.delete_my_commands`` a Coroutine Function (`#3136`_)
- Fix ``ConversationHandler.check_update`` not respecting ``per_user`` (`#3128`_)
Minor Changes, Documentation Improvements and CI
------------------------------------------------
- Add Python 3.11 to Test Suite & Adapt Enum Behaviour (`#3168`_)
- Drop Manual Token Validation (`#3167`_)
- Simplify Unit Tests for ``Bot.send_chat_action`` (`#3151`_)
- Drop ``pre-commit`` Dependencies from ``requirements-dev.txt`` (`#3120`_)
- Change Default Values for ``concurrent_updates`` and ``connection_pool_size`` (`#3127`_)
- Documentation Improvements (`#3139`_, `#3153`_, `#3135`_)
- Type Hinting Fixes (`#3202`_)
Dependencies
------------
- Bump ``sphinx`` from 5.0.2 to 5.1.1 (`#3177`_)
- Update ``pre-commit`` Dependencies (`#3085`_)
- Bump ``pytest-asyncio`` from 0.18.3 to 0.19.0 (`#3158`_)
- Update ``tornado`` requirement from ~=6.1 to ~=6.2 (`#3149`_)
- Bump ``black`` from 22.3.0 to 22.6.0 (`#3132`_)
- Bump ``actions/setup-python`` from 3 to 4 (`#3131`_)
.. _`#3195`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3195
.. _`#3148`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3148
.. _`#3152`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3152
.. _`#3126`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3126
.. _`#3155`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3155
.. _`#3147`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3147
.. _`#3136`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3136
.. _`#3128`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3128
.. _`#3168`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3168
.. _`#3167`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3167
.. _`#3151`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3151
.. _`#3120`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3120
.. _`#3127`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3127
.. _`#3139`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3139
.. _`#3153`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3153
.. _`#3135`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3135
.. _`#3202`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3202
.. _`#3177`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3177
.. _`#3085`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3085
.. _`#3158`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3158
.. _`#3149`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3149
.. _`#3132`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3132
.. _`#3131`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3131
Version 20.0a2
==============
*Released 2022-06-27*
+1 -1
View File
@@ -1 +1 @@
include LICENSE LICENSE.lesser Makefile requirements.txt README_RAW.rst telegram/py.typed
include LICENSE LICENSE.lesser Makefile requirements.txt requirements-opts.txt README_RAW.rst telegram/py.typed
+4 -3
View File
@@ -14,7 +14,7 @@
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-6.1-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
@@ -93,7 +93,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support
====================
All types and methods of the Telegram Bot API **6.1** are supported.
All types and methods of the Telegram Bot API **6.2** are supported.
Installing
==========
@@ -122,7 +122,7 @@ However, for some features using a 3rd party library is more sane than implement
The dependencies are:
* `httpx ~= 0.23.0 <https://www.python-httpx.org>`_ for ``telegram.request.HTTPXRequest``, the default networking backend
* `tornado~=6.1 <https://www.tornadoweb.org/en/stable/>`_ for ``telegram.ext.Updater.start_webhook``
* `tornado~=6.2 <https://www.tornadoweb.org/en/stable/>`_ for ``telegram.ext.Updater.start_webhook``
* `cachetools~=5.2.0 <https://cachetools.readthedocs.io/en/latest/>`_ for ``telegram.ext.CallbackDataCache``
* `APScheduler~=3.9.1 <https://apscheduler.readthedocs.io/en/3.x/>`_ for ``telegram.ext.JobQueue``
@@ -138,6 +138,7 @@ PTB can be installed with optional dependencies:
* ``pip install python-telegram-bot[passport]`` installs the `cryptography>=3.0 <https://cryptography.io/en/stable>`_ library. Use this, if you want to use Telegram Passport related functionality.
* ``pip install python-telegram-bot[socks]`` installs ``httpx[socks]``. Use this, if you want to work behind a Socks5 server.
* ``pip install python-telegram-bot[rate-limiter]`` installs ``aiolimiter~=1.0.0``. Use this, if you want to use ``telegram.ext.AIORateLimiter``.
Quick Start
===========
+2 -2
View File
@@ -14,7 +14,7 @@
:target: https://pypi.org/project/python-telegram-bot-raw/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-6.1-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
@@ -89,7 +89,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support
====================
All types and methods of the Telegram Bot API **6.1** are supported.
All types and methods of the Telegram Bot API **6.2** are supported.
Installing
==========
+1 -1
View File
@@ -1,4 +1,4 @@
sphinx==5.0.2
sphinx==5.1.1
sphinx-pypi-upload
furo==2022.6.21
sphinx-paramlinks==0.5.4
+7 -3
View File
@@ -29,9 +29,9 @@ author = "Leandro Toledo"
# built documents.
#
# The short X.Y version.
version = "20.0a2" # telegram.__version__[:3]
version = "20.0a4" # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = "20.0a2" # telegram.__version__
release = "20.0a4" # telegram.__version__
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "4.5.0"
@@ -478,6 +478,10 @@ def autodoc_process_bases(app, name, obj, option, bases: list):
bases.insert(0, ":class:`str`")
continue
if "IntEnum" in base:
bases[idx] = ":class:`enum.IntEnum`"
continue
# Drop generics (at least for now)
if base.endswith("]"):
base = base.split("[", maxsplit=1)[0]
@@ -486,7 +490,7 @@ def autodoc_process_bases(app, name, obj, option, bases: list):
# Now convert `telegram._message.Message` to `telegram.Message` etc
match = re.search(pattern=r"(telegram(\.ext|))\.[_\w\.]+", string=base)
if not match or "_utils" in base:
return
continue
parts = match.group(0).split(".")
+1 -1
View File
@@ -49,7 +49,7 @@ this library, we introduced the
:class:`telegram.ext.ConversationHandler`
for that exact purpose. This example uses it to retrieve
user-information in a conversation-like style. To get a better
understanding, take a look at the :ref:`state diagrem <conversationbot-diagram>`.
understanding, take a look at the :ref:`state diagram <conversationbot-diagram>`.
:any:`examples.conversationbot2`
--------------------------------
@@ -206,6 +206,8 @@
- Used for getting a sticker set
* - :meth:`~telegram.Bot.upload_sticker_file`
- Used for uploading a sticker file
* - :meth:`~telegram.Bot.get_custom_emoji_stickers`
- Used for getting custom emoji files based on their IDs
.. raw:: html
+10
View File
@@ -0,0 +1,10 @@
.. tip::
When making requests to the Bot API in an asynchronous fashion (e.g. via
:attr:`block=False <telegram.ext.BaseHandler.block>`, :meth:`Application.create_task <telegram.ext.Application.create_task>`,
:meth:`~telegram.ext.ApplicationBuilder.concurrent_updates` or the :class:`~telegram.ext.JobQueue`), it can happen that more requests
are being made in parallel than there are connections in the pool.
If the number of requests is much higher than the number of connections, even setting
:meth:`~telegram.ext.ApplicationBuilder.pool_timeout` to a larger value may not always be enough to prevent pool
timeouts.
You should therefore set :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, :meth:`~telegram.ext.ApplicationBuilder.connection_pool_size` and
:meth:`~telegram.ext.ApplicationBuilder.pool_timeout` to values that make sense for your setup.
+7
View File
@@ -0,0 +1,7 @@
Arbitrary Callback Data
-----------------------
.. toctree::
telegram.ext.callbackdatacache
telegram.ext.invalidcallbackdata
@@ -0,0 +1,6 @@
telegram.ext.AIORateLimiter
============================
.. autoclass:: telegram.ext.AIORateLimiter
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
telegram.ext.BaseRateLimiter
============================
.. autoclass:: telegram.ext.BaseRateLimiter
:members:
:show-inheritance:
+1 -2
View File
@@ -3,5 +3,4 @@ telegram.ext.ExtBot
.. autoclass:: telegram.ext.ExtBot
:show-inheritance:
.. autofunction:: telegram.ext.ExtBot.insert_callback_data
:members: insert_callback_data, defaults, rate_limiter, initialize, shutdown
@@ -0,0 +1,23 @@
Handlers
--------
.. toctree::
telegram.ext.basehandler
telegram.ext.callbackqueryhandler
telegram.ext.chatjoinrequesthandler
telegram.ext.chatmemberhandler
telegram.ext.choseninlineresulthandler
telegram.ext.commandhandler
telegram.ext.conversationhandler
telegram.ext.filters
telegram.ext.inlinequeryhandler
telegram.ext.messagehandler
telegram.ext.pollanswerhandler
telegram.ext.pollhandler
telegram.ext.precheckoutqueryhandler
telegram.ext.prefixhandler
telegram.ext.shippingqueryhandler
telegram.ext.stringcommandhandler
telegram.ext.stringregexhandler
telegram.ext.typehandler
@@ -0,0 +1,9 @@
Persistence
-----------
.. toctree::
telegram.ext.basepersistence
telegram.ext.dictpersistence
telegram.ext.persistenceinput
telegram.ext.picklepersistence
@@ -0,0 +1,7 @@
Rate Limiting
-------------
.. toctree::
telegram.ext.baseratelimiter
telegram.ext.aioratelimiter
+4 -42
View File
@@ -13,45 +13,7 @@ telegram.ext package
telegram.ext.job
telegram.ext.jobqueue
telegram.ext.updater
Handlers
--------
.. toctree::
telegram.ext.basehandler
telegram.ext.callbackqueryhandler
telegram.ext.chatjoinrequesthandler
telegram.ext.chatmemberhandler
telegram.ext.choseninlineresulthandler
telegram.ext.commandhandler
telegram.ext.conversationhandler
telegram.ext.filters
telegram.ext.inlinequeryhandler
telegram.ext.messagehandler
telegram.ext.pollanswerhandler
telegram.ext.pollhandler
telegram.ext.precheckoutqueryhandler
telegram.ext.prefixhandler
telegram.ext.shippingqueryhandler
telegram.ext.stringcommandhandler
telegram.ext.stringregexhandler
telegram.ext.typehandler
Persistence
-----------
.. toctree::
telegram.ext.basepersistence
telegram.ext.dictpersistence
telegram.ext.persistenceinput
telegram.ext.picklepersistence
Arbitrary Callback Data
-----------------------
.. toctree::
telegram.ext.callbackdatacache
telegram.ext.invalidcallbackdata
telegram.ext.handlers-tree.rst
telegram.ext.persistence-tree.rst
telegram.ext.acd-tree.rst
telegram.ext.rate-limiting-tree.rst
+8
View File
@@ -0,0 +1,8 @@
Games
-----
.. toctree::
telegram.callbackgame
telegram.game
telegram.gamehighscore
+34
View File
@@ -0,0 +1,34 @@
Inline Mode
-----------
.. toctree::
telegram.choseninlineresult
telegram.inlinequery
telegram.inlinequeryresult
telegram.inlinequeryresultarticle
telegram.inlinequeryresultaudio
telegram.inlinequeryresultcachedaudio
telegram.inlinequeryresultcacheddocument
telegram.inlinequeryresultcachedgif
telegram.inlinequeryresultcachedmpeg4gif
telegram.inlinequeryresultcachedphoto
telegram.inlinequeryresultcachedsticker
telegram.inlinequeryresultcachedvideo
telegram.inlinequeryresultcachedvoice
telegram.inlinequeryresultcontact
telegram.inlinequeryresultdocument
telegram.inlinequeryresultgame
telegram.inlinequeryresultgif
telegram.inlinequeryresultlocation
telegram.inlinequeryresultmpeg4gif
telegram.inlinequeryresultphoto
telegram.inlinequeryresultvenue
telegram.inlinequeryresultvideo
telegram.inlinequeryresultvoice
telegram.inputmessagecontent
telegram.inputtextmessagecontent
telegram.inputlocationmessagecontent
telegram.inputvenuemessagecontent
telegram.inputcontactmessagecontent
telegram.inputinvoicemessagecontent
+27
View File
@@ -0,0 +1,27 @@
Passport
--------
.. toctree::
telegram.credentials
telegram.datacredentials
telegram.encryptedcredentials
telegram.encryptedpassportelement
telegram.filecredentials
telegram.iddocumentdata
telegram.passportdata
telegram.passportelementerror
telegram.passportelementerrordatafield
telegram.passportelementerrorfile
telegram.passportelementerrorfiles
telegram.passportelementerrorfrontside
telegram.passportelementerrorreverseside
telegram.passportelementerrorselfie
telegram.passportelementerrortranslationfile
telegram.passportelementerrortranslationfiles
telegram.passportelementerrorunspecified
telegram.passportfile
telegram.personaldetails
telegram.residentialaddress
telegram.securedata
telegram.securevalue
+13
View File
@@ -0,0 +1,13 @@
Payments
--------
.. toctree::
telegram.invoice
telegram.labeledprice
telegram.orderinfo
telegram.precheckoutquery
telegram.shippingaddress
telegram.shippingoption
telegram.shippingquery
telegram.successfulpayment
+5 -94
View File
@@ -89,98 +89,9 @@ Available Types
telegram.webappdata
telegram.webappinfo
telegram.webhookinfo
telegram.stickers-tree.rst
telegram.inline-tree.rst
telegram.payments-tree.rst
telegram.games-tree.rst
telegram.passport-tree.rst
Stickers
--------
.. toctree::
telegram.maskposition
telegram.sticker
telegram.stickerset
Inline Mode
-----------
.. toctree::
telegram.choseninlineresult
telegram.inlinequery
telegram.inlinequeryresult
telegram.inlinequeryresultarticle
telegram.inlinequeryresultaudio
telegram.inlinequeryresultcachedaudio
telegram.inlinequeryresultcacheddocument
telegram.inlinequeryresultcachedgif
telegram.inlinequeryresultcachedmpeg4gif
telegram.inlinequeryresultcachedphoto
telegram.inlinequeryresultcachedsticker
telegram.inlinequeryresultcachedvideo
telegram.inlinequeryresultcachedvoice
telegram.inlinequeryresultcontact
telegram.inlinequeryresultdocument
telegram.inlinequeryresultgame
telegram.inlinequeryresultgif
telegram.inlinequeryresultlocation
telegram.inlinequeryresultmpeg4gif
telegram.inlinequeryresultphoto
telegram.inlinequeryresultvenue
telegram.inlinequeryresultvideo
telegram.inlinequeryresultvoice
telegram.inputmessagecontent
telegram.inputtextmessagecontent
telegram.inputlocationmessagecontent
telegram.inputvenuemessagecontent
telegram.inputcontactmessagecontent
telegram.inputinvoicemessagecontent
Payments
--------
.. toctree::
telegram.invoice
telegram.labeledprice
telegram.orderinfo
telegram.precheckoutquery
telegram.shippingaddress
telegram.shippingoption
telegram.shippingquery
telegram.successfulpayment
Games
-----
.. toctree::
telegram.callbackgame
telegram.game
telegram.gamehighscore
Passport
--------
.. toctree::
telegram.credentials
telegram.datacredentials
telegram.encryptedcredentials
telegram.encryptedpassportelement
telegram.filecredentials
telegram.iddocumentdata
telegram.passportdata
telegram.passportelementerror
telegram.passportelementerrordatafield
telegram.passportelementerrorfile
telegram.passportelementerrorfiles
telegram.passportelementerrorfrontside
telegram.passportelementerrorreverseside
telegram.passportelementerrorselfie
telegram.passportelementerrortranslationfile
telegram.passportelementerrortranslationfiles
telegram.passportelementerrorunspecified
telegram.passportfile
telegram.personaldetails
telegram.residentialaddress
telegram.securedata
telegram.securevalue
+8
View File
@@ -0,0 +1,8 @@
Stickers
--------
.. toctree::
telegram.maskposition
telegram.sticker
telegram.stickerset
+4
View File
@@ -0,0 +1,4 @@
-r requirements.txt
-r requirements-dev.txt
-r requirements-opts.txt
-r docs/requirements-docs.txt
+1 -11
View File
@@ -1,17 +1,7 @@
# cryptography is an optional dependency, but running the tests properly requires it
cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3
pre-commit
# Make sure that the versions specified here match the pre-commit settings!
black==22.3.0
flake8==4.0.1
pylint==2.13.9
mypy==0.961
pyupgrade==2.34.0
isort==5.10.1
pytest==7.1.2
pytest-asyncio==0.18.3
pytest-asyncio==0.19.0
pytest-timeout==2.1.0 # used to timeout tests
flaky # Used for flaky tests (flaky decorator)
+7
View File
@@ -0,0 +1,7 @@
# Format:
# package_name==version # req-1, req-2, req-3!ext
# `pip install ptb-raw[req-1/2]` will install `package_name`
# `pip install ptb[req-1/2/3]` will also install `package_name`
httpx[socks] # socks
cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=3.0 # passport
aiolimiter~=1.0.0 # rate-limiter!ext
+1 -1
View File
@@ -10,7 +10,7 @@ httpx ~= 0.23.0
# only telegram.ext: # Keep this line here; used in setup(-raw).py
# tornado is rather stable, but let's not allow the next mayor release without prior testing
tornado~=6.1
tornado~=6.2
# Cachetools and APS don't have a strict stability policy.
# Let's be cautious for now.
+25 -5
View File
@@ -2,6 +2,7 @@
"""The setup and build script for the python-telegram-bot library."""
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from setuptools import find_packages, setup
@@ -35,6 +36,28 @@ def get_packages_requirements(raw=False):
return packs, reqs
def get_optional_requirements(raw=False):
"""Build the optional dependencies"""
requirements = defaultdict(list)
with Path("requirements-opts.txt").open() as reqs:
for line in reqs:
if line.startswith("#"):
continue
dependency, names = line.split("#")
dependency = dependency.strip()
for name in names.split(","):
name = name.strip()
if name.endswith("!ext"):
if raw:
continue
else:
name = name[:-4]
requirements[name].append(dependency)
return requirements
def get_setup_kwargs(raw=False):
"""Builds a dictionary of kwargs for the setup function"""
packages, requirements = get_packages_requirements(raw=raw)
@@ -69,11 +92,7 @@ def get_setup_kwargs(raw=False):
long_description_content_type="text/x-rst",
packages=packages,
install_requires=requirements,
extras_require={
"socks": "httpx[socks]",
# 3.4-3.4.3 contained some cyclical import bugs
"passport": "cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=3.0",
},
extras_require=get_optional_requirements(raw=raw),
include_package_data=True,
classifiers=[
"Development Status :: 5 - Production/Stable",
@@ -89,6 +108,7 @@ def get_setup_kwargs(raw=False):
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
],
python_requires=">=3.7",
)
+304 -58
View File
@@ -139,6 +139,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
serialized instance will not reflect that change. Trying to pickle a bot instance will
raise :exc:`pickle.PicklingError`.
.. seealso:: :attr:`telegram.ext.Application.bot`,
:attr:`telegram.ext.CallbackContext.bot`,
:attr:`telegram.ext.Updater.bot`
.. versionadded:: 13.2
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`bot` is equal.
@@ -172,7 +176,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data.
private_key_password (:obj:`bytes`, optional): Password for above private key.
.. include:: bot_methods.rst
.. include:: inclusions/bot_methods.rst
"""
@@ -197,7 +201,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
private_key: bytes = None,
private_key_password: bytes = None,
):
self.token = self._validate_token(token)
if not token:
raise InvalidToken("You must pass the token you received from https://t.me/Botfather!")
self.token = token
self.base_url = base_url + self.token
self.base_file_url = base_file_url + self.token
@@ -294,6 +300,25 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
# Drop any None values because Telegram doesn't handle them well
data = {key: value for key, value in data.items() if value is not None}
return await self._do_post(
endpoint=endpoint,
data=data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
)
async def _do_post(
self,
endpoint: str,
data: JSONDict,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
) -> Union[bool, JSONDict, None]:
# This also converts datetimes into timestamps.
# We don't do this earlier so that _insert_defaults (see above) has a chance to convert
# to the default timezone in case this is called by ExtBot
@@ -372,7 +397,12 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
return
await asyncio.gather(self._request[0].initialize(), self._request[1].initialize())
await self.get_me()
# Since the bot is to be initialized only once, we can also use it for
# verifying the token passed and raising an exception if it's invalid.
try:
await self.get_me()
except InvalidToken as exc:
raise InvalidToken(f"The token `{self.token}` was rejected by the server.") from exc
self._initialized = True
async def shutdown(self) -> None:
@@ -418,18 +448,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""
return self._request[1]
@staticmethod
def _validate_token(token: str) -> str:
"""A very basic validation on token."""
if any(x.isspace() for x in token):
raise InvalidToken()
left, sep, _right = token.partition(":")
if (not sep) or (not left.isdigit()) or (len(left) < 3):
raise InvalidToken()
return token
@property
def bot(self) -> User:
""":class:`telegram.User`: User instance for the bot as returned by :meth:`get_me`.
@@ -576,6 +594,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) -> Message:
"""Use this method to send text messages.
.. seealso:: :attr:`telegram.Message.reply_text`, :attr:`telegram.Chat.send_message`,
:attr:`telegram.User.send_message`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -669,16 +690,19 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Use this method to delete a message, including service messages, with the following
limitations:
- A message can only be deleted if it was sent less than 48 hours ago.
- A dice message in a private chat can only be deleted if it was sent more than 24
hours ago.
- Bots can delete outgoing messages in private chats, groups, and supergroups.
- Bots can delete incoming messages in private chats.
- Bots granted :attr:`~telegram.ChatMemberAdministrator.can_post_messages` permissions
can delete outgoing messages in channels.
- If the bot is an administrator of a group, it can delete any message there.
- If the bot has :attr:`~telegram.ChatMemberAdministrator.can_delete_messages`
permission in a supergroup or a channel, it can delete any message there.
- A message can only be deleted if it was sent less than 48 hours ago.
- A dice message in a private chat can only be deleted if it was sent more than 24
hours ago.
- Bots can delete outgoing messages in private chats, groups, and supergroups.
- Bots can delete incoming messages in private chats.
- Bots granted :attr:`~telegram.ChatMemberAdministrator.can_post_messages` permissions
can delete outgoing messages in channels.
- If the bot is an administrator of a group, it can delete any message there.
- If the bot has :attr:`~telegram.ChatMemberAdministrator.can_delete_messages`
permission in a supergroup or a channel, it can delete any message there.
.. seealso:: :meth:`telegram.Message.delete`,
:meth:`telegram.CallbackQuery.delete_message`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
@@ -746,6 +770,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
As a workaround, it is still possible to use :meth:`copy_message`. However, this
behaviour is undocumented and might be changed by Telegram.
.. seealso:: :attr:`telegram.Message.forward`, :attr:`telegram.Chat.forward_to`,
:attr:`telegram.Chat.forward_from`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -828,6 +855,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The photo argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
.. seealso:: :attr:`telegram.Message.reply_photo`, :attr:`telegram.Chat.send_photo`,
:attr:`telegram.User.send_photo`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -958,6 +988,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The audio argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
.. seealso:: :attr:`telegram.Message.reply_audio`, :attr:`telegram.Chat.send_audio`,
:attr:`telegram.User.send_audio`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -1105,6 +1138,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
* Sending by URL will currently only work ``GIF``, ``PDF`` & ``ZIP`` files.
.. seealso:: :attr:`telegram.Message.reply_document`, :attr:`telegram.Chat.send_document`,
:attr:`telegram.User.send_document`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -1233,6 +1269,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The :paramref:`sticker` argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
.. seealso:: :attr:`telegram.Message.reply_sticker`, :attr:`telegram.Chat.send_sticker`,
:attr:`telegram.User.send_sticker`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -1340,6 +1379,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
easily generate thumbnails. However, this behaviour is undocumented and might be
changed by Telegram.
.. seealso:: :attr:`telegram.Message.reply_video`, :attr:`telegram.Chat.send_video`,
:attr:`telegram.User.send_video`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -1486,6 +1528,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
easily generate thumbnails. However, this behaviour is undocumented and might be
changed by Telegram.
.. seealso:: :attr:`telegram.Message.reply_video_note`,
:attr:`telegram.Chat.send_video_note`,
:attr:`telegram.User.send_video_note`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -1616,6 +1662,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
generate thumb nails. However, this behaviour is undocumented and might be changed
by Telegram.
.. seealso:: :attr:`telegram.Message.reply_animation`,
:attr:`telegram.Chat.send_animation`,
:attr:`telegram.User.send_animation`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -1763,6 +1813,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
* To use this method, the file must have the type :mimetype:`audio/ogg` and be no more
than ``1MB`` in size. ``1-20MB`` voice notes will be sent as files.
.. seealso:: :attr:`telegram.Message.reply_voice`, :attr:`telegram.Chat.send_voice`,
:attr:`telegram.User.send_voice`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -1877,6 +1930,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) -> List[Message]:
"""Use this method to send a group of photos or videos as an album.
.. seealso:: :attr:`telegram.Message.reply_media_group`,
:attr:`telegram.Chat.send_media_group`,
:attr:`telegram.User.send_media_group`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -1968,6 +2025,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
You can either supply a :paramref:`latitude` and :paramref:`longitude` or a
:paramref:`location`.
.. seealso:: :attr:`telegram.Message.reply_location`, :attr:`telegram.Chat.send_location`,
:attr:`telegram.User.send_location`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -2069,7 +2129,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
self,
chat_id: Union[str, int] = None,
message_id: int = None,
inline_message_id: int = None,
inline_message_id: str = None,
latitude: float = None,
longitude: float = None,
reply_markup: InlineKeyboardMarkup = None,
@@ -2092,6 +2152,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
You can either supply a :paramref:`latitude` and :paramref:`longitude` or a
:paramref:`location`.
.. seealso:: :attr:`telegram.Message.edit_live_location`,
:attr:`telegram.CallbackQuery.edit_message_live_location`
Args:
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
specified. Unique identifier for the target chat or username of the target channel
@@ -2180,7 +2243,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
self,
chat_id: Union[str, int] = None,
message_id: int = None,
inline_message_id: int = None,
inline_message_id: str = None,
reply_markup: InlineKeyboardMarkup = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2192,6 +2255,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""Use this method to stop updating a live location message sent by the bot or via the bot
(for inline bots) before live_period expires.
.. seealso:: :attr:`telegram.Message.stop_live_location`
:attr:`telegram.CallbackQuery.stop_message_live_location`
Args:
chat_id (:obj:`int` | :obj:`str`): Required if inline_message_id is not specified.
Unique identifier for the target chat or username of the target channel
@@ -2278,6 +2344,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
* Foursquare details and Google Place details are mutually exclusive. However, this
behaviour is undocumented and might be changed by Telegram.
.. seealso:: :attr:`telegram.Message.reply_venue`, :attr:`telegram.Chat.send_venue`,
:attr:`telegram.User.send_venue`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -2416,6 +2485,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:paramref:`first_name` with optionally :paramref:`last_name` and optionally
:paramref:`vcard`.
.. seealso:: :attr:`telegram.Message.reply_contact`, :attr:`telegram.Chat.send_contact`,
:attr:`telegram.User.send_contact`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -2527,6 +2599,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) -> Message:
"""Use this method to send a game.
.. seealso:: :attr:`telegram.Message.reply_game`, :attr:`telegram.Chat.send_game`,
:attr:`telegram.User.send_game`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat.
game_short_name (:obj:`str`): Short name of the game, serves as the unique identifier
@@ -2604,6 +2679,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Telegram clients clear its typing status). Telegram only recommends using this method when
a response from the bot will take a noticeable amount of time to arrive.
.. seealso:: :attr:`telegram.Message.reply_chat_action`, :attr:`telegram.Chat.send_action`,
:attr:`telegram.User.send_chat_action`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -2753,6 +2831,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:paramref:`telegram.InlineQuery.answer.auto_pagination` set to :obj:`True`, which will
take care of passing the correct value.
.. seealso:: :attr:`telegram.InlineQuery.answer`
Args:
inline_query_id (:obj:`str`): Unique identifier for the answered query.
results (List[:class:`telegram.InlineQueryResult`] | Callable): A list of results for
@@ -2857,9 +2937,11 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> Optional[UserProfilePhotos]:
) -> UserProfilePhotos:
"""Use this method to get a list of profile pictures for a user.
.. seealso:: :meth:`telegram.User.get_profile_photos`
Args:
user_id (:obj:`int`): Unique identifier of the target user.
offset (:obj:`int`, optional): Sequential number of the first photo to be returned.
@@ -2907,7 +2989,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
api_kwargs=api_kwargs,
)
return UserProfilePhotos.de_json(result, self) # type: ignore[arg-type]
return UserProfilePhotos.de_json(result, self) # type: ignore[arg-type,return-value]
@_log
async def get_file(
@@ -3014,6 +3096,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
using invite links, etc., unless unbanned first. The bot must be an administrator in the
chat for this to work and must have the appropriate admin rights.
.. seealso:: :attr:`telegram.Chat.ban_member`
.. versionadded:: 13.7
Args:
@@ -3095,6 +3179,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
their channels**. The bot must be an administrator in the supergroup or channel for this
to work and must have the appropriate administrator rights.
.. seealso:: :attr:`telegram.Chat.ban_chat`, :attr:`telegram.Chat.ban_sender_chat`
.. versionadded:: 13.9
Args:
@@ -3160,6 +3246,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
join it. So if the user is a member of the chat they will also be *removed* from the chat.
If you don't want this, use the parameter :paramref:`only_if_banned`.
.. seealso:: :attr:`telegram.Chat.unban_member`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup or channel (in the format ``@channelusername``).
@@ -3222,6 +3310,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The bot must be an administrator for this to work and must have the
appropriate administrator rights.
.. seealso:: :attr:`telegram.Chat.unban_chat`
.. versionadded:: 13.9
Args:
@@ -3290,6 +3380,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
and accept the terms. Otherwise, you may use links like t.me/your_bot?start=XXXX that open
your bot with a parameter.
.. seealso:: :attr:`telegram.CallbackQuery.answer`
Args:
callback_query_id (:obj:`str`): Unique identifier for the query to be answered.
text (:obj:`str`, optional): Text of the notification. If not specified, nothing will
@@ -3359,7 +3451,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
text: str,
chat_id: Union[str, int] = None,
message_id: int = None,
inline_message_id: int = None,
inline_message_id: str = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
reply_markup: InlineKeyboardMarkup = None,
@@ -3378,6 +3470,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
It is currently only possible to edit messages without
:attr:`telegram.Message.reply_markup` or with inline keyboards.
.. seealso:: :attr:`telegram.Message.edit_text`,
:attr:`telegram.CallbackQuery.edit_message_text`
Args:
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
specified. Unique identifier for the target chat or username of the target channel
@@ -3474,6 +3569,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
It is currently only possible to edit messages without
:attr:`telegram.Message.reply_markup` or with inline keyboards
.. seealso:: :attr:`telegram.Message.edit_caption`,
:attr:`telegram.CallbackQuery.edit_message_caption`
Args:
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
specified. Unique identifier for the target chat or username of the target channel
@@ -3548,7 +3646,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
media: "InputMedia",
chat_id: Union[str, int] = None,
message_id: int = None,
inline_message_id: int = None,
inline_message_id: str = None,
reply_markup: InlineKeyboardMarkup = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3568,6 +3666,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
It is currently only possible to edit messages without
:attr:`telegram.Message.reply_markup` or with inline keyboards
.. seealso:: :attr:`telegram.Message.edit_media`,
:attr:`telegram.CallbackQuery.edit_message_media`
Args:
media (:class:`telegram.InputMedia`): An object for a new media content
of the message.
@@ -3629,7 +3730,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
self,
chat_id: Union[str, int] = None,
message_id: int = None,
inline_message_id: int = None,
inline_message_id: str = None,
reply_markup: Optional["InlineKeyboardMarkup"] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3646,6 +3747,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
It is currently only possible to edit messages without
:attr:`telegram.Message.reply_markup` or with inline keyboards
.. seealso:: :attr:`telegram.Message.edit_reply_markup`,
:attr:`telegram.CallbackQuery.edit_message_reply_markup`
Args:
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
specified. Unique identifier for the target chat or username of the target channel
@@ -4002,6 +4106,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) -> bool:
"""Use this method for your bot to leave a group, supergroup or channel.
.. seealso:: :attr:`telegram.Chat.leave`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup or channel (in the format ``@channelusername``).
@@ -4113,6 +4219,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""
Use this method to get a list of administrators in a chat.
.. seealso:: :attr:`telegram.Chat.get_administrators`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup or channel (in the format ``@channelusername``).
@@ -4168,6 +4276,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) -> int:
"""Use this method to get the number of members in a chat.
.. seealso:: :attr:`telegram.Chat.get_member_count`
.. versionadded:: 13.7
Args:
@@ -4223,6 +4333,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) -> ChatMember:
"""Use this method to get information about a member of a chat.
.. seealso:: :attr:`telegram.Chat.get_member`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup or channel (in the format ``@channelusername``).
@@ -4420,7 +4532,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
score: int,
chat_id: Union[str, int] = None,
message_id: int = None,
inline_message_id: int = None,
inline_message_id: str = None,
force: bool = None,
disable_edit_message: bool = None,
*,
@@ -4433,6 +4545,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""
Use this method to set the score of the specified user in a game message.
.. seealso::`telegram.CallbackQuery.set_game_score`
Args:
user_id (:obj:`int`): User identifier.
score (:obj:`int`): New score, must be non-negative.
@@ -4501,7 +4615,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
user_id: Union[int, str],
chat_id: Union[str, int] = None,
message_id: int = None,
inline_message_id: int = None,
inline_message_id: str = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -4518,6 +4632,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
closest neighbors on each side. Will also return the top three users if the user and
his neighbors are not among them. Please note that this behavior is subject to change.
.. seealso:: :attr:`telegram.CallbackQuery.get_game_high_scores`
Args:
user_id (:obj:`int`): Target user id.
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
@@ -4615,6 +4731,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
order of the arguments had to be changed. Use keyword arguments to make sure that the
arguments are passed correctly.
.. seealso:: :attr:`telegram.Message.reply_invoice`, :attr:`telegram.Chat.send_invoice`,
:attr:`telegram.User.send_invoice`
.. versionchanged:: 13.5
As of Bot API 5.2, the parameter :paramref:`start_parameter` is optional.
@@ -4798,6 +4917,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:class:`telegram.Update` with a :attr:`telegram.Update.shipping_query` field to the bot.
Use this method to reply to shipping queries.
.. seealso:: :attr:`telegram.ShippingQuery.answer`
Args:
shipping_query_id (:obj:`str`): Unique identifier for the query to be answered.
ok (:obj:`bool`): Specify :obj:`True` if delivery to the specified address is possible
@@ -4875,6 +4996,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The Bot API must receive an answer within 10 seconds after the pre-checkout
query was sent.
.. seealso:: :attr:`telegram.PreCheckoutQuery.answer`
Args:
pre_checkout_query_id (:obj:`str`): Unique identifier for the query to be answered.
ok (:obj:`bool`): Specify :obj:`True` if everything is alright
@@ -5005,7 +5128,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
the supergroup for this to work and must have the appropriate admin rights. Pass
:obj:`True` for all boolean parameters to lift restrictions from a user.
.. seealso:: :meth:`telegram.ChatPermissions.all_permissions`
.. seealso:: :meth:`telegram.ChatPermissions.all_permissions`,
:attr:`telegram.Chat.restrict_member`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
@@ -5092,6 +5216,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
an administrator in the chat for this to work and must have the appropriate admin rights.
Pass :obj:`False` for all boolean parameters to demote a user.
.. seealso:: :attr:`telegram.Chat.promote_member`
.. versionchanged:: 20.0
The argument ``can_manage_voice_chats`` was renamed to
:paramref:`can_manage_video_chats` in accordance to Bot API 6.0.
@@ -5210,6 +5336,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
administrator in the group or a supergroup for this to work and must have the
:attr:`telegram.ChatMemberAdministrator.can_restrict_members` admin rights.
.. seealso:: :attr:`telegram.Chat.set_permissions`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of
the target supergroup (in the format `@supergroupusername`).
@@ -5267,6 +5395,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Use this method to set a custom title for administrators promoted by the bot in a
supergroup. The bot must be an administrator for this to work.
.. seealso:: :attr:`telegram.Chat.set_administrator_custom_title`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of
the target supergroup (in the format `@supergroupusername`).
@@ -5327,6 +5457,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
link is revoked. The bot must be an administrator in the chat for this to work and must
have the appropriate admin rights.
.. seealso:: :attr:`telegram.Chat.export_invite_link`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -5393,6 +5525,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
administrator in the chat for this to work and must have the appropriate admin rights.
The link can be revoked using the method :meth:`revoke_chat_invite_link`.
.. seealso:: :attr:`telegram.Chat.create_invite_link`
.. versionadded:: 13.4
Args:
@@ -5491,6 +5625,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
parameters to the default values. However, since not documented, this behaviour may
change unbeknown to PTB.
.. seealso:: :attr:`telegram.Chat.edit_invite_link`
.. versionadded:: 13.4
Args:
@@ -5585,6 +5721,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
revoked, a new link is automatically generated. The bot must be an administrator in the
chat for this to work and must have the appropriate admin rights.
.. seealso:: :attr:`telegram.Chat.revoke_invite_link`
.. versionadded:: 13.4
Args:
@@ -5650,6 +5788,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The bot must be an administrator in the chat for this to work and must have the
:attr:`telegram.ChatPermissions.can_invite_users` administrator right.
.. seealso:: :attr:`telegram.Chat.approve_join_request`,
:attr:`telegram.ChatJoinRequest.approve`, :attr:`telegram.User.approve_join_request`
.. versionadded:: 13.8
Args:
@@ -5710,6 +5851,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The bot must be an administrator in the chat for this to work and must have the
:attr:`telegram.ChatPermissions.can_invite_users` administrator right.
.. seealso:: :attr:`telegram.Chat.decline_join_request`,
:attr:`telegram.ChatJoinRequest.decline`, :attr:`telegram.User.decline_join_request`
.. versionadded:: 13.8
Args:
@@ -5770,6 +5914,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Photos can't be changed for private chats. The bot must be an administrator in the chat
for this to work and must have the appropriate admin rights.
.. seealso:: :attr:`telegram.Chat.set_photo`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -5829,6 +5975,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
must be an administrator in the chat for this to work and must have the appropriate admin
rights.
.. seealso:: :attr:`telegram.Chat.delete_photo`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -5885,6 +6033,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The bot must be an administrator in the chat for this to work and must have the appropriate
admin rights.
.. seealso:: :attr:`telegram.Chat.set_title`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -5942,6 +6092,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
must be an administrator in the chat for this to work and must have the appropriate admin
rights.
.. seealso:: :attr:`telegram.Chat.set_description`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -6005,6 +6157,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages` admin
right in a channel.
.. seealso:: :attr:`telegram.Chat.pin_message`, :attr:`telegram.User.pin_message`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -6071,6 +6225,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages` admin
right in a channel.
.. seealso:: :attr:`telegram.Chat.unpin_message`, :attr:`telegram.User.unpin_message`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -6133,6 +6289,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
admin right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages`
admin right in a channel.
.. seealso:: :attr:`telegram.Chat.unpin_all_messages`,
:attr:`telegram.User.unpin_all_messages`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -6222,6 +6381,61 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
)
return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type]
@_log
async def get_custom_emoji_stickers(
self,
custom_emoji_ids: List[str],
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> List[Sticker]:
# skipcq: FLK-D207
"""
Use this method to get information about emoji stickers by their identifiers.
Args:
custom_emoji_ids (List[:obj:`str`]): List of custom emoji identifiers.
At most :tg-const:`telegram.constants.CustomEmojiStickerLimit.\
CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Keyword Args:
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
List[:class:`telegram.Sticker`]
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"custom_emoji_ids": custom_emoji_ids}
result = await self._post(
"getCustomEmojiStickers",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
return Sticker.de_list(result, self) # type: ignore[return-value, arg-type]
@_log
async def upload_sticker_file(
self,
@@ -6295,10 +6509,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
title: str,
emojis: str,
png_sticker: FileInput = None,
contains_masks: bool = None,
mask_position: MaskPosition = None,
tgs_sticker: FileInput = None,
webm_sticker: FileInput = None,
sticker_type: str = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -6309,8 +6523,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""
Use this method to create new sticker set owned by a user.
The bot will be able to edit the created sticker set.
You must use exactly one of the fields :paramref:`png_sticker`, :paramref:`tgs_sticker`, or
:paramref:`webm_sticker`.
You must use exactly one of the fields :paramref:`png_sticker`, :paramref:`tgs_sticker`,
or :paramref:`webm_sticker`.
Warning:
As of API 4.7 :paramref:`png_sticker` is an optional argument and therefore the order
@@ -6321,6 +6535,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
The :paramref:`png_sticker` and :paramref:`tgs_sticker` argument can be either a
file_id, an URL or a file from disk ``open(filename, 'rb')``
.. versionchanged:: 20.0
The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type`
instead.
Args:
user_id (:obj:`int`): User identifier of created sticker set owner.
name (:obj:`str`): Short name of sticker set, to be used in t.me/addstickers/ URLs
@@ -6354,10 +6572,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
.. versionadded:: 13.11
emojis (:obj:`str`): One or more emoji corresponding to the sticker.
contains_masks (:obj:`bool`, optional): Pass :obj:`True`, if a set of mask stickers
should be created.
mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask
should be placed on faces.
sticker_type (:obj:`str`, optional): Type of stickers in the set, pass
:attr:`telegram.Sticker.REGULAR` or :attr:`telegram.Sticker.MASK`. Custom emoji
sticker sets can't be created via the Bot API at the moment. By default, a
regular sticker set is created.
.. versionadded:: 20.0
Keyword Args:
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
@@ -6390,10 +6612,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
data["tgs_sticker"] = parse_file_input(tgs_sticker)
if webm_sticker is not None:
data["webm_sticker"] = parse_file_input(webm_sticker)
if contains_masks is not None:
data["contains_masks"] = contains_masks
if mask_position is not None:
data["mask_position"] = mask_position
if sticker_type is not None:
data["sticker_type"] = sticker_type
result = await self._post(
"createNewStickerSet",
@@ -6793,6 +7015,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""
Use this method to send a native poll.
.. seealso:: :attr:`telegram.Message.reply_poll`, :attr:`telegram.Chat.send_poll`,
:attr:`telegram.User.send_poll`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -6925,6 +7150,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""
Use this method to stop a poll which was sent by the bot.
.. seealso:: :attr:`telegram.Message.stop_poll`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -6991,9 +7218,20 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""
Use this method to send an animated emoji that will display a random value.
.. seealso:: :attr:`telegram.Message.reply_dice`, :attr:`telegram.Chat.send_dice`,
:attr:`telegram.User.send_dice`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user
emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based.
Currently, must be one of :class:`telegram.constants.DiceEmoji`. Dice can have
values 1-6 for :tg-const:`telegram.constants.DiceEmoji.DICE`,
@@ -7005,23 +7243,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:tg-const:`telegram.constants.DiceEmoji.DICE`.
.. versionchanged:: 13.4
Added the :tg-const:`telegram.constants.DiceEmoji.BOWLING` emoji.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
Added the :tg-const:`telegram.constants.DiceEmoji.BOWLING` emoji..
allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
should be sent even if the specified replied-to message is not found.
protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
forwarding and saving.
.. versionadded:: 13.10
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
should be sent even if the specified replied-to message is not found.
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user.
Keyword Args:
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to
@@ -7345,7 +7574,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
return result # type: ignore[return-value]
@_log
def delete_my_commands(
async def delete_my_commands(
self,
scope: BotCommandScope = None,
language_code: str = None,
@@ -7402,7 +7631,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
if language_code:
data["language_code"] = language_code
result = self._post(
result = await self._post(
"deleteMyCommands",
data,
read_timeout=read_timeout,
@@ -7422,6 +7651,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""
Use this method to log out from the cloud Bot API server before launching the bot locally.
@@ -7443,6 +7673,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
.. versionadded:: 20.0
Returns:
:obj:`True`: On success
@@ -7457,6 +7691,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
@_log
@@ -7533,6 +7768,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
be copied. The method is analogous to the method :meth:`forward_message`, but the copied
message doesn't have a link to the original message.
.. seealso:: :attr:`telegram.Message.copy`, :attr:`telegram.Chat.send_copy`,
:attr:`telegram.Chat.copy_message`, :attr:`telegram.User.send_copy`,
:attr:`telegram.User.copy_message`
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -7584,6 +7823,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"chat_id": chat_id,
@@ -7630,7 +7870,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
button.
.. seealso:: :meth:`get_chat_menu_button`, :meth:`telegram.Chat.set_menu_button`,
:meth:`telegram.User.set_menu_button`
:meth:`telegram.Chat.get_menu_button`, meth:`telegram.User.set_menu_button`,
:meth:`telegram.User.get_menu_button`
.. versionadded:: 20.0
@@ -7658,6 +7899,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
data: JSONDict = {}
if chat_id is not None:
@@ -7690,7 +7932,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
the default menu button.
.. seealso:: :meth:`set_chat_menu_button`, :meth:`telegram.Chat.get_menu_button`,
:meth:`telegram.User.get_menu_button`
:meth:`telegram.Chat.set_menu_button`, :meth:`telegram.User.get_menu_button`,
:meth:`telegram.User.set_menu_button`
.. versionadded:: 20.0
@@ -7716,6 +7959,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Returns:
:class:`telegram.MenuButton`: On success, the current menu button is returned.
"""
data = {}
if chat_id is not None:
@@ -8033,6 +8277,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""Alias for :meth:`unpin_chat_message`"""
unpinAllChatMessages = unpin_all_chat_messages
"""Alias for :meth:`unpin_all_chat_messages`"""
getCustomEmojiStickers = get_custom_emoji_stickers
"""Alias for :meth:`get_custom_emoji_stickers`"""
getStickerSet = get_sticker_set
"""Alias for :meth:`get_sticker_set`"""
uploadStickerFile = upload_sticker_file
+16 -2
View File
@@ -134,6 +134,12 @@ class Chat(TelegramObject):
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
has_restricted_voice_and_video_messages (:obj:`bool`, optional): :obj:`True`, if the
privacy settings of the other party restrict sending voice and video note messages
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes:
@@ -187,6 +193,11 @@ class Chat(TelegramObject):
joining the supergroup need to be approved by supergroup administrators. Returned only
in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
has_restricted_voice_and_video_messages (:obj:`bool`): Optional. :obj:`True`, if the
privacy settings of the other party restrict sending voice and video note messages
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
"""
@@ -215,6 +226,7 @@ class Chat(TelegramObject):
"has_private_forwards",
"join_to_send_messages",
"join_by_request",
"has_restricted_voice_and_video_messages",
)
SENDER: ClassVar[str] = constants.ChatType.SENDER
@@ -256,6 +268,7 @@ class Chat(TelegramObject):
has_protected_content: bool = None,
join_to_send_messages: bool = None,
join_by_request: bool = None,
has_restricted_voice_and_video_messages: bool = None,
**_kwargs: Any,
):
# Required
@@ -286,6 +299,7 @@ class Chat(TelegramObject):
self.location = location
self.join_to_send_messages = join_to_send_messages
self.join_by_request = join_by_request
self.has_restricted_voice_and_video_messages = has_restricted_voice_and_video_messages
self.set_bot(bot)
self._id_attrs = (self.id,)
@@ -2375,7 +2389,7 @@ class Chat(TelegramObject):
Caution:
Can only work, if the chat is a private chat.
..seealso:: :meth:`get_menu_button`
.. seealso:: :meth:`get_menu_button`
.. versionadded:: 20.0
@@ -2411,7 +2425,7 @@ class Chat(TelegramObject):
Caution:
Can only work, if the chat is a private chat.
..seealso:: :meth:`set_menu_button`
.. seealso:: :meth:`set_menu_button`
.. versionadded:: 20.0
+2
View File
@@ -44,6 +44,8 @@ class ChatMember(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`user` and :attr:`status` are equal.
.. seealso:: `Chat Member Example <examples.chatmemberbot.html>`_
.. versionchanged:: 20.0
* As of Bot API 5.3, :class:`ChatMember` is nothing but the base class for the subclasses
+5 -5
View File
@@ -44,7 +44,7 @@ class File(TelegramObject):
Note:
* Maximum file size to download is
:tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD`.
:tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD`.
* If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`,
then it will automatically be decrypted as it downloads when you call :meth:`download()`.
@@ -111,7 +111,7 @@ class File(TelegramObject):
original filename as reported by Telegram. If the file has no filename, it the file ID will
be used as filename. If a :paramref:`custom_path` is supplied, it will be saved to that
path instead. If :paramref:`out` is defined, the file contents will be saved to that object
using the ``out.write`` method.
using the :obj:`out.write<io.BufferedWriter.write>` method.
Note:
* :paramref:`custom_path` and :paramref:`out` are mutually exclusive.
@@ -144,8 +144,8 @@ class File(TelegramObject):
Returns:
:class:`pathlib.Path` | :obj:`io.BufferedWriter`: The same object as :paramref:`out` if
specified. Otherwise, returns the filename downloaded to or the file path of the
local file.
specified. Otherwise, returns the filename downloaded to or the file path of the
local file.
Raises:
ValueError: If both :paramref:`custom_path` and :paramref:`out` are passed.
@@ -214,7 +214,7 @@ class File(TelegramObject):
Returns:
:obj:`bytearray`: The same object as :paramref:`buf` if it was specified. Otherwise a
newly allocated :obj:`bytearray`.
newly allocated :obj:`bytearray`.
"""
if buf is None:
+48 -9
View File
@@ -54,6 +54,11 @@ class Sticker(_BaseThumbedMedium):
is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker.
.. versionadded:: 13.11
type (:obj:`str`): Type of the sticker. Currently one of :attr:`REGULAR`,
:attr:`MASK`, :attr:`CUSTOM_EMOJI`. The type of the sticker is independent from its
format, which is determined by the fields :attr:`is_animated` and :attr:`is_video`.
.. versionadded:: 20.0
thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the ``.WEBP`` or
``.JPG`` format.
emoji (:obj:`str`, optional): Emoji associated with the sticker
@@ -63,8 +68,12 @@ class Sticker(_BaseThumbedMedium):
position where the mask should be placed.
file_size (:obj:`int`, optional): File size in bytes.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
premium_animation (:class:`telegram.File`, optional): Premium animation for the sticker,
if the sticker is premium.
premium_animation (:class:`telegram.File`, optional): For premium regular stickers,
premium animation for the sticker.
.. versionadded:: 20.0
custom_emoji (:obj:`str`, optional): For custom emoji stickers, unique identifier of the
custom emoji.
.. versionadded:: 20.0
_kwargs (:obj:`dict`): Arbitrary keyword arguments.
@@ -80,6 +89,11 @@ class Sticker(_BaseThumbedMedium):
is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker.
.. versionadded:: 13.11
type (:obj:`str`): Type of the sticker. Currently one of :attr:`REGULAR`,
:attr:`MASK`, :attr:`CUSTOM_EMOJI`. The type of the sticker is independent from its
format, which is determined by the fields :attr:`is_animated` and :attr:`is_video`.
.. versionadded:: 20.0
thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the ``.WEBP`` or
``.JPG`` format.
emoji (:obj:`str`): Optional. Emoji associated with the sticker.
@@ -88,11 +102,14 @@ class Sticker(_BaseThumbedMedium):
where the mask should be placed.
file_size (:obj:`int`): Optional. File size in bytes.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
premium_animation (:class:`telegram.File`): Optional. Premium animation for the
sticker, if the sticker is premium.
premium_animation (:class:`telegram.File`): Optional. For premium regular stickers,
premium animation for the sticker.
.. versionadded:: 20.0
custom_emoji (:obj:`str`): Optional. For custom emoji stickers, unique identifier of the
custom emoji.
.. versionadded:: 20.0
"""
__slots__ = (
@@ -104,6 +121,8 @@ class Sticker(_BaseThumbedMedium):
"set_name",
"width",
"premium_animation",
"type",
"custom_emoji_id",
)
def __init__(
@@ -114,6 +133,7 @@ class Sticker(_BaseThumbedMedium):
height: int,
is_animated: bool,
is_video: bool,
type: str, # pylint: disable=redefined-builtin
thumb: PhotoSize = None,
emoji: str = None,
file_size: int = None,
@@ -121,6 +141,7 @@ class Sticker(_BaseThumbedMedium):
mask_position: "MaskPosition" = None,
bot: "Bot" = None,
premium_animation: "File" = None,
custom_emoji_id: str = None,
**_kwargs: Any,
):
super().__init__(
@@ -135,11 +156,20 @@ class Sticker(_BaseThumbedMedium):
self.height = height
self.is_animated = is_animated
self.is_video = is_video
self.type = type
# Optional
self.emoji = emoji
self.set_name = set_name
self.mask_position = mask_position
self.premium_animation = premium_animation
self.custom_emoji_id = custom_emoji_id
REGULAR: ClassVar[str] = constants.StickerType.REGULAR
""":const:`telegram.constants.StickerType.REGULAR`"""
MASK: ClassVar[str] = constants.StickerType.MASK
""":const:`telegram.constants.StickerType.MASK`"""
CUSTOM_EMOJI: ClassVar[str] = constants.StickerType.CUSTOM_EMOJI
""":const:`telegram.constants.StickerType.CUSTOM_EMOJI`"""
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Sticker"]:
@@ -167,6 +197,9 @@ class StickerSet(TelegramObject):
arguments had to be changed. Use keyword arguments to make sure that the arguments are
passed correctly.
.. versionchanged:: 20.0:
The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead.
Args:
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
@@ -174,8 +207,12 @@ class StickerSet(TelegramObject):
is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
.. versionadded:: 13.11
contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
sticker_type (:obj:`str`): Type of stickers in the set, currently one of
:attr:`telegram.Sticker.REGULAR`, :attr:`telegram.Sticker.MASK`,
:attr:`telegram.Sticker.CUSTOM_EMOJI`.
.. versionadded:: 20.0
thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the ``.WEBP``,
``.TGS``, or ``.WEBM`` format.
@@ -186,21 +223,23 @@ class StickerSet(TelegramObject):
is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
.. versionadded:: 13.11
contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
sticker_type (:obj:`str`): Type of stickers in the set.
.. versionadded:: 20.0
thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the ``.WEBP``,
``.TGS`` or ``.WEBM`` format.
"""
__slots__ = (
"contains_masks",
"is_animated",
"is_video",
"name",
"stickers",
"thumb",
"title",
"sticker_type",
)
def __init__(
@@ -208,9 +247,9 @@ class StickerSet(TelegramObject):
name: str,
title: str,
is_animated: bool,
contains_masks: bool,
stickers: List[Sticker],
is_video: bool,
sticker_type: str,
thumb: PhotoSize = None,
**_kwargs: Any,
):
@@ -218,8 +257,8 @@ class StickerSet(TelegramObject):
self.title = title
self.is_animated = is_animated
self.is_video = is_video
self.contains_masks = contains_masks
self.stickers = stickers
self.sticker_type = sticker_type
# Optional
self.thumb = thumb
+4
View File
@@ -62,6 +62,10 @@ class InlineKeyboardButton(TelegramObject):
* After Bot API 6.1, only ``HTTPS`` links will be allowed in :paramref:`login_url`.
.. seealso:: `Inline Keyboard Example 1 <examples.inlinekeyboard.html>`_,
`Inline Keyboard Example 2 <examples.inlinekeyboard2.html>`_,
:class:`telegram.InlineKeyboardMarkup`
.. versionchanged:: 20.0
:attr:`web_app` is considered as well when comparing objects of this type in terms of
equality.
+3
View File
@@ -36,6 +36,9 @@ class InlineKeyboardMarkup(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their size of :attr:`inline_keyboard` and all the buttons are equal.
.. seealso:: `Inline Keyboard Example 1 <examples.inlinekeyboard.html>`_,
`Inline Keyboard Example 2 <examples.inlinekeyboard2.html>`_
Args:
inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows,
each represented by a list of InlineKeyboardButton objects.
@@ -31,6 +31,8 @@ if TYPE_CHECKING:
class InlineQueryResultArticle(InlineQueryResult):
"""This object represents a Telegram InlineQueryResultArticle.
.. seealso:: `Inline Example <examples.inlinebot.html>`_
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
title (:obj:`str`): Title of the result.
@@ -33,6 +33,8 @@ class InputTextMessageContent(InputMessageContent):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`message_text` is equal.
.. seealso:: `Inline Example <examples.inlinebot.html>`_
Args:
message_text (:obj:`str`): Text of the message to be sent,
1-:tg-const:`telegram.constants.MessageLimit.TEXT_LENGTH` characters after entities
+2
View File
@@ -29,6 +29,8 @@ class KeyboardButtonPollType(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal.
.. seealso:: `Pollbot Example <examples.pollbot.html>`_
Attributes:
type (:obj:`str`): Optional. If :tg-const:`telegram.Poll.QUIZ` is passed, the user will be
allowed to create only polls in the quiz mode. If :tg-const:`telegram.Poll.REGULAR` is
+34
View File
@@ -370,6 +370,8 @@ class Message(TelegramObject):
to the message.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
.. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored
by this function. Instead, the supplied replacement for the emoji will be used.
"""
# fmt: on
@@ -2864,6 +2866,9 @@ class Message(TelegramObject):
Use this if you want to retrieve the message text with the entities formatted as HTML in
the same way the original message was formatted.
Note:
|custom_emoji_formatting_note|
.. versionchanged:: 13.10
Spoiler entities are now formatted as HTML.
@@ -2880,6 +2885,9 @@ class Message(TelegramObject):
Use this if you want to retrieve the message text with the entities formatted as HTML.
This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
Note:
|custom_emoji_formatting_note|
.. versionchanged:: 13.10
Spoiler entities are now formatted as HTML.
@@ -2897,6 +2905,9 @@ class Message(TelegramObject):
Use this if you want to retrieve the message caption with the caption entities formatted as
HTML in the same way the original message was formatted.
Note:
|custom_emoji_formatting_note|
.. versionchanged:: 13.10
Spoiler entities are now formatted as HTML.
@@ -2913,6 +2924,9 @@ class Message(TelegramObject):
Use this if you want to retrieve the message caption with the caption entities formatted as
HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
Note:
|custom_emoji_formatting_note|
.. versionchanged:: 13.10
Spoiler entities are now formatted as HTML.
@@ -3093,6 +3107,8 @@ class Message(TelegramObject):
:tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by
Telegram for backward compatibility. You should use :meth:`text_markdown_v2` instead.
|custom_emoji_formatting_note|
Returns:
:obj:`str`: Message text with entities formatted as Markdown.
@@ -3111,6 +3127,9 @@ class Message(TelegramObject):
Use this if you want to retrieve the message text with the entities formatted as Markdown
in the same way the original message was formatted.
Note:
|custom_emoji_formatting_note|
.. versionchanged:: 13.10
Spoiler entities are now formatted as Markdown V2.
@@ -3132,6 +3151,8 @@ class Message(TelegramObject):
Telegram for backward compatibility. You should use :meth:`text_markdown_v2_urled`
instead.
|custom_emoji_formatting_note|
Returns:
:obj:`str`: Message text with entities formatted as Markdown.
@@ -3150,6 +3171,9 @@ class Message(TelegramObject):
Use this if you want to retrieve the message text with the entities formatted as Markdown.
This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
Note:
|custom_emoji_formatting_note|
.. versionchanged:: 13.10
Spoiler entities are now formatted as Markdown V2.
@@ -3171,6 +3195,8 @@ class Message(TelegramObject):
Telegram for backward compatibility. You should use :meth:`caption_markdown_v2`
instead.
|custom_emoji_formatting_note|
Returns:
:obj:`str`: Message caption with caption entities formatted as Markdown.
@@ -3189,6 +3215,9 @@ class Message(TelegramObject):
Use this if you want to retrieve the message caption with the caption entities formatted as
Markdown in the same way the original message was formatted.
Note:
|custom_emoji_formatting_note|
.. versionchanged:: 13.10
Spoiler entities are now formatted as Markdown V2.
@@ -3212,6 +3241,8 @@ class Message(TelegramObject):
Telegram for backward compatibility. You should use :meth:`caption_markdown_v2_urled`
instead.
|custom_emoji_formatting_note|
Returns:
:obj:`str`: Message caption with caption entities formatted as Markdown.
@@ -3230,6 +3261,9 @@ class Message(TelegramObject):
Use this if you want to retrieve the message caption with the caption entities formatted as
Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
Note:
|custom_emoji_formatting_note|
.. versionchanged:: 13.10
Spoiler entities are now formatted as Markdown V2.
+21 -2
View File
@@ -44,7 +44,11 @@ class MessageEntity(TelegramObject):
:attr:`URL`, :attr:`EMAIL`, :attr:`PHONE_NUMBER`, :attr:`BOLD` (bold text),
:attr:`ITALIC` (italic text), :attr:`STRIKETHROUGH`, :attr:`SPOILER` (spoiler message),
:attr:`CODE` (monowidth string), :attr:`PRE` (monowidth block), :attr:`TEXT_LINK` (for
clickable text URLs), :attr:`TEXT_MENTION` (for users without usernames).
clickable text URLs), :attr:`TEXT_MENTION` (for users without usernames),
:attr:`CUSTOM_EMOJI` (for inline custom emoji stickers).
.. versionadded:: 20.0
added inline custom emoji
offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity.
length (:obj:`int`): Length of the entity in UTF-16 code units.
url (:obj:`str`, optional): For :attr:`TEXT_LINK` only, url that will be opened after
@@ -53,6 +57,11 @@ class MessageEntity(TelegramObject):
user.
language (:obj:`str`, optional): For :attr:`PRE` only, the programming language of
the entity text.
custom_emoji_id (:obj:`str`, optional): For :attr:`CUSTOM_EMOJI` only, unique identifier
of the custom emoji. Use :meth:`telegram.Bot.get_custom_emoji_stickers` to get full
information about the sticker.
.. versionadded:: 20.0
Attributes:
type (:obj:`str`): Type of the entity.
@@ -61,10 +70,13 @@ class MessageEntity(TelegramObject):
url (:obj:`str`): Optional. Url that will be opened after user taps on the text.
user (:class:`telegram.User`): Optional. The mentioned user.
language (:obj:`str`): Optional. Programming language of the entity text.
custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji.
.. versionadded:: 20.0
"""
__slots__ = ("length", "url", "user", "type", "language", "offset")
__slots__ = ("length", "url", "user", "type", "language", "offset", "custom_emoji_id")
def __init__(
self,
@@ -74,6 +86,7 @@ class MessageEntity(TelegramObject):
url: str = None,
user: User = None,
language: str = None,
custom_emoji_id: str = None,
**_kwargs: Any,
):
# Required
@@ -84,6 +97,7 @@ class MessageEntity(TelegramObject):
self.url = url
self.user = user
self.language = language
self.custom_emoji_id = custom_emoji_id
self._id_attrs = (self.type, self.offset, self.length)
@@ -134,5 +148,10 @@ class MessageEntity(TelegramObject):
.. versionadded:: 13.10
"""
CUSTOM_EMOJI: ClassVar[str] = constants.MessageEntityType.CUSTOM_EMOJI
""":const:`telegram.constants.MessageEntityType.CUSTOM_EMOJI`
.. versionadded:: 20.0
"""
ALL_TYPES: ClassVar[List[str]] = list(constants.MessageEntityType)
"""List[:obj:`str`]: A list of all available message entity types."""
+2
View File
@@ -29,6 +29,8 @@ class LabeledPrice(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`label` and :attr:`amount` are equal.
.. seealso:: `Paymentbot Example <examples.paymentbot.html>`_
Args:
label (:obj:`str`): Portion label.
amount (:obj:`int`): Price of the product in the smallest units of the currency (integer,
+2
View File
@@ -33,6 +33,8 @@ class ShippingOption(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id` is equal.
.. seealso:: `Paymentbot Example <examples.paymentbot.html>`_
Args:
id (:obj:`str`): Shipping option identifier.
title (:obj:`str`): Option title.
+2
View File
@@ -112,6 +112,8 @@ class Poll(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id` is equal.
.. seealso:: `Pollbot Example <examples.pollbot.html>`_
Args:
id (:obj:`str`): Unique poll identifier.
question (:obj:`str`): Poll question, 1-300 characters.
+2 -2
View File
@@ -1463,7 +1463,7 @@ class User(TelegramObject):
For the documentation of the arguments, please see
:meth:`telegram.Bot.set_chat_menu_button`.
..seealso:: :meth:`get_menu_button`
.. seealso:: :meth:`get_menu_button`
.. versionadded:: 20.0
@@ -1496,7 +1496,7 @@ class User(TelegramObject):
For the documentation of the arguments, please see
:meth:`telegram.Bot.get_chat_menu_button`.
..seealso:: :meth:`set_menu_button`
.. seealso:: :meth:`set_menu_button`
.. versionadded:: 20.0
+32 -8
View File
@@ -23,30 +23,54 @@ Warning:
user. Changes to this module are not considered breaking changes and may not be documented in
the changelog.
"""
from enum import Enum
import enum as _enum
import sys
from typing import Type, TypeVar, Union
_A = TypeVar("_A")
_B = TypeVar("_B")
_Enum = TypeVar("_Enum", bound=Enum)
_Enum = TypeVar("_Enum", bound=_enum.Enum)
def get_member(enum: Type[_Enum], value: _A, default: _B) -> Union[_Enum, _A, _B]:
"""Tries to call ``enum(value)`` to convert the value into an enumeration member.
def get_member(enum_cls: Type[_Enum], value: _A, default: _B) -> Union[_Enum, _A, _B]:
"""Tries to call ``enum_cls(value)`` to convert the value into an enumeration member.
If that fails, the ``default`` is returned.
"""
try:
return enum(value)
return enum_cls(value)
except ValueError:
return default
class StringEnum(str, Enum):
"""Helper class for string enums where the value is not important to be displayed on
stringification.
# Python 3.11 and above has a different output for mixin classes for IntEnum, StrEnum and IntFlag
# see https://docs.python.org/3.11/library/enum.html#notes. We want e.g. str(StrEnumTest.FOO) to
# return "foo" instead of "StrEnumTest.FOO", which is not the case < py3.11
class StringEnum(str, _enum.Enum):
"""Helper class for string enums where ``str(member)`` prints the value, but ``repr(member)``
gives ``EnumName.MEMBER_NAME``.
"""
__slots__ = ()
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
def __str__(self) -> str:
return str.__str__(self)
# Apply the __repr__ modification and __str__ fix to IntEnum
class IntEnum(_enum.IntEnum):
"""Helper class for int enums where ``str(member)`` prints the value, but ``repr(member)``
gives ``EnumName.MEMBER_NAME``.
"""
__slots__ = ()
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
if sys.version_info < (3, 11):
def __str__(self) -> str:
return str(self.value)
+1 -1
View File
@@ -50,7 +50,7 @@ class Version(NamedTuple):
return version
__version_info__ = Version(major=20, minor=0, micro=0, releaselevel="alpha", serial=2)
__version_info__ = Version(major=20, minor=0, micro=0, releaselevel="alpha", serial=4)
__version__ = str(__version_info__)
# # SETUP.PY MARKER
+2
View File
@@ -29,6 +29,8 @@ class WebAppData(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`data` and :attr:`button_text` are equal.
.. seealso:: `Webappbot Example <examples.webappbot.html>`_
.. versionadded:: 20.0
Args:
+2
View File
@@ -30,6 +30,8 @@ class WebAppInfo(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`url` are equal.
.. seealso:: `Webappbot Example <examples.webappbot.html>`_
.. versionadded:: 20.0
Args:
+44 -4
View File
@@ -25,7 +25,8 @@ enums. If they are related to a specific class, then they are also available as
those classes.
.. versionchanged:: 20.0
Since v20.0, most of the constants in this module are grouped into enums.
* Most of the constants in this module are grouped into enums.
"""
__all__ = [
@@ -38,6 +39,7 @@ __all__ = [
"ChatInviteLinkLimit",
"ChatMemberStatus",
"ChatType",
"CustomEmojiStickerLimit",
"DiceEmoji",
"FileSizeLimit",
"FloodLimit",
@@ -57,14 +59,14 @@ __all__ = [
"PollLimit",
"PollType",
"SUPPORTED_WEBHOOK_PORTS",
"StickerType",
"WebhookLimit",
"UpdateType",
]
from enum import IntEnum
from typing import List, NamedTuple
from telegram._utils.enum import StringEnum
from telegram._utils.enum import IntEnum, StringEnum
class _BotAPIVersion(NamedTuple):
@@ -92,7 +94,7 @@ class _BotAPIVersion(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=1)
BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=2)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@@ -277,6 +279,22 @@ class ChatType(StringEnum):
""":obj:`str`: A :class:`telegram.Chat` that is a channel."""
class CustomEmojiStickerLimit(IntEnum):
"""This enum contains limitations for :meth:`telegram.Bot.get_custom_emoji_stickers`.
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 20.0
"""
__slots__ = ()
CUSTOM_EMOJI_IDENTIFIER_LIMIT = 200
""":obj:`int`: Maximum amount of custom emoji identifiers which can be specified for the
:paramref:`~telegram.Bot.get_custom_emoji_stickers.custom_emoji_ids` parameter of
:meth:`telegram.Bot.get_custom_emoji_stickers`.
"""
class DiceEmoji(StringEnum):
"""This enum contains the available emoji for :class:`telegram.Dice`/
:meth:`telegram.Bot.send_dice`. The enum
@@ -605,6 +623,11 @@ class MessageEntityType(StringEnum):
""":obj:`str`: Message entities representing strikethrough text."""
SPOILER = "spoiler"
""":obj:`str`: Message entities representing spoiler text."""
CUSTOM_EMOJI = "custom_emoji"
""":obj:`str`: Message entities representing inline custom emoji stickers.
.. versionadded:: 20.0
"""
class MessageLimit(IntEnum):
@@ -718,6 +741,23 @@ class MessageType(StringEnum):
""":obj:`str`: Messages with :attr:`telegram.Message.video_chat_participants_invited`."""
class StickerType(StringEnum):
"""This enum contains the available types of :class:`telegram.Sticker`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: 20.0
"""
__slots__ = ()
REGULAR = "regular"
""":obj:`str`: Regular sticker."""
MASK = "mask"
""":obj:`str`: Mask sticker."""
CUSTOM_EMOJI = "custom_emoji"
""":obj:`str`: Custom emoji sticker."""
class ParseMode(StringEnum):
"""This enum contains the available parse modes. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
+3 -7
View File
@@ -35,7 +35,7 @@ __all__ = (
"TimedOut",
)
from typing import Optional, Tuple, Union
from typing import Tuple, Union
def _lstrip_str(in_s: str, lstr: str) -> str:
@@ -100,14 +100,10 @@ class InvalidToken(TelegramError):
.. versionadded:: 20.0
"""
__slots__ = ("_message",)
__slots__ = ()
def __init__(self, message: str = None) -> None:
self._message = message
super().__init__("Invalid token" if self._message is None else self._message)
def __reduce__(self) -> Tuple[type, Tuple[Optional[str]]]: # type: ignore[override]
return self.__class__, (self._message,)
super().__init__("Invalid token" if message is None else message)
class NetworkError(TelegramError):
+5 -1
View File
@@ -19,10 +19,13 @@
"""Extensions over the Telegram Bot API to facilitate bot making"""
__all__ = (
"AIORateLimiter",
"Application",
"ApplicationBuilder",
"ApplicationHandlerStop",
"BaseHandler",
"BasePersistence",
"BaseRateLimiter",
"CallbackContext",
"CallbackDataCache",
"CallbackQueryHandler",
@@ -36,7 +39,6 @@ __all__ = (
"DictPersistence",
"ExtBot",
"filters",
"BaseHandler",
"InlineQueryHandler",
"InvalidCallbackData",
"Job",
@@ -56,9 +58,11 @@ __all__ = (
)
from . import filters
from ._aioratelimiter import AIORateLimiter
from ._application import Application, ApplicationHandlerStop
from ._applicationbuilder import ApplicationBuilder
from ._basepersistence import BasePersistence, PersistenceInput
from ._baseratelimiter import BaseRateLimiter
from ._callbackcontext import CallbackContext
from ._callbackdatacache import CallbackDataCache, InvalidCallbackData
from ._callbackqueryhandler import CallbackQueryHandler
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2022
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an implementation of the BaseRateLimiter class based on the aiolimiter
library.
"""
import asyncio
import contextlib
import logging
import sys
from typing import Any, AsyncIterator, Callable, Coroutine, Dict, Optional, Union
try:
from aiolimiter import AsyncLimiter
AIO_LIMITER_AVAILABLE = True
except ImportError:
AIO_LIMITER_AVAILABLE = False
from telegram._utils.types import JSONDict
from telegram.error import RetryAfter
from telegram.ext._baseratelimiter import BaseRateLimiter
# Useful for something like:
# async with group_limiter if group else null_context():
# so we don't have to differentiate between "I'm using a context manager" and "I'm not"
if sys.version_info >= (3, 10):
null_context = contextlib.nullcontext # pylint: disable=invalid-name
else:
@contextlib.asynccontextmanager
async def null_context() -> AsyncIterator[None]:
yield None
class AIORateLimiter(BaseRateLimiter[int]):
"""
Implementation of :class:`~telegram.ext.BaseRateLimiter` using the library
`aiolimiter <https://aiolimiter.readthedocs.io/>`_.
Important:
If you want to use this class, you must install PTB with the optional requirement
``rate-limiter``, i.e.
.. code-block:: bash
pip install python-telegram-bot[rate-limiter]
The rate limiting is applied by combining two levels of throttling and :meth:`process_request`
roughly boils down to::
async with group_limiter(group_id):
async with overall_limiter:
await callback(*args, **kwargs)
Here, ``group_id`` is determined by checking if there is a ``chat_id`` parameter in the
:paramref:`~telegram.ext.BaseRateLimiter.process_request.data`.
The ``overall_limiter`` is applied only if a ``chat_id`` argument is present at all.
Attention:
* Some bot methods accept a ``chat_id`` parameter in form of a ``@username`` for
supergroups and channels. As we can't know which ``@username`` corresponds to which
integer ``chat_id``, these will be treated as different groups, which may lead to
exceeding the rate limit.
* As channels can't be differentiated from supergroups by the ``@username`` or integer
``chat_id``, this also applies the group related rate limits to channels.
* A :exc:`~telegram.error.RetryAfter` exception will halt *all* requests for
:attr:`~telegram.error.RetryAfter.retry_after` + 0.1 seconds. This may be stricter than
necessary in some cases, e.g. the bot may hit a rate limit in one group but might still
be allowed to send messages in another group.
Note:
This class is to be understood as minimal effort reference implementation.
If you would like to handle rate limiting in a more sophisticated, fine-tuned way, we
welcome you to implement your own subclass of :class:`~telegram.ext.BaseRateLimiter`.
Feel free to check out the source code of this class for inspiration.
.. versionadded:: 20.0
Args:
overall_max_rate (:obj:`float`): The maximum number of requests allowed for the entire bot
per :paramref:`overall_time_period`. When set to 0, no rate limiting will be applied.
Defaults to ``30``.
overall_time_period (:obj:`float`): The time period (in seconds) during which the
:paramref:`overall_max_rate` is enforced. When set to 0, no rate limiting will be
applied. Defaults to 1.
group_max_rate (:obj:`float`): The maximum number of requests allowed for requests related
to groups and channels per :paramref:`group_time_period`. When set to 0, no rate
limiting will be applied. Defaults to 20.
group_time_period (:obj:`float`): The time period (in seconds) during which the
:paramref:`group_time_period` is enforced. When set to 0, no rate limiting will be
applied. Defaults to 60.
max_retries (:obj:`int`): The maximum number of retries to be made in case of a
:exc:`~telegram.error.RetryAfter` exception.
If set to 0, no retries will be made. Defaults to ``0``.
"""
__slots__ = (
"_base_limiter",
"_group_limiters",
"_group_max_rate",
"_group_time_period",
"_logger",
"_max_retries",
"_retry_after_event",
)
def __init__(
self,
overall_max_rate: float = 30,
overall_time_period: float = 1,
group_max_rate: float = 20,
group_time_period: float = 60,
max_retries: int = 0,
) -> None:
if not AIO_LIMITER_AVAILABLE:
raise RuntimeError(
"To use `AIORateLimiter`, PTB must be installed via `pip install "
"python-telegram-bot[rate-limiter]`."
)
if overall_max_rate and overall_time_period:
self._base_limiter: Optional[AsyncLimiter] = AsyncLimiter(
max_rate=overall_max_rate, time_period=overall_time_period
)
else:
self._base_limiter = None
if group_max_rate and group_time_period:
self._group_max_rate = group_max_rate
self._group_time_period = group_time_period
else:
self._group_max_rate = 0
self._group_time_period = 0
self._group_limiters: Dict[Union[str, int], AsyncLimiter] = {}
self._max_retries = max_retries
self._logger = logging.getLogger(__name__)
self._retry_after_event = asyncio.Event()
self._retry_after_event.set()
async def initialize(self) -> None:
"""Does nothing."""
async def shutdown(self) -> None:
"""Does nothing."""
def _get_group_limiter(self, group_id: Union[str, int, bool]) -> "AsyncLimiter":
# Remove limiters that haven't been used for so long that all their capacity is unused
# We only do that if we have a lot of limiters lying around to avoid looping on every call
# This is a minimal effort approach - a full-fledged cache could use a TTL approach
# or at least adapt the threshold dynamically depending on the number of active limiters
if len(self._group_limiters) > 512:
# We copy to avoid modifying the dict while we iterate over it
for key, limiter in self._group_limiters.copy().items():
if key == group_id:
continue
if limiter.has_capacity(limiter.max_rate):
del self._group_limiters[key]
if group_id not in self._group_limiters:
self._group_limiters[group_id] = AsyncLimiter(
max_rate=self._group_max_rate,
time_period=self._group_time_period,
)
return self._group_limiters[group_id]
async def _run_request(
self,
chat: bool,
group: Union[str, int, bool],
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]],
args: Any,
kwargs: Dict[str, Any],
) -> Union[bool, JSONDict, None]:
base_context = self._base_limiter if (chat and self._base_limiter) else null_context()
group_context = (
self._get_group_limiter(group) if group and self._group_max_rate else null_context()
)
async with group_context: # skipcq: PTC-W0062
async with base_context:
# In case a retry_after was hit, we wait with processing the request
await self._retry_after_event.wait()
return await callback(*args, **kwargs)
# mypy doesn't understand that the last run of the for loop raises an exception
async def process_request( # type: ignore[return]
self,
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]],
args: Any,
kwargs: Dict[str, Any],
endpoint: str, # skipcq: PYL-W0613
data: Dict[str, Any],
rate_limit_args: Optional[int],
) -> Union[bool, JSONDict, None]:
"""
Processes a request by applying rate limiting.
See :meth:`telegram.ext.BaseRateLimiter.process_request` for detailed information on the
arguments.
Args:
rate_limit_args (:obj:`None` | :obj:`int`): If set, specifies the maximum number of
retries to be made in case of a :exc:`~telegram.error.RetryAfter` exception.
Defaults to :paramref:`AIORateLimiter.max_retries`.
"""
max_retries = rate_limit_args or self._max_retries
group: Union[int, str, bool] = False
chat: bool = False
chat_id = data.get("chat_id")
if chat_id is not None:
chat = True
# In case user passes integer chat id as string
try:
chat_id = int(chat_id)
except (ValueError, TypeError):
pass
if (isinstance(chat_id, int) and chat_id < 0) or isinstance(chat_id, str):
# string chat_id only works for channels and supergroups
# We can't really tell channels from groups though ...
group = chat_id
for i in range(max_retries + 1):
try:
return await self._run_request(
chat=chat, group=group, callback=callback, args=args, kwargs=kwargs
)
except RetryAfter as exc:
if i == max_retries:
self._logger.exception(
"Rate limit hit after maximum of %d retries", max_retries, exc_info=exc
)
raise exc
sleep = exc.retry_after + 0.1
self._logger.info("Rate limit hit. Retrying after %f seconds", sleep)
# Make sure we don't allow other requests to be processed
self._retry_after_event.clear()
await asyncio.sleep(sleep)
finally:
# Allow other requests to be processed
self._retry_after_event.set()
+24 -3
View File
@@ -182,6 +182,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
post_init (:term:`coroutine function`): Optional. A callback that will be executed by
:meth:`Application.run_polling` and :meth:`Application.run_webhook` after initializing
the application via :meth:`initialize`.
post_shutdown (:term:`coroutine function`): Optional. A callback that will be executed by
:meth:`Application.run_polling` and :meth:`Application.run_webhook` after shutting down
the application via :meth:`shutdown`.
"""
@@ -213,6 +216,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
"job_queue",
"persistence",
"post_init",
"post_shutdown",
"update_queue",
"updater",
"user_data",
@@ -231,6 +235,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
post_init: Optional[
Callable[["Application[BT, CCT, UD, CD, BD, JQ]"], Coroutine[Any, Any, None]]
],
post_shutdown: Optional[
Callable[["Application[BT, CCT, UD, CD, BD, JQ]"], Coroutine[Any, Any, None]]
],
):
if not was_called_by(
inspect.currentframe(), Path(__file__).parent.resolve() / "_applicationbuilder.py"
@@ -248,11 +255,12 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
self.handlers: Dict[int, List[BaseHandler]] = {}
self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {}
self.post_init = post_init
self.post_shutdown = post_shutdown
if isinstance(concurrent_updates, int) and concurrent_updates < 0:
raise ValueError("`concurrent_updates` must be a non-negative integer!")
if concurrent_updates is True:
concurrent_updates = 4096
concurrent_updates = 256
self._concurrent_updates_sem = asyncio.BoundedSemaphore(concurrent_updates or 1)
self._concurrent_updates: int = concurrent_updates or 0
@@ -362,6 +370,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
* :attr:`persistence` by calling :meth:`update_persistence` and
:meth:`BasePersistence.flush`
Does *not* call :attr:`post_shutdown` - that is only done by :meth:`run_polling` and
:meth:`run_webhook`.
.. seealso::
:meth:`initialize`
@@ -573,6 +584,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
If :attr:`post_init` is set, it will be called between :meth:`initialize` and
:meth:`telegram.ext.Updater.start_polling`.
If :attr:`post_shutdown` is set, it will be called after both :meth:`shutdown`
and :meth:`telegram.ext.Updater.shutdown`.
.. seealso::
:meth:`initialize`, :meth:`start`, :meth:`stop`, :meth:`shutdown`
:meth:`telegram.ext.Updater.start_polling`, :meth:`run_webhook`
@@ -683,6 +697,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
If :attr:`post_init` is set, it will be called between :meth:`initialize` and
:meth:`telegram.ext.Updater.start_webhook`.
If :attr:`post_shutdown` is set, it will be called after both :meth:`shutdown`
and :meth:`telegram.ext.Updater.shutdown`.
.. seealso::
:meth:`initialize`, :meth:`start`, :meth:`stop`, :meth:`shutdown`
:meth:`telegram.ext.Updater.start_webhook`, :meth:`run_polling`
@@ -691,7 +708,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
listen (:obj:`str`, optional): IP-Address to listen on. Defaults to
`127.0.0.1 <https://en.wikipedia.org/wiki/Localhost>`_.
port (:obj:`int`, optional): Port the bot should be listening on. Must be one of
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`. Defaults to ``80``.
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS` unless the bot is running
behind a proxy. Defaults to ``80``.
url_path (:obj:`str`, optional): Path inside url. Defaults to `` '' ``
cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file.
key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file.
@@ -813,7 +831,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
if self.running:
loop.run_until_complete(self.stop())
loop.run_until_complete(self.shutdown())
loop.run_until_complete(self.updater.shutdown()) # type: ignore[union-attr]
if self.post_shutdown:
loop.run_until_complete(self.post_shutdown(self))
finally:
if close_loop:
loop.close()
@@ -1399,6 +1418,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
Note:
Attempts to add the same callback multiple times will be ignored.
.. seealso:: `Errorhandler Example <examples.errorhandlerbot.py>`_
Args:
callback (:term:`coroutine function`): The callback function for this error handler.
Will be called when an error is raised. Callback signature::
+76 -5
View File
@@ -45,7 +45,8 @@ from telegram.request import BaseRequest
from telegram.request._httpxrequest import HTTPXRequest
if TYPE_CHECKING:
from telegram.ext import BasePersistence, CallbackContext, Defaults
from telegram.ext import BasePersistence, BaseRateLimiter, CallbackContext, Defaults
from telegram.ext._utils.types import RLARGS
# Type hinting is a bit complicated here because we try to get to a sane level of
# leveraging generics and therefore need a number of type variables.
@@ -81,6 +82,7 @@ _BOT_CHECKS = [
("defaults", "defaults"),
("arbitrary_callback_data", "arbitrary_callback_data"),
("private_key", "private_key"),
("rate_limiter", "rate_limiter instance"),
]
_TWO_ARGS_REQ = "The parameter `{}` may only be set, if no {} was set."
@@ -135,9 +137,11 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
"_persistence",
"_pool_timeout",
"_post_init",
"_post_shutdown",
"_private_key",
"_private_key_password",
"_proxy_url",
"_rate_limiter",
"_read_timeout",
"_request",
"_token",
@@ -178,6 +182,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._concurrent_updates: DVInput[Union[int, bool]] = DEFAULT_FALSE
self._updater: ODVInput[Updater] = DEFAULT_NONE
self._post_init: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None
self._post_shutdown: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None
self._rate_limiter: ODVInput["BaseRateLimiter"] = DEFAULT_NONE
def _build_request(self, get_updates: bool) -> BaseRequest:
prefix = "_get_updates_" if get_updates else "_"
@@ -191,7 +197,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
)
else:
connection_pool_size = (
DefaultValue.get_value(getattr(self, f"{prefix}connection_pool_size")) or 128
DefaultValue.get_value(getattr(self, f"{prefix}connection_pool_size")) or 256
)
timeouts = dict(
@@ -225,6 +231,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
arbitrary_callback_data=DefaultValue.get_value(self._arbitrary_callback_data),
request=self._build_request(get_updates=False),
get_updates_request=self._build_request(get_updates=True),
rate_limiter=DefaultValue.get_value(self._rate_limiter),
)
def build(
@@ -271,6 +278,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
persistence=persistence,
context_types=DefaultValue.get_value(self._context_types),
post_init=self._post_init,
post_shutdown=self._post_shutdown,
**self._application_kwargs, # For custom Application subclasses
)
@@ -424,7 +432,9 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
def connection_pool_size(self: BuilderType, connection_pool_size: int) -> BuilderType:
"""Sets the size of the connection pool for the
:paramref:`~telegram.request.HTTPXRequest.connection_pool_size` parameter of
:attr:`telegram.Bot.request`. Defaults to ``128``.
:attr:`telegram.Bot.request`. Defaults to ``256``.
.. include:: inclusions/pool_size_tip.rst
Args:
connection_pool_size (:obj:`int`): The size of the connection pool.
@@ -504,6 +514,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
:paramref:`~telegram.request.HTTPXRequest.pool_timeout` parameter of
:attr:`telegram.Bot.request`. Defaults to :obj:`None`.
.. include:: inclusions/pool_size_tip.rst
Args:
pool_timeout (:obj:`float`): See
:paramref:`telegram.request.HTTPXRequest.pool_timeout` for more information.
@@ -770,11 +782,13 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
that your bot does not (explicitly or implicitly) rely on updates being processed
sequentially.
.. include:: inclusions/pool_size_tip.rst
.. seealso:: :attr:`telegram.ext.Application.concurrent_updates`
Args:
concurrent_updates (:obj:`bool` | :obj:`int`): Passing :obj:`True` will allow for
``4096`` updates to be processed concurrently. Pass an integer to specify a
``256`` updates to be processed concurrently. Pass an integer to specify a
different number of updates that may be processed concurrently.
Returns:
@@ -928,10 +942,67 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._post_init = post_init
return self
def post_shutdown(
self: BuilderType, post_shutdown: Callable[[Application], Coroutine[Any, Any, None]]
) -> BuilderType:
"""
Sets a callback to be executed by :meth:`Application.run_polling` and
:meth:`Application.run_webhook` *after* executing :meth:`Updater.shutdown`
and :meth:`Application.shutdown`.
Tip:
This can be used for custom shutdown logic that requires to await coroutines, e.g.
closing a database connection
Example:
.. code::
async def post_shutdown(application: Application) -> None:
await application.bot_data['database'].close()
application = Application.builder()
.token("TOKEN")
.post_shutdown(post_shutdown)
.build()
Args:
post_shutdown (:term:`coroutine function`): The custom callback. Must be a
:term:`coroutine function` and must accept exactly one positional argument, which
is the :class:`~telegram.ext.Application`::
async def post_shutdown(application: Application) -> None:
Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
self._post_shutdown = post_shutdown
return self
def rate_limiter(
self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]",
rate_limiter: "BaseRateLimiter[RLARGS]",
) -> "ApplicationBuilder[ExtBot[RLARGS], CCT, UD, CD, BD, JQ]":
"""Sets a :class:`telegram.ext.BaseRateLimiter` instance for the
:paramref:`telegram.ext.ExtBot.rate_limiter` parameter of
:attr:`telegram.ext.Application.bot`.
Args:
rate_limiter (:class:`telegram.ext.BaseRateLimiter`): The rate limiter.
Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
if self._bot is not DEFAULT_NONE:
raise RuntimeError(_TWO_ARGS_REQ.format("rate_limiter", "bot instance"))
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format("rate_limiter", "updater"))
self._rate_limiter = rate_limiter
return self # type: ignore[return-value]
InitApplicationBuilder = ( # This is defined all the way down here so that its type is inferred
ApplicationBuilder[ # by Pylance correctly.
ExtBot,
ExtBot[None],
ContextTypes.DEFAULT_TYPE,
Dict,
Dict,
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2022
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains a class that allows to rate limit requests to the Bot API."""
from abc import ABC, abstractmethod
from typing import Any, Callable, Coroutine, Dict, Generic, Optional, Union
from telegram._utils.types import JSONDict
from telegram.ext._utils.types import RLARGS
class BaseRateLimiter(ABC, Generic[RLARGS]):
"""
Abstract interface class that allows to rate limit the requests that python-telegram-bot
sends to the Telegram Bot API. An implementation of this class
must implement all abstract methods and properties.
This class is a :class:`~typing.Generic` class and accepts one type variable that specifies
the type of the argument :paramref:`~process_request.rate_limit_args` of
:meth:`process_request` and the methods of :class:`~telegram.ext.ExtBot`.
Hint:
Requests to :meth:`~telegram.Bot.get_updates` are never rate limited.
.. versionadded:: 20.0
"""
__slots__ = ()
@abstractmethod
async def initialize(self) -> None:
"""Initialize resources used by this class. Must be implemented by a subclass."""
@abstractmethod
async def shutdown(self) -> None:
"""Stop & clear resources used by this class. Must be implemented by a subclass."""
@abstractmethod
async def process_request(
self,
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]],
args: Any,
kwargs: Dict[str, Any],
endpoint: str,
data: Dict[str, Any],
rate_limit_args: Optional[RLARGS],
) -> Union[bool, JSONDict, None]:
"""
Process a request. Must be implemented by a subclass.
This method must call :paramref:`callback` and return the result of the call.
`When` the callback is called is up to the implementation.
Important:
This method must only return once the result of :paramref:`callback` is known!
If a :exc:`~telegram.error.RetryAfter` error is raised, this method may try to make
a new request by calling the callback again.
Warning:
This method *should not* handle any other exception raised by :paramref:`callback`!
There are basically two different approaches how a rate limiter can be implemented:
1. React only if necessary. In this case, the :paramref:`callback` is called without any
precautions. If a :exc:`~telegram.error.RetryAfter` error is raised, processing requests
is halted for the :attr:`~telegram.error.RetryAfter.retry_after` and finally the
:paramref:`callback` is called again. This approach is often amendable for bots that
don't have a large user base and/or don't send more messages than they get updates.
2. Throttle all outgoing requests. In this case the implementation makes sure that the
requests are spread out over a longer time interval in order to stay below the rate
limits. This approach is often amendable for bots that have a large user base and/or
send more messages than they get updates.
An implementation can use the information provided by :paramref:`data`,
:paramref:`endpoint` and :paramref:`rate_limit_args` to handle each request differently.
Examples:
* It is usually desirable to call :meth:`telegram.Bot.answer_inline_query`
as quickly as possible, while delaying :meth:`telegram.Bot.send_message`
is acceptable.
* There are `different <https://core.telegram.org/bots/faq\
#my-bot-is-hitting-limits-how-do-i-avoid-this>`_ rate limits for group chats and
private chats.
* When sending broadcast messages to a large number of users, these requests can
typically be delayed for a longer time than messages that are direct replies to a
user input.
Args:
callback (Callable[..., :term:`coroutine`]): The coroutine function that must be called
to make the request.
args (Tuple[:obj:`object`]): The positional arguments for the :paramref:`callback`
function.
kwargs (Dict[:obj:`str`, :obj:`object`]): The keyword arguments for the
:paramref:`callback` function.
endpoint (:obj:`str`): The endpoint that the request is made for, e.g.
``"sendMessage"``.
data (Dict[:obj:`str`, :obj:`object`]): The parameters that were passed to the method
of :class:`~telegram.ext.ExtBot`. Any ``api_kwargs`` are included in this and
any :paramref:`~telegram.ext.ExtBot.defaults` are already applied.
Example:
When calling::
await ext_bot.send_message(
chat_id=1,
text="Hello world!",
api_kwargs={"custom": "arg"}
)
then :paramref:`data` will be::
{"chat_id": 1, "text": "Hello world!", "custom": "arg"}
rate_limit_args (:obj:`None` | :class:`object`): Custom arguments passed to the methods
of :class:`~telegram.ext.ExtBot`. Can e.g. be used to specify the priority of
the request.
Returns:
:obj:`bool` | Dict[:obj:`str`, :obj:`object`] | :obj:`None`: The result of the
callback function.
"""
+14 -2
View File
@@ -160,6 +160,9 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
``chat_data`` needs to be transferred. For details see our `wiki page
<https://github.com/python-telegram-bot/python-telegram-bot/wiki/
Storing-bot,-user-and-chat-related-data#chat-migration>`_.
.. versionchanged:: 20.0
The chat data is now also present in error handlers if the error is caused by a job.
"""
if self._chat_id is not None:
return self._application.chat_data[self._chat_id]
@@ -176,6 +179,9 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
""":obj:`ContextTypes.user_data`: Optional. An object that can be used to keep any data in.
For each update from the same user it will be the same :obj:`ContextTypes.user_data`.
Defaults to :obj:`dict`.
.. versionchanged:: 20.0
The user data is now also present in error handlers if the error is caused by a job.
"""
if self._user_id is not None:
return self._application.user_data[self._user_id]
@@ -275,10 +281,16 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
Returns:
:class:`telegram.ext.CallbackContext`
"""
self = cls.from_update(update, application)
# update and job will never be present at the same time
if update is not None:
self = cls.from_update(update, application)
elif job is not None:
self = cls.from_job(job, application)
else:
self = cls(application) # type: ignore
self.error = error
self.coroutine = coroutine
self.job = job
return self
@classmethod
+5
View File
@@ -94,6 +94,11 @@ class CallbackDataCache:
sent via inline mode.
If necessary, will drop the least recently used items.
.. seealso:: :attr:`telegram.ext.ExtBot.callback_data_cache`,
`Arbitrary callback_data <https://github.com/python-telegram-bot/
python-telegram-bot/wiki/Arbitrary-callback_data>`_,
Arbitrary Callback Data Example <examples.arbitrarycallbackdatabot.html>
.. versionadded:: 13.6
Args:
+4 -2
View File
@@ -31,12 +31,14 @@ RT = TypeVar("RT")
class ChatMemberHandler(BaseHandler[Update, CCT]):
"""BaseHandler class to handle Telegram updates that contain a chat member update.
.. versionadded:: 13.4
Warning:
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
.. seealso:: `Chat Member Example <examples.chatmemberbot.html>`_
.. versionadded:: 13.4
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
+2
View File
@@ -33,6 +33,8 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext`
interface.
.. seealso:: `ContextTypes Example <examples.contexttypesbot.html>`_
.. versionadded:: 13.6
Args:
+8 -1
View File
@@ -184,7 +184,12 @@ class ConversationHandler(BaseHandler[Update, CCT]):
states to continue the parent conversation after the child conversation has ended or even
map a state to :attr:`END` to end the *parent* conversation from within the child
conversation. For an example on nested :class:`ConversationHandler` s, see
:any:`examples.conversationbot`.
:any:`examples.nestedconversationbot`.
.. seealso:: `Conversation Example <examples.conversationbot.html>`_,
`Conversation Example 2 <examples.conversationbot2.html>`_,
`Nested Conversation Example <examples.nestedconversationbot.html>`_,
`Persistent Conversation Example <examples.persistentconversationbot.html>`_
Args:
entry_points (List[:class:`telegram.ext.BaseHandler`]): A list of :obj:`BaseHandler`
@@ -693,6 +698,8 @@ class ConversationHandler(BaseHandler[Update, CCT]):
return None
if self.per_chat and not update.effective_chat:
return None
if self.per_user and not update.effective_user:
return None
if self.per_message and not update.callback_query:
return None
if update.callback_query and self.per_chat and not update.callback_query.message:
+2706 -14
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -46,6 +46,8 @@ class InlineQueryHandler(BaseHandler[Update, CCT]):
chats and may not be set for inline queries coming from third-party clients. These
updates won't be handled, if :attr:`chat_types` is passed.
.. seealso:: `Inlinebot Example <examples.inlinebot.html>`_
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
+14 -2
View File
@@ -40,6 +40,12 @@ class JobQueue:
"""This class allows you to periodically perform tasks with the bot. It is a convenience
wrapper for the APScheduler library.
.. seealso:: :attr:`telegram.ext.Application.job_queue`,
:attr:`telegram.ext.CallbackContext.job_queue`,
`Timerbot Example <examples.timerbot.html>`_,
`Job Queue <https://github.com/python-telegram-bot/
python-telegram-bot/wiki/Extensions-%E2%80%93-JobQueue>`_
Attributes:
scheduler (:class:`apscheduler.schedulers.asyncio.AsyncIOScheduler`): The scheduler.
@@ -47,7 +53,6 @@ class JobQueue:
Uses :class:`~apscheduler.schedulers.asyncio.AsyncIOScheduler` instead of
:class:`~apscheduler.schedulers.background.BackgroundScheduler`
"""
__slots__ = ("_application", "scheduler", "_executor")
@@ -567,7 +572,11 @@ class JobQueue:
await asyncio.sleep(0.01)
def jobs(self) -> Tuple["Job", ...]:
"""Returns a tuple of all *scheduled* jobs that are currently in the :class:`JobQueue`."""
"""Returns a tuple of all *scheduled* jobs that are currently in the :class:`JobQueue`.
Returns:
Tuple[:class:`Job`]: Tuple of all *scheduled* jobs.
"""
return tuple(
Job._from_aps_job(job) # pylint: disable=protected-access
for job in self.scheduler.get_jobs()
@@ -576,6 +585,9 @@ class JobQueue:
def get_jobs_by_name(self, name: str) -> Tuple["Job", ...]:
"""Returns a tuple of all *pending/scheduled* jobs with the given name that are currently
in the :class:`JobQueue`.
Returns:
Tuple[:class:`Job`]: Tuple of all *pending* or *scheduled* jobs matching the name.
"""
return tuple(job for job in self.jobs() if job.name == name)
+2
View File
@@ -32,6 +32,8 @@ class PollAnswerHandler(BaseHandler[Update, CCT]):
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
.. seealso:: `Pollbot EXample <examples.pollbot.html>`_
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
+2
View File
@@ -32,6 +32,8 @@ class PollHandler(BaseHandler[Update, CCT]):
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
.. seealso:: `Pollbot Example <examples.pollbot.html>`_
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
+2
View File
@@ -31,6 +31,8 @@ class PreCheckoutQueryHandler(BaseHandler[Update, CCT]):
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
.. seealso:: `Paymentbot Example <examples.paymentbot.html>`_
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
+2
View File
@@ -31,6 +31,8 @@ class ShippingQueryHandler(BaseHandler[Update, CCT]):
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
.. seealso:: `Paymentbot Example <examples.paymentbot.html>`_
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
+2 -1
View File
@@ -389,7 +389,8 @@ class Updater(AbstractAsyncContextManager):
listen (:obj:`str`, optional): IP-Address to listen on. Defaults to
`127.0.0.1 <https://en.wikipedia.org/wiki/Localhost>`_.
port (:obj:`int`, optional): Port the bot should be listening on. Must be one of
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`. Defaults to ``80``.
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS` unless the bot is running
behind a proxy. Defaults to ``80``.
url_path (:obj:`str`, optional): Path inside url (http(s)://listen:port/<url_path>).
Defaults to ``''``.
cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file.
+13 -1
View File
@@ -39,8 +39,10 @@ from typing import (
)
if TYPE_CHECKING:
from typing import Optional
from telegram import Bot
from telegram.ext import CallbackContext, JobQueue
from telegram.ext import BaseRateLimiter, CallbackContext, JobQueue
CCT = TypeVar("CCT", bound="CallbackContext")
"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.
@@ -101,3 +103,13 @@ JQ = TypeVar("JQ", bound=Union[None, "JobQueue"])
"""Type of the job queue.
.. versionadded:: 20.0"""
RL = TypeVar("RL", bound="Optional[BaseRateLimiter]")
"""Type of the rate limiter.
.. versionadded:: 20.0"""
RLARGS = TypeVar("RLARGS")
"""Type of the rate limiter arguments.
.. versionadded:: 20.0"""
+1
View File
@@ -1965,6 +1965,7 @@ class Sticker:
.. versionadded:: 20.0
"""
# neither mask nor emoji can be a message.sticker, so no filters for them
class _SuccessfulPayment(MessageFilter):
+6 -1
View File
@@ -93,7 +93,10 @@ def mention_markdown(user_id: Union[int, str], name: str, version: int = 1) -> s
Returns:
:obj:`str`: The inline mention for the user as Markdown.
"""
return f"[{escape_markdown(name, version=version)}](tg://user?id={user_id})"
tg_link = f"tg://user?id={user_id}"
if version == 1:
return f"[{name}]({tg_link})"
return f"[{escape_markdown(name, version=version)}]({tg_link})"
def effective_message_type(entity: Union["Message", "Update"]) -> Optional[str]:
@@ -143,6 +146,8 @@ def create_deep_linked_url(bot_username: str, payload: str = None, group: bool =
Examples:
``create_deep_linked_url(bot.get_me().username, "some-params")``
.. seealso:: `Deeplinking Example <examples.deeplinking.html>`_
Args:
bot_username (:obj:`str`): The username to link to
payload (:obj:`str`, optional): Parameters to encode in the created URL
+5 -5
View File
@@ -312,20 +312,20 @@ class BaseRequest(
message += f"\nThe server response contained unknown parameters: {parameters}"
if code == HTTPStatus.FORBIDDEN:
if code == HTTPStatus.FORBIDDEN: # 403
raise Forbidden(message)
if code in (HTTPStatus.NOT_FOUND, HTTPStatus.UNAUTHORIZED):
if code in (HTTPStatus.NOT_FOUND, HTTPStatus.UNAUTHORIZED): # 404 and 401
# TG returns 404 Not found for
# 1) malformed tokens
# 2) correct tokens but non-existing method, e.g. api.tg.org/botTOKEN/unkonwnMethod
# We can basically rule out 2) since we don't let users make requests manually
# TG returns 401 Unauthorized for correctly formatted tokens that are not valid
raise InvalidToken(message)
if code == HTTPStatus.BAD_REQUEST:
if code == HTTPStatus.BAD_REQUEST: # 400
raise BadRequest(message)
if code == HTTPStatus.CONFLICT:
if code == HTTPStatus.CONFLICT: # 409
raise Conflict(message)
if code == HTTPStatus.BAD_GATEWAY:
if code == HTTPStatus.BAD_GATEWAY: # 502
raise NetworkError(description or "Bad Gateway")
raise NetworkError(f"{message} ({code})")
+1 -1
View File
@@ -58,7 +58,7 @@ def get(name, fallback):
if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None:
try:
return BOTS[JOB_INDEX][name]
except KeyError:
except (KeyError, IndexError):
pass
# Otherwise go with the fallback
+6
View File
@@ -558,6 +558,7 @@ async def check_shortcut_call(
bot: The bot
bot_method_name: The bot methods name, e.g. `'send_message'`
skip_params: Parameters that are allowed to be missing, e.g. `['inline_message_id']`
`rate_limit_args` will be skipped by default
shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id``
Returns:
@@ -565,8 +566,13 @@ async def check_shortcut_call(
"""
if not skip_params:
skip_params = set()
else:
skip_params = set(skip_params)
skip_params.add("rate_limit_args")
if not shortcut_kwargs:
shortcut_kwargs = set()
else:
shortcut_kwargs = set(shortcut_kwargs)
orig_bot_method = getattr(bot, bot_method_name)
bot_signature = inspect.signature(orig_bot_method)
+123 -2
View File
@@ -130,6 +130,7 @@ class TestApplication:
updater=updater,
concurrent_updates=False,
post_init=None,
post_shutdown=None,
)
assert len(recwarn) == 1
assert (
@@ -139,7 +140,7 @@ class TestApplication:
assert recwarn[0].filename == __file__, "stacklevel is incorrect!"
@pytest.mark.parametrize(
"concurrent_updates, expected", [(0, 0), (4, 4), (False, 0), (True, 4096)]
"concurrent_updates, expected", [(0, 0), (4, 4), (False, 0), (True, 256)]
)
@pytest.mark.filterwarnings("ignore: `Application` instances should")
def test_init(self, bot, concurrent_updates, expected):
@@ -152,6 +153,9 @@ class TestApplication:
async def post_init(application: Application) -> None:
pass
async def post_shutdown(application: Application) -> None:
pass
app = Application(
bot=bot,
update_queue=update_queue,
@@ -161,6 +165,7 @@ class TestApplication:
updater=updater,
concurrent_updates=concurrent_updates,
post_init=post_init,
post_shutdown=post_shutdown,
)
assert app.bot is bot
assert app.update_queue is update_queue
@@ -172,6 +177,7 @@ class TestApplication:
assert app.bot is updater.bot
assert app.concurrent_updates == expected
assert app.post_init is post_init
assert app.post_shutdown is post_shutdown
# These should be done by the builder
assert app.persistence.bot is None
@@ -192,6 +198,7 @@ class TestApplication:
updater=updater,
concurrent_updates=-1,
post_init=None,
post_shutdown=None,
)
def test_custom_context_init(self, bot):
@@ -1433,6 +1440,52 @@ class TestApplication:
thread.join()
assert events == ["init", "post_init", "start_polling"], "Wrong order of events detected!"
@pytest.mark.skipif(
platform.system() == "Windows",
reason="Can't send signals without stopping whole process on windows",
)
def test_run_polling_post_shutdown(self, bot, monkeypatch):
events = []
async def get_updates(*args, **kwargs):
# This makes sure that other coroutines have a chance of running as well
await asyncio.sleep(0)
return []
def thread_target():
waited = 0
while not app.running:
time.sleep(0.05)
waited += 0.05
if waited > 5:
pytest.fail("App apparently won't start")
os.kill(os.getpid(), signal.SIGINT)
async def post_shutdown(app: Application) -> None:
events.append("post_shutdown")
app = Application.builder().token(bot.token).post_shutdown(post_shutdown).build()
monkeypatch.setattr(app.bot, "get_updates", get_updates)
monkeypatch.setattr(
app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown"))
)
monkeypatch.setattr(
app.updater,
"shutdown",
call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")),
)
thread = Thread(target=thread_target)
thread.start()
app.run_polling(drop_pending_updates=True, close_loop=False)
thread.join()
assert events == [
"updater.shutdown",
"shutdown",
"post_shutdown",
], "Wrong order of events detected!"
@pytest.mark.skipif(
platform.system() == "Windows",
reason="Can't send signals without stopping whole process on windows",
@@ -1620,6 +1673,69 @@ class TestApplication:
thread.join()
assert events == ["init", "post_init", "start_webhook"], "Wrong order of events detected!"
@pytest.mark.skipif(
platform.system() == "Windows",
reason="Can't send signals without stopping whole process on windows",
)
def test_run_webhook_post_shutdown(self, bot, monkeypatch):
events = []
async def delete_webhook(*args, **kwargs):
return True
async def set_webhook(*args, **kwargs):
return True
async def get_updates(*args, **kwargs):
# This makes sure that other coroutines have a chance of running as well
await asyncio.sleep(0)
return []
def thread_target():
waited = 0
while not app.running:
time.sleep(0.05)
waited += 0.05
if waited > 5:
pytest.fail("App apparently won't start")
os.kill(os.getpid(), signal.SIGINT)
async def post_shutdown(app: Application) -> None:
events.append("post_shutdown")
app = Application.builder().token(bot.token).post_shutdown(post_shutdown).build()
monkeypatch.setattr(app.bot, "set_webhook", set_webhook)
monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook)
monkeypatch.setattr(
app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown"))
)
monkeypatch.setattr(
app.updater,
"shutdown",
call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")),
)
thread = Thread(target=thread_target)
thread.start()
ip = "127.0.0.1"
port = randrange(1024, 49152)
app.run_webhook(
ip_address=ip,
port=port,
url_path="TOKEN",
drop_pending_updates=True,
close_loop=False,
)
thread.join()
assert events == [
"updater.shutdown",
"shutdown",
"post_shutdown",
], "Wrong order of events detected!"
@pytest.mark.skipif(
platform.system() == "Windows",
reason="Can't send signals without stopping whole process on windows",
@@ -1718,7 +1834,12 @@ class TestApplication:
assert not app.running
assert not app.updater.running
assert set(shutdowns) == {"application", "updater"}
if method == "initialize":
# If App.initialize fails, then App.shutdown pretty much does nothing, especially
# doesn't call Updater.shutdown.
assert set(shutdowns) == {"application"}
else:
assert set(shutdowns) == {"application", "updater"}
@pytest.mark.parametrize("method", ["start_polling", "start_webhook"])
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
+14 -1
View File
@@ -23,6 +23,7 @@ import httpx
import pytest
from telegram.ext import (
AIORateLimiter,
Application,
ApplicationBuilder,
ContextTypes,
@@ -82,6 +83,7 @@ class TestApplicationBuilder:
assert app.bot.private_key is None
assert app.bot.arbitrary_callback_data is False
assert app.bot.defaults is None
assert app.bot.rate_limiter is None
get_updates_client = app.bot._request[0]._client
assert get_updates_client.limits == httpx.Limits(
@@ -93,7 +95,7 @@ class TestApplicationBuilder:
)
client = app.bot.request._client
assert client.limits == httpx.Limits(max_connections=128, max_keepalive_connections=128)
assert client.limits == httpx.Limits(max_connections=256, max_keepalive_connections=256)
assert client.proxies is None
assert client.timeout == httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=1.0)
@@ -107,6 +109,7 @@ class TestApplicationBuilder:
assert app.persistence is None
assert app.post_init is None
assert app.post_shutdown is None
@pytest.mark.parametrize(
"method, description", _BOT_CHECKS, ids=[entry[0] for entry in _BOT_CHECKS]
@@ -195,6 +198,7 @@ class TestApplicationBuilder:
"proxy_url",
"bot",
"update_queue",
"rate_limiter",
]
+ [entry[0] for entry in _BOT_CHECKS],
)
@@ -246,10 +250,13 @@ class TestApplicationBuilder:
defaults = Defaults()
request = HTTPXRequest()
get_updates_request = HTTPXRequest()
rate_limiter = AIORateLimiter()
builder.token(bot.token).base_url("base_url").base_file_url("base_file_url").private_key(
PRIVATE_KEY
).defaults(defaults).arbitrary_callback_data(42).request(request).get_updates_request(
get_updates_request
).rate_limiter(
rate_limiter
)
built_bot = builder.build().bot
@@ -265,6 +272,7 @@ class TestApplicationBuilder:
assert built_bot._request[0] is get_updates_request
assert built_bot.callback_data_cache.maxsize == 42
assert built_bot.private_key
assert built_bot.rate_limiter is rate_limiter
@dataclass
class Client:
@@ -322,6 +330,9 @@ class TestApplicationBuilder:
async def post_init(app: Application) -> None:
pass
async def post_shutdown(app: Application) -> None:
pass
app = (
builder.token(bot.token)
.job_queue(job_queue)
@@ -330,6 +341,7 @@ class TestApplicationBuilder:
.context_types(context_types)
.concurrent_updates(concurrent_updates)
.post_init(post_init)
.post_shutdown(post_shutdown)
).build()
assert app.job_queue is job_queue
assert app.job_queue.application is app
@@ -341,6 +353,7 @@ class TestApplicationBuilder:
assert app.context_types is context_types
assert app.concurrent_updates == concurrent_updates
assert app.post_init is post_init
assert app.post_shutdown is post_shutdown
updater = Updater(bot=bot, update_queue=update_queue)
app = ApplicationBuilder().updater(updater).build()
+94 -85
View File
@@ -137,6 +137,34 @@ xfail = pytest.mark.xfail(
)
def bot_methods(ext_bot=True):
arg_values = []
ids = []
non_api_methods = [
"de_json",
"de_list",
"to_dict",
"to_json",
"parse_data",
"get_bot",
"set_bot",
"initialize",
"shutdown",
"insert_callback_data",
]
classes = (Bot, ExtBot) if ext_bot else (Bot,)
for cls in classes:
for name, attribute in inspect.getmembers(cls, predicate=inspect.isfunction):
if name.startswith("_") or name in non_api_methods:
continue
arg_values.append((cls, name, attribute))
ids.append(f"{cls.__name__}.{name}")
return pytest.mark.parametrize(
argnames="bot_class, bot_method_name,bot_method", argvalues=arg_values, ids=ids
)
class TestBot:
"""
Most are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot
@@ -155,22 +183,6 @@ class TestBot:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
@pytest.mark.parametrize(
"token",
argvalues=[
"123",
"12a:abcd1234",
"12:abcd1234",
"1234:abcd1234\n",
" 1234:abcd1234",
" 1234:abcd1234\r",
"1234:abcd 1234",
],
)
async def test_invalid_token(self, token):
with pytest.raises(InvalidToken, match="Invalid token"):
Bot(token)
async def test_initialize_and_shutdown(self, bot, monkeypatch):
async def initialize(*args, **kwargs):
self.test_flag = ["initialize"]
@@ -279,12 +291,14 @@ class TestBot:
assert acd_bot.arbitrary_callback_data == acd
assert acd_bot.callback_data_cache.maxsize == maxsize
@flaky(3, 1)
async def test_invalid_token_server_response(self, monkeypatch):
monkeypatch.setattr("telegram.Bot._validate_token", lambda x, y: "")
with pytest.raises(InvalidToken):
async with make_bot(token="12") as bot:
await bot.get_me()
async def test_no_token_passed(self):
with pytest.raises(InvalidToken, match="You must pass the token"):
Bot("")
async def test_invalid_token_server_response(self):
with pytest.raises(InvalidToken, match="The token `12` was rejected by the server."):
async with make_bot(token="12"):
pass
async def test_unknown_kwargs(self, bot, monkeypatch):
async def post(url, request_data: RequestData, *args, **kwargs):
@@ -366,29 +380,10 @@ class TestBot:
with pytest.raises(pickle.PicklingError, match="Bot objects cannot be pickled"):
pickle.dumps(bot)
@pytest.mark.parametrize(
"bot_method_name",
argvalues=[
name
for name, _ in inspect.getmembers(Bot, predicate=inspect.isfunction)
if not name.startswith("_")
and name
not in [
"de_json",
"de_list",
"to_dict",
"to_json",
"parse_data",
"get_updates",
"getUpdates",
"get_bot",
"set_bot",
"initialize",
"shutdown",
]
],
)
async def test_defaults_handling(self, bot_method_name, bot, raw_bot, monkeypatch):
@bot_methods(ext_bot=False)
async def test_defaults_handling(
self, bot_class, bot_method_name, bot_method, bot, raw_bot, monkeypatch
):
"""
Here we check that the bot methods handle tg.ext.Defaults correctly. This has two parts:
@@ -408,6 +403,9 @@ class TestBot:
Finally, there are some tests for Defaults.{parse_mode, quote, allow_sending_without_reply}
at the appropriate places, as those are the only things we can actually check.
"""
if bot_method_name.lower().replace("_", "") == "getupdates":
return
try:
# Check that ExtBot does the right thing
bot_method = getattr(bot, bot_method_name)
@@ -483,9 +481,9 @@ class TestBot:
corresponding methods of tg.Bot.
"""
# Some methods of ext.ExtBot
global_extra_args = set()
global_extra_args = {"rate_limit_args"}
extra_args_per_method = defaultdict(
set, {"__init__": {"arbitrary_callback_data", "defaults"}}
set, {"__init__": {"arbitrary_callback_data", "defaults", "rate_limiter"}}
)
different_hints_per_method = defaultdict(set, {"__setattr__": {"ext_bot"}})
@@ -958,30 +956,18 @@ class TestBot:
assert not unprotected_dice.has_protected_content
@flaky(3, 1)
@pytest.mark.parametrize(
"chat_action",
[
ChatAction.FIND_LOCATION,
ChatAction.RECORD_VIDEO,
ChatAction.RECORD_VIDEO_NOTE,
ChatAction.RECORD_VOICE,
ChatAction.TYPING,
ChatAction.UPLOAD_DOCUMENT,
ChatAction.UPLOAD_PHOTO,
ChatAction.UPLOAD_VIDEO,
ChatAction.UPLOAD_VIDEO_NOTE,
ChatAction.UPLOAD_VOICE,
ChatAction.CHOOSE_STICKER,
],
)
@pytest.mark.parametrize("chat_action", list(ChatAction))
async def test_send_chat_action(self, bot, chat_id, chat_action):
assert await bot.send_chat_action(chat_id, chat_action)
async def test_wrong_chat_action(self, bot, chat_id):
with pytest.raises(BadRequest, match="Wrong parameter action"):
await bot.send_chat_action(chat_id, "unknown action")
@pytest.mark.asyncio
async def test_answer_web_app_query(self, bot, monkeypatch):
params = False
# For now just test that our internals pass the correct data
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
@@ -2253,8 +2239,8 @@ class TestBot:
)
# get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set,
# set_sticker_position_in_set and delete_sticker_from_set are tested in the
# test_sticker module.
# set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers
# are tested in the test_sticker module.
async def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id):
# Use BaseException that's not a subclass of Exception such that
@@ -2923,24 +2909,47 @@ class TestBot:
bot.callback_data_cache.clear_callback_data()
bot.callback_data_cache.clear_callback_queries()
def test_camel_case_redefinition_extbot(self):
invalid_camel_case_functions = []
for function_name, function in ExtBot.__dict__.items():
camel_case_function = getattr(ExtBot, to_camel_case(function_name), False)
if callable(function) and camel_case_function and camel_case_function is not function:
invalid_camel_case_functions.append(function_name)
assert invalid_camel_case_functions == []
@bot_methods()
def test_camel_case_aliases(self, bot_class, bot_method_name, bot_method):
camel_case_name = to_camel_case(bot_method_name)
camel_case_function = getattr(bot_class, camel_case_name, False)
assert camel_case_function is not False, f"{camel_case_name} not found"
assert camel_case_function is bot_method, f"{camel_case_name} is not {bot_method}"
def test_camel_case_bot(self):
not_available_camelcase_functions = []
for function_name, function in Bot.__dict__.items():
if (
function_name.startswith("_")
or not callable(function)
or function_name in ["to_dict"]
):
continue
camel_case_function = getattr(Bot, to_camel_case(function_name), False)
if not camel_case_function:
not_available_camelcase_functions.append(function_name)
assert not_available_camelcase_functions == []
@bot_methods()
def test_coroutine_functions(self, bot_class, bot_method_name, bot_method):
"""Check that all bot methods are defined as async def ..."""
# not islower() skips the camelcase aliases
if not bot_method_name.islower():
return
# unfortunately `inspect.iscoroutinefunction` doesn't do the trick directly,
# as the @_log decorator interferes
source = "".join(inspect.getsourcelines(bot_method)[0])
assert (
f"async def {bot_method_name}" in source
), f"{bot_method_name} should be a coroutine function"
@bot_methods()
def test_api_kwargs_and_timeouts_present(self, bot_class, bot_method_name, bot_method):
"""Check that all bot methods have `api_kwargs` and timeout params."""
param_names = inspect.signature(bot_method).parameters.keys()
assert (
"pool_timeout" in param_names
), f"{bot_method_name} is missing the parameter `pool_timeout`"
assert (
"read_timeout" in param_names
), f"{bot_method_name} is missing the parameter `read_timeout`"
assert (
"connect_timeout" in param_names
), f"{bot_method_name} is missing the parameter `connect_timeout`"
assert (
"write_timeout" in param_names
), f"{bot_method_name} is missing the parameter `write_timeout`"
assert (
"api_kwargs" in param_names
), f"{bot_method_name} is missing the parameter `api_kwargs`"
if bot_class is ExtBot and bot_method_name.replace("_", "").lower() != "getupdates":
assert (
"rate_limit_args" in param_names
), f"{bot_method_name} of ExtBot is missing the parameter `rate_limit_args`"
+18 -3
View File
@@ -30,7 +30,7 @@ from telegram import (
User,
)
from telegram.error import TelegramError
from telegram.ext import ApplicationBuilder, CallbackContext
from telegram.ext import ApplicationBuilder, CallbackContext, Job
"""
CallbackContext.refresh_data is tested in TestBasePersistence
@@ -116,11 +116,10 @@ class TestCallbackContext:
update = Update(
0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False))
)
job = object()
coroutine = object()
callback_context = CallbackContext.from_error(
update=update, error=error, application=app, job=job, coroutine=coroutine
update=update, error=error, application=app, coroutine=coroutine
)
assert callback_context.error is error
@@ -131,6 +130,22 @@ class TestCallbackContext:
assert callback_context.job_queue is app.job_queue
assert callback_context.update_queue is app.update_queue
assert callback_context.coroutine is coroutine
def test_from_error_job_user_chat_data(self, app):
error = TelegramError("test")
job = Job(callback=lambda x: x, data=None, chat_id=42, user_id=43)
callback_context = CallbackContext.from_error(
update=None, error=error, application=app, job=job
)
assert callback_context.error is error
assert callback_context.chat_data == {}
assert callback_context.user_data == {}
assert callback_context.bot_data is app.bot_data
assert callback_context.bot is app.bot
assert callback_context.job_queue is app.job_queue
assert callback_context.update_queue is app.update_queue
assert callback_context.job is job
def test_match(self, app):
+13
View File
@@ -44,6 +44,7 @@ def chat(bot):
has_protected_content=True,
join_to_send_messages=True,
join_by_request=True,
has_restricted_voice_and_video_messages=True,
)
@@ -68,6 +69,7 @@ class TestChat:
has_private_forwards = True
join_to_send_messages = True
join_by_request = True
has_restricted_voice_and_video_messages = True
def test_slot_behaviour(self, chat, mro_slots):
for attr in chat.__slots__:
@@ -92,6 +94,9 @@ class TestChat:
"location": self.location.to_dict(),
"join_to_send_messages": self.join_to_send_messages,
"join_by_request": self.join_by_request,
"has_restricted_voice_and_video_messages": (
self.has_restricted_voice_and_video_messages
),
}
chat = Chat.de_json(json_dict, bot)
@@ -112,6 +117,10 @@ class TestChat:
assert chat.location.address == self.location.address
assert chat.join_to_send_messages == self.join_to_send_messages
assert chat.join_by_request == self.join_by_request
assert (
chat.has_restricted_voice_and_video_messages
== self.has_restricted_voice_and_video_messages
)
def test_to_dict(self, chat):
chat_dict = chat.to_dict()
@@ -131,6 +140,10 @@ class TestChat:
assert chat_dict["location"] == chat.location.to_dict()
assert chat_dict["join_to_send_messages"] == chat.join_to_send_messages
assert chat_dict["join_by_request"] == chat.join_by_request
assert (
chat_dict["has_restricted_voice_and_video_messages"]
== chat.has_restricted_voice_and_video_messages
)
def test_enum_init(self):
chat = Chat(id=1, type="foo")
+18 -3
View File
@@ -17,13 +17,12 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import json
from enum import IntEnum
import pytest
from flaky import flaky
from telegram import constants
from telegram._utils.enum import StringEnum
from telegram._utils.enum import IntEnum, StringEnum
from telegram.error import BadRequest
from tests.conftest import data_file
@@ -62,8 +61,24 @@ class TestConstants:
assert json.dumps(IntEnumTest.FOO) == json.dumps(1)
def test_string_representation(self):
# test __repr__
assert repr(StrEnumTest.FOO) == "<StrEnumTest.FOO>"
assert str(StrEnumTest.FOO) == "StrEnumTest.FOO"
# test __format__
assert f"{StrEnumTest.FOO} this {StrEnumTest.BAR}" == "foo this bar"
assert f"{StrEnumTest.FOO:*^10}" == "***foo****"
# test __str__
assert str(StrEnumTest.FOO) == "foo"
def test_int_representation(self):
# test __repr__
assert repr(IntEnumTest.FOO) == "<IntEnumTest.FOO>"
# test __format__
assert f"{IntEnumTest.FOO}/0 is undefined!" == "1/0 is undefined!"
assert f"{IntEnumTest.FOO:*^10}" == "****1*****"
# test __str__
assert str(IntEnumTest.FOO) == "1"
def test_string_inheritance(self):
assert isinstance(StrEnumTest.FOO, str)
+6
View File
@@ -740,6 +740,12 @@ class TestConversationHandler:
],
bot=bot,
)
# First check that updates without user won't be handled
message.from_user = None
assert not handler.check_update(Update(update_id=0, message=message))
message.from_user = user1
async with app:
await app.process_update(Update(update_id=0, message=message))
+4 -4
View File
@@ -48,16 +48,16 @@ Because imports in pytest are intricate, we just run
pytest -k test_helpers.py
with the TEST_NO_PYTZ environment variable set in addition to the regular test suite.
with the TEST_PYTZ environment variable set to False in addition to the regular test suite.
Because actually uninstalling pytz would lead to errors in the test suite we just mock the
import to raise the expected exception.
Note that a fixture that just does this for every test that needs it is a nice idea, but for some
reason makes test_updater.py hang indefinitely on GitHub Actions (at least when Hinrich tried that)
"""
TEST_NO_PYTZ = env_var_2_bool(os.getenv("TEST_NO_PYTZ", False))
TEST_PYTZ = env_var_2_bool(os.getenv("TEST_PYTZ", True))
if TEST_NO_PYTZ:
if not TEST_PYTZ:
orig_import = __import__
def import_mock(module_name, *args, **kwargs):
@@ -72,7 +72,7 @@ if TEST_NO_PYTZ:
class TestDatetime:
def test_helpers_utc(self):
# Here we just test, that we got the correct UTC variant
if TEST_NO_PYTZ:
if not TEST_PYTZ:
assert tg_dtm.UTC is tg_dtm.DTM_UTC
else:
assert tg_dtm.UTC is not tg_dtm.DTM_UTC
+1 -1
View File
@@ -828,7 +828,7 @@ class TestFilters:
def test_filters_sticker(self, update):
assert not filters.Sticker.ALL.check_update(update)
update.message.sticker = Sticker("1", "uniq", 1, 2, False, False)
update.message.sticker = Sticker("1", "uniq", 1, 2, False, False, Sticker.REGULAR)
assert filters.Sticker.ALL.check_update(update)
assert filters.Sticker.STATIC.check_update(update)
assert not filters.Sticker.VIDEO.check_update(update)
+52 -24
View File
@@ -25,32 +25,50 @@ from telegram.constants import MessageType
class TestHelpers:
def test_escape_markdown(self):
test_str = "*bold*, _italic_, `code`, [text_link](http://github.com/)"
expected_str = r"\*bold\*, \_italic\_, \`code\`, \[text\_link](http://github.com/)"
@pytest.mark.parametrize(
"test_str,expected",
[
("*bold*", r"\*bold\*"),
("_italic_", r"\_italic\_"),
("`code`", r"\`code\`"),
("[text_link](https://github.com/)", r"\[text\_link](https://github.com/)"),
],
ids=["bold", "italic", "code", "text_link"],
)
def test_escape_markdown(self, test_str, expected):
assert expected == helpers.escape_markdown(test_str)
assert expected_str == helpers.escape_markdown(test_str)
@pytest.mark.parametrize(
"test_str, expected",
[
(r"a_b*c[d]e", r"a\_b\*c\[d\]e"),
(r"(fg) ", r"\(fg\) "),
(r"h~I`>JK#L+MN", r"h\~I\`\>JK\#L\+MN"),
(r"-O=|p{qr}s.t!\ ", r"\-O\=\|p\{qr\}s\.t\!\\ "),
(r"\u", r"\\u"),
],
)
def test_escape_markdown_v2(self, test_str, expected):
assert expected == helpers.escape_markdown(test_str, version=2)
def test_escape_markdown_v2(self):
test_str = r"a_b*c[d]e (fg) h~I`>JK#L+MN -O=|p{qr}s.t!\ \u"
expected_str = r"a\_b\*c\[d\]e \(fg\) h\~I\`\>JK\#L\+MN \-O\=\|p\{qr\}s\.t\!\\ \\u"
assert expected_str == helpers.escape_markdown(test_str, version=2)
def test_escape_markdown_v2_monospaced(self):
test_str = r"mono/pre: `abc` \int (`\some \`stuff)"
expected_str = "mono/pre: \\`abc\\` \\\\int (\\`\\\\some \\\\\\`stuff)"
assert expected_str == helpers.escape_markdown(
@pytest.mark.parametrize(
"test_str, expected",
[
(r"mono/pre:", r"mono/pre:"),
("`abc`", r"\`abc\`"),
(r"\int", r"\\int"),
(r"(`\some \` stuff)", r"(\`\\some \\\` stuff)"),
],
)
def test_escape_markdown_v2_monospaced(self, test_str, expected):
assert expected == helpers.escape_markdown(
test_str, version=2, entity_type=MessageEntity.PRE
)
assert expected_str == helpers.escape_markdown(
assert expected == helpers.escape_markdown(
test_str, version=2, entity_type=MessageEntity.CODE
)
def test_escape_markdown_v2_text_link(self):
test_str = "https://url.containing/funny)cha)\\ra\\)cter\\s"
expected_str = "https://url.containing/funny\\)cha\\)\\\\ra\\\\\\)cter\\\\s"
@@ -59,8 +77,10 @@ class TestHelpers:
)
def test_markdown_invalid_version(self):
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Markdown version must be either"):
helpers.escape_markdown("abc", version=-1)
with pytest.raises(ValueError, match="Markdown version must be either"):
helpers.mention_markdown(1, "abc", version=-1)
def test_create_deep_linked_url(self):
username = "JamesTheMock"
@@ -124,12 +144,20 @@ class TestHelpers:
assert expected == helpers.mention_html(1, "the name")
def test_mention_markdown(self):
expected = "[the name](tg://user?id=1)"
assert expected == helpers.mention_markdown(1, "the name")
@pytest.mark.parametrize(
"test_str, expected",
[
("the name", "[the name](tg://user?id=1)"),
("under_score", "[under_score](tg://user?id=1)"),
("starred*text", "[starred*text](tg://user?id=1)"),
("`backtick`", "[`backtick`](tg://user?id=1)"),
("[square brackets", "[[square brackets](tg://user?id=1)"),
],
)
def test_mention_markdown(self, test_str, expected):
assert expected == helpers.mention_markdown(1, test_str)
def test_mention_markdown_2(self):
expected = r"[the\_name](tg://user?id=1)"
assert expected == helpers.mention_markdown(1, "the_name")
assert expected == helpers.mention_markdown(1, "the_name", 2)
+9 -2
View File
@@ -86,7 +86,12 @@ class TestJobQueue:
self.job_time = time.time()
async def error_handler_context(self, update, context):
self.received_error = (str(context.error), context.job)
self.received_error = (
str(context.error),
context.job,
context.user_data,
context.chat_data,
)
async def error_handler_raise_error(self, *args):
raise Exception("Failing bigly")
@@ -453,7 +458,7 @@ class TestJobQueue:
async def test_process_error_context(self, job_queue, app):
app.add_error_handler(self.error_handler_context)
job = job_queue.run_once(self.job_with_exception, 0.1)
job = job_queue.run_once(self.job_with_exception, 0.1, chat_id=42, user_id=43)
await asyncio.sleep(0.15)
assert self.received_error[0] == "Test Error"
assert self.received_error[1] is job
@@ -461,6 +466,8 @@ class TestJobQueue:
await job.run(app)
assert self.received_error[0] == "Test Error"
assert self.received_error[1] is job
assert self.received_error[2] is app.user_data[43]
assert self.received_error[3] is app.chat_data[42]
# Remove handler
app.remove_error_handler(self.error_handler_context)
+61 -1
View File
@@ -105,7 +105,7 @@ def message(bot):
)
},
{"photo": [PhotoSize("photo_id", "unique_id", 50, 50)], "caption": "photo_file"},
{"sticker": Sticker("sticker_id", "unique_id", 50, 50, True, False)},
{"sticker": Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR)},
{"video": Video("video_id", "unique_id", 12, 12, 12), "caption": "video_file"},
{"voice": Voice("voice_id", "unique_id", 5)},
{"video_note": VideoNote("video_note_id", "unique_id", 20, 12)},
@@ -542,6 +542,36 @@ class TestMessage:
)
assert expected == message.text_markdown
@pytest.mark.parametrize(
"type_",
argvalues=[
"text_html",
"text_html_urled",
"text_markdown",
"text_markdown_urled",
"text_markdown_v2",
"text_markdown_v2_urled",
],
)
def test_text_custom_emoji(self, type_):
text = "Look a custom emoji: 😎"
expected = "Look a custom emoji: 😎"
emoji_entity = MessageEntity(
type=MessageEntity.CUSTOM_EMOJI,
offset=21,
length=2,
custom_emoji_id="5472409228461217725",
)
message = Message(
1,
from_user=self.from_user,
date=self.date,
chat=self.chat,
text=text,
entities=[emoji_entity],
)
assert expected == message[type_]
def test_caption_html_simple(self):
test_html_string = (
"<u>Test</u> for &lt;<b>bold</b>, <i>ita_lic</i>, "
@@ -651,6 +681,36 @@ class TestMessage:
)
assert expected == message.caption_markdown
@pytest.mark.parametrize(
"type_",
argvalues=[
"caption_html",
"caption_html_urled",
"caption_markdown",
"caption_markdown_urled",
"caption_markdown_v2",
"caption_markdown_v2_urled",
],
)
def test_caption_custom_emoji(self, type_):
caption = "Look a custom emoji: 😎"
expected = "Look a custom emoji: 😎"
emoji_entity = MessageEntity(
type=MessageEntity.CUSTOM_EMOJI,
offset=21,
length=2,
custom_emoji_id="5472409228461217725",
)
message = Message(
1,
from_user=self.from_user,
date=self.date,
chat=self.chat,
caption=caption,
caption_entities=[emoji_entity],
)
assert expected == message[type_]
async def test_parse_entities_url_emoji(self):
url = b"http://github.com/?unicode=\\u2713\\U0001f469".decode("unicode-escape")
text = "some url"
+8 -8
View File
@@ -24,7 +24,7 @@ Currently this only means that cryptography is not installed.
Because imports in pytest are intricate, we just run
pytest -k test_no_passport.py
with the TEST_NO_PASSPORT environment variable set in addition to the regular test suite.
with the TEST_PASSPORT environment variable set to False in addition to the regular test suite.
Because actually uninstalling the optional dependencies would lead to errors in the test suite we
just mock the import to raise the expected exception.
@@ -41,9 +41,9 @@ from telegram import _bot as bot
from telegram._passport import credentials as credentials
from tests.conftest import env_var_2_bool
TEST_NO_PASSPORT = env_var_2_bool(os.getenv("TEST_NO_PASSPORT", False))
TEST_PASSPORT = env_var_2_bool(os.getenv("TEST_PASSPORT", True))
if TEST_NO_PASSPORT:
if not TEST_PASSPORT:
orig_import = __import__
def import_mock(module_name, *args, **kwargs):
@@ -58,24 +58,24 @@ if TEST_NO_PASSPORT:
class TestNoPassport:
"""
The monkeypatches simulate cryptography not being installed even when TEST_NO_PASSPORT is
False, though that doesn't test the actual imports
The monkeypatches simulate cryptography not being installed even when TEST_PASSPORT is
True, though that doesn't test the actual imports
"""
def test_bot_init(self, bot_info, monkeypatch):
if not TEST_NO_PASSPORT:
if TEST_PASSPORT:
monkeypatch.setattr(bot, "CRYPTO_INSTALLED", False)
with pytest.raises(RuntimeError, match="passport"):
bot.Bot(bot_info["token"], private_key=1, private_key_password=2)
def test_credentials_decrypt(self, monkeypatch):
if not TEST_NO_PASSPORT:
if TEST_PASSPORT:
monkeypatch.setattr(credentials, "CRYPTO_INSTALLED", False)
with pytest.raises(RuntimeError, match="passport"):
credentials.decrypt(1, 1, 1)
def test_encrypted_credentials_decrypted_secret(self, monkeypatch):
if not TEST_NO_PASSPORT:
if TEST_PASSPORT:
monkeypatch.setattr(credentials, "CRYPTO_INSTALLED", False)
ec = credentials.EncryptedCredentials("data", "hash", "secret")
with pytest.raises(RuntimeError, match="passport"):
+9 -1
View File
@@ -468,7 +468,15 @@ class TestPhoto:
b = PhotoSize("", photo.file_unique_id, self.width, self.height)
c = PhotoSize(photo.file_id, photo.file_unique_id, 0, 0)
d = PhotoSize("", "", self.width, self.height)
e = Sticker(photo.file_id, photo.file_unique_id, self.width, self.height, False, False)
e = Sticker(
photo.file_id,
photo.file_unique_id,
self.width,
self.height,
False,
False,
Sticker.REGULAR,
)
assert a == b
assert hash(a) == hash(b)
+364
View File
@@ -0,0 +1,364 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2022
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""
We mostly test on directly on AIORateLimiter here, b/c BaseRateLimiter doesn't contain anything
notable
"""
import asyncio
import json
import os
import platform
import time
from datetime import datetime
from http import HTTPStatus
import pytest
from flaky import flaky
from telegram import BotCommand, Chat, Message, User
from telegram.constants import ParseMode
from telegram.error import RetryAfter
from telegram.ext import AIORateLimiter, BaseRateLimiter, Defaults, ExtBot
from telegram.request import BaseRequest, RequestData
from tests.conftest import env_var_2_bool
TEST_RATE_LIMITER = env_var_2_bool(os.getenv("TEST_RATE_LIMITER", True))
@pytest.mark.skipif(
TEST_RATE_LIMITER, reason="Only relevant if the optional dependency is not installed"
)
class TestNoRateLimiter:
def test_init(self):
with pytest.raises(RuntimeError, match=r"python-telegram-bot\[rate-limiter\]"):
AIORateLimiter()
class TestBaseRateLimiter:
rl_received = None
request_received = None
async def test_no_rate_limiter(self, bot):
with pytest.raises(ValueError, match="if a `ExtBot.rate_limiter` is set"):
await bot.send_message(chat_id=42, text="test", rate_limit_args="something")
async def test_argument_passing(self, bot_info, monkeypatch, bot):
class TestRateLimiter(BaseRateLimiter):
async def initialize(self) -> None:
pass
async def shutdown(self) -> None:
pass
async def process_request(
self,
callback,
args,
kwargs,
endpoint,
data,
rate_limit_args,
):
if TestBaseRateLimiter.rl_received is None:
TestBaseRateLimiter.rl_received = []
TestBaseRateLimiter.rl_received.append((endpoint, data, rate_limit_args))
return await callback(*args, **kwargs)
class TestRequest(BaseRequest):
async def initialize(self) -> None:
pass
async def shutdown(self) -> None:
pass
async def do_request(self, *args, **kwargs):
if TestBaseRateLimiter.request_received is None:
TestBaseRateLimiter.request_received = []
TestBaseRateLimiter.request_received.append((args, kwargs))
# return bot.bot.to_dict() for the `get_me` call in `Bot.initialize`
return 200, json.dumps({"ok": True, "result": bot.bot.to_dict()}).encode()
defaults = Defaults(parse_mode=ParseMode.HTML)
test_request = TestRequest()
standard_bot = ExtBot(token=bot.token, defaults=defaults, request=test_request)
rl_bot = ExtBot(
token=bot.token,
defaults=defaults,
request=test_request,
rate_limiter=TestRateLimiter(),
)
async with standard_bot:
await standard_bot.set_my_commands(
commands=[BotCommand("test", "test")],
language_code="en",
api_kwargs={"api": "kwargs"},
)
async with rl_bot:
await rl_bot.set_my_commands(
commands=[BotCommand("test", "test")],
language_code="en",
rate_limit_args=(43, "test-1"),
api_kwargs={"api": "kwargs"},
)
assert len(self.rl_received) == 2
assert self.rl_received[0] == ("getMe", {}, None)
assert self.rl_received[1] == (
"setMyCommands",
dict(commands=[BotCommand("test", "test")], language_code="en", api="kwargs"),
(43, "test-1"),
)
assert len(self.request_received) == 4
# self.request_received[i] = i-th received request
# self.request_received[i][0] = i-th received request's args
# self.request_received[i][1] = i-th received request's kwargs
assert self.request_received[0][1]["url"].endswith("getMe")
assert self.request_received[2][1]["url"].endswith("getMe")
assert self.request_received[1][0] == self.request_received[3][0]
assert self.request_received[1][1].keys() == self.request_received[3][1].keys()
for key, value in self.request_received[1][1].items():
if isinstance(value, RequestData):
assert value.parameters == self.request_received[3][1][key].parameters
assert value.parameters["api"] == "kwargs"
else:
assert value == self.request_received[3][1][key]
@pytest.mark.skipif(
not TEST_RATE_LIMITER, reason="Only relevant if the optional dependency is installed"
)
@pytest.mark.skipif(
os.getenv("GITHUB_ACTIONS", False) and platform.system() == "Darwin",
reason="The timings are apparently rather inaccurate on MacOS.",
)
@flaky(10, 1) # Timings aren't quite perfect
class TestAIORateLimiter:
count = 0
call_times = []
class CountRequest(BaseRequest):
def __init__(self, retry_after=None):
self.retry_after = retry_after
async def initialize(self) -> None:
pass
async def shutdown(self) -> None:
pass
async def do_request(self, *args, **kwargs):
TestAIORateLimiter.count += 1
TestAIORateLimiter.call_times.append(time.time())
if self.retry_after:
raise RetryAfter(retry_after=1)
url = kwargs.get("url").lower()
if url.endswith("getme"):
return (
HTTPStatus.OK,
json.dumps(
{"ok": True, "result": User(id=1, first_name="bot", is_bot=True).to_dict()}
).encode(),
)
if url.endswith("sendmessage"):
return (
HTTPStatus.OK,
json.dumps(
{
"ok": True,
"result": Message(
message_id=1, date=datetime.now(), chat=Chat(1, "chat")
).to_dict(),
}
).encode(),
)
@pytest.fixture(autouse=True)
def reset(self):
self.count = 0
TestAIORateLimiter.count = 0
self.call_times = []
TestAIORateLimiter.call_times = []
@pytest.mark.parametrize("max_retries", [0, 1, 4])
async def test_max_retries(self, bot, max_retries):
bot = ExtBot(
token=bot.token,
request=self.CountRequest(retry_after=1),
rate_limiter=AIORateLimiter(
max_retries=max_retries, overall_max_rate=0, group_max_rate=0
),
)
with pytest.raises(RetryAfter):
await bot.get_me()
# Check that we retried the request the correct number of times
assert TestAIORateLimiter.count == max_retries + 1
# Check that the retries were delayed correctly
times = TestAIORateLimiter.call_times
if len(times) <= 1:
return
delays = [j - i for i, j in zip(times[:-1], times[1:])]
assert delays == pytest.approx([1.1 for _ in range(max_retries)], rel=0.05)
async def test_delay_all_pending_on_retry(self, bot):
# Makes sure that a RetryAfter blocks *all* pending requests
bot = ExtBot(
token=bot.token,
request=self.CountRequest(retry_after=1),
rate_limiter=AIORateLimiter(max_retries=1, overall_max_rate=0, group_max_rate=0),
)
task_1 = asyncio.create_task(bot.get_me())
await asyncio.sleep(0.1)
task_2 = asyncio.create_task(bot.get_me())
assert not task_1.done()
assert not task_2.done()
await asyncio.sleep(1.1)
assert isinstance(task_1.exception(), RetryAfter)
assert not task_2.done()
await asyncio.sleep(1.1)
assert isinstance(task_2.exception(), RetryAfter)
@pytest.mark.parametrize("group_id", [-1, "-1", "@username"])
@pytest.mark.parametrize("chat_id", [1, "1"])
async def test_basic_rate_limiting(self, bot, group_id, chat_id):
try:
rl_bot = ExtBot(
token=bot.token,
request=self.CountRequest(retry_after=None),
rate_limiter=AIORateLimiter(
overall_max_rate=1,
overall_time_period=1 / 4,
group_max_rate=1,
group_time_period=1 / 2,
),
)
async with rl_bot:
non_group_tasks = {}
group_tasks = {}
for i in range(4):
group_tasks[i] = asyncio.create_task(
rl_bot.send_message(chat_id=group_id, text="test")
)
for i in range(8):
non_group_tasks[i] = asyncio.create_task(
rl_bot.send_message(chat_id=chat_id, text="test")
)
await asyncio.sleep(0.85)
# We expect 5 requests:
# 1: `get_me` from `async with rl_bot`
# 2: `send_message` at time 0.00
# 3: `send_message` at time 0.25
# 4: `send_message` at time 0.50
# 5: `send_message` at time 0.75
assert TestAIORateLimiter.count == 5
assert sum(1 for task in non_group_tasks.values() if task.done()) < 8
assert sum(1 for task in group_tasks.values() if task.done()) < 4
# 3 seconds after start
await asyncio.sleep(3.1 - 0.85)
assert all(task.done() for task in non_group_tasks.values())
assert all(task.done() for task in group_tasks.values())
finally:
# cleanup
await asyncio.gather(*non_group_tasks.values(), *group_tasks.values())
TestAIORateLimiter.count = 0
TestAIORateLimiter.call_times = []
async def test_rate_limiting_no_chat_id(self, bot):
try:
rl_bot = ExtBot(
token=bot.token,
request=self.CountRequest(retry_after=None),
rate_limiter=AIORateLimiter(
overall_max_rate=1,
overall_time_period=1 / 2,
),
)
async with rl_bot:
non_chat_tasks = {}
chat_tasks = {}
for i in range(4):
chat_tasks[i] = asyncio.create_task(
rl_bot.send_message(chat_id=-1, text="test")
)
for i in range(8):
non_chat_tasks[i] = asyncio.create_task(rl_bot.get_me())
await asyncio.sleep(0.6)
# We expect 11 requests:
# 1: `get_me` from `async with rl_bot`
# 2: `send_message` at time 0.00
# 3: `send_message` at time 0.05
# 4: 8 times `get_me`
assert TestAIORateLimiter.count == 11
assert sum(1 for task in non_chat_tasks.values() if task.done()) == 8
assert sum(1 for task in chat_tasks.values() if task.done()) == 2
# 1.6 seconds after start
await asyncio.sleep(1.6 - 0.6)
assert all(task.done() for task in non_chat_tasks.values())
assert all(task.done() for task in chat_tasks.values())
finally:
# cleanup
await asyncio.gather(*non_chat_tasks.values(), *chat_tasks.values())
TestAIORateLimiter.count = 0
TestAIORateLimiter.call_times = []
@pytest.mark.parametrize("intermediate", [True, False])
async def test_group_caching(self, bot, intermediate):
try:
max_rate = 1000
rl_bot = ExtBot(
token=bot.token,
request=self.CountRequest(retry_after=None),
rate_limiter=AIORateLimiter(
overall_max_rate=max_rate,
overall_time_period=1,
group_max_rate=max_rate,
group_time_period=1,
),
)
# Unfortunately, there is no reliable way to test this without checking the internals
assert len(rl_bot.rate_limiter._group_limiters) == 0
await asyncio.gather(
*(rl_bot.send_message(chat_id=-(i + 1), text=f"{i}") for i in range(513))
)
if intermediate:
await rl_bot.send_message(chat_id=-1, text="999")
assert 1 <= len(rl_bot.rate_limiter._group_limiters) <= 513
else:
await asyncio.sleep(1)
await rl_bot.send_message(chat_id=-1, text="999")
assert len(rl_bot.rate_limiter._group_limiters) == 1
finally:
TestAIORateLimiter.count = 0
TestAIORateLimiter.call_times = []
+1 -1
View File
@@ -240,7 +240,7 @@ class TestRequest:
)
with pytest.raises(exception_class, match="Test Message"):
await httpx_request.post(None, None, None)
await httpx_request.post("", None, None)
@pytest.mark.parametrize(
["exception", "catch_class", "match"],
+113 -14
View File
@@ -87,6 +87,8 @@ class TestSticker:
thumb_width = 319
thumb_height = 320
thumb_file_size = 21472
type = Sticker.REGULAR
custom_emoji_id = "ThisIsSuchACustomEmojiID"
sticker_file_id = "5a3128a4d2a04750b5b58397f3b5e812"
sticker_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e"
@@ -120,6 +122,7 @@ class TestSticker:
assert sticker.thumb.width == self.thumb_width
assert sticker.thumb.height == self.thumb_height
assert sticker.thumb.file_size == self.thumb_file_size
assert sticker.type == self.type
# we need to be a premium TG user to send a premium sticker, so the below is not tested
# assert sticker.premium_animation == self.premium_animation
@@ -139,6 +142,8 @@ class TestSticker:
assert message.sticker.is_animated == sticker.is_animated
assert message.sticker.is_video == sticker.is_video
assert message.sticker.file_size == sticker.file_size
assert message.sticker.type == sticker.type
assert message.has_protected_content
# we need to be a premium TG user to send a premium sticker, so the below is not tested
# assert message.sticker.premium_animation == sticker.premium_animation
@@ -150,7 +155,6 @@ class TestSticker:
assert message.sticker.thumb.width == sticker.thumb.width
assert message.sticker.thumb.height == sticker.thumb.height
assert message.sticker.thumb.file_size == sticker.thumb.file_size
assert message.has_protected_content
@flaky(3, 1)
async def test_get_and_download(self, bot, sticker):
@@ -197,6 +201,7 @@ class TestSticker:
assert message.sticker.is_animated == sticker.is_animated
assert message.sticker.is_video == sticker.is_video
assert message.sticker.file_size == sticker.file_size
assert message.sticker.type == sticker.type
assert isinstance(message.sticker.thumb, PhotoSize)
assert isinstance(message.sticker.thumb.file_id, str)
@@ -219,6 +224,8 @@ class TestSticker:
"emoji": self.emoji,
"file_size": self.file_size,
"premium_animation": self.premium_animation.to_dict(),
"type": self.type,
"custom_emoji_id": self.custom_emoji_id,
}
json_sticker = Sticker.de_json(json_dict, bot)
@@ -232,6 +239,8 @@ class TestSticker:
assert json_sticker.file_size == self.file_size
assert json_sticker.thumb == sticker.thumb
assert json_sticker.premium_animation == self.premium_animation
assert json_sticker.type == self.type
assert json_sticker.custom_emoji_id == self.custom_emoji_id
async def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
@@ -310,6 +319,7 @@ class TestSticker:
assert sticker_dict["is_video"] == sticker.is_video
assert sticker_dict["file_size"] == sticker.file_size
assert sticker_dict["thumb"] == sticker.thumb.to_dict()
assert sticker_dict["type"] == sticker.type
@flaky(3, 1)
async def test_error_send_empty_file(self, bot, chat_id):
@@ -343,6 +353,16 @@ class TestSticker:
}
assert premium_sticker.premium_animation.to_dict() == premium_sticker_dict
@flaky(3, 1)
async def test_custom_emoji(self, bot):
# testing custom emoji stickers is as much of an annoyance as the premium animation, see
# in test_premium_animation
custom_emoji_set = await bot.get_sticker_set("PTBStaticEmojiTestPack")
# the first one to appear here is a sticker with unique file id of AQADjBsAAkKD0Uty
# this could change in the future ofc.
custom_emoji_sticker = custom_emoji_set.stickers[0]
assert custom_emoji_sticker.custom_emoji_id == "6046140249875156202"
def test_equality(self, sticker):
a = Sticker(
sticker.file_id,
@@ -351,14 +371,41 @@ class TestSticker:
self.height,
self.is_animated,
self.is_video,
self.type,
)
b = Sticker(
"", sticker.file_unique_id, self.width, self.height, self.is_animated, self.is_video
"",
sticker.file_unique_id,
self.width,
self.height,
self.is_animated,
self.is_video,
self.type,
)
c = Sticker(
sticker.file_id,
sticker.file_unique_id,
0,
0,
False,
True,
self.type,
)
d = Sticker(
"",
"",
self.width,
self.height,
self.is_animated,
self.is_video,
self.type,
)
c = Sticker(sticker.file_id, sticker.file_unique_id, 0, 0, False, True)
d = Sticker("", "", self.width, self.height, self.is_animated, self.is_video)
e = PhotoSize(
sticker.file_id, sticker.file_unique_id, self.width, self.height, self.is_animated
sticker.file_id,
sticker.file_unique_id,
self.width,
self.height,
self.is_animated,
)
assert a == b
@@ -427,9 +474,9 @@ class TestStickerSet:
title = "Test stickers"
is_animated = True
is_video = True
contains_masks = False
stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True)]
stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True, Sticker.REGULAR)]
name = "NOTAREALNAME"
sticker_type = Sticker.REGULAR
def test_de_json(self, bot, sticker):
name = f"test_by_{bot.username}"
@@ -438,9 +485,9 @@ class TestStickerSet:
"title": self.title,
"is_animated": self.is_animated,
"is_video": self.is_video,
"contains_masks": self.contains_masks,
"stickers": [x.to_dict() for x in self.stickers],
"thumb": sticker.thumb.to_dict(),
"sticker_type": self.sticker_type,
}
sticker_set = StickerSet.de_json(json_dict, bot)
@@ -448,9 +495,9 @@ class TestStickerSet:
assert sticker_set.title == self.title
assert sticker_set.is_animated == self.is_animated
assert sticker_set.is_video == self.is_video
assert sticker_set.contains_masks == self.contains_masks
assert sticker_set.stickers == self.stickers
assert sticker_set.thumb == sticker.thumb
assert sticker_set.sticker_type == self.sticker_type
async def test_create_sticker_set(
self, bot, chat_id, sticker_file, animated_sticker_file, video_sticker_file
@@ -536,8 +583,9 @@ class TestStickerSet:
assert sticker_set_dict["title"] == sticker_set.title
assert sticker_set_dict["is_animated"] == sticker_set.is_animated
assert sticker_set_dict["is_video"] == sticker_set.is_video
assert sticker_set_dict["contains_masks"] == sticker_set.contains_masks
assert sticker_set_dict["stickers"][0] == sticker_set.stickers[0].to_dict()
assert sticker_set_dict["thumb"] == sticker_set.thumb.to_dict()
assert sticker_set_dict["sticker_type"] == sticker_set.sticker_type
@flaky(3, 1)
async def test_bot_methods_2_png(self, bot, sticker_set):
@@ -639,6 +687,32 @@ class TestStickerSet:
assert test_flag
monkeypatch.delattr(bot, "_post")
async def test_create_new_sticker_all_params(self, monkeypatch, bot, chat_id, mask_position):
async def make_assertion(_, data, *args, **kwargs):
assert data["user_id"] == chat_id
assert data["name"] == "name"
assert data["title"] == "title"
assert data["emojis"] == "emoji"
assert data["mask_position"] == mask_position
assert data["png_sticker"] == "wow.png"
assert data["tgs_sticker"] == "wow.tgs"
assert data["webm_sticker"] == "wow.webm"
assert data["sticker_type"] == Sticker.MASK
monkeypatch.setattr(bot, "_post", make_assertion)
await bot.create_new_sticker_set(
chat_id,
"name",
"title",
"emoji",
mask_position=mask_position,
png_sticker="wow.png",
tgs_sticker="wow.tgs",
webm_sticker="wow.webm",
sticker_type=Sticker.MASK,
)
monkeypatch.delattr(bot, "_post")
async def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id):
# For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
@@ -685,21 +759,26 @@ class TestStickerSet:
self.name,
self.title,
self.is_animated,
self.contains_masks,
self.stickers,
self.is_video,
self.sticker_type,
)
b = StickerSet(
self.name,
self.title,
self.is_animated,
self.contains_masks,
self.stickers,
self.is_video,
self.sticker_type,
)
c = StickerSet(self.name, None, None, None, None, None)
c = StickerSet(self.name, None, None, None, None, Sticker.CUSTOM_EMOJI)
d = StickerSet(
"blah", self.title, self.is_animated, self.contains_masks, self.stickers, self.is_video
"blah",
self.title,
self.is_animated,
self.stickers,
self.is_video,
self.sticker_type,
)
e = Audio(self.name, "", 0, None, None)
@@ -775,3 +854,23 @@ class TestMaskPosition:
assert a != e
assert hash(a) != hash(e)
class TestGetCustomEmojiSticker:
async def test_custom_emoji_sticker(self, bot):
# we use the same ID as in test_custom_emoji
emoji_sticker_list = await bot.get_custom_emoji_stickers(["6046140249875156202"])
assert emoji_sticker_list[0].emoji == "😎"
assert emoji_sticker_list[0].height == 100
assert emoji_sticker_list[0].width == 100
assert not emoji_sticker_list[0].is_animated
assert not emoji_sticker_list[0].is_video
assert emoji_sticker_list[0].set_name == "PTBStaticEmojiTestPack"
assert emoji_sticker_list[0].type == Sticker.CUSTOM_EMOJI
assert emoji_sticker_list[0].custom_emoji_id == "6046140249875156202"
assert emoji_sticker_list[0].thumb.width == 100
assert emoji_sticker_list[0].thumb.height == 100
assert emoji_sticker_list[0].thumb.file_size == 3614
assert emoji_sticker_list[0].thumb.file_unique_id == "AQAD6gwAAoY06FNy"
assert emoji_sticker_list[0].file_size == 3678
assert emoji_sticker_list[0].file_unique_id == "AgAD6gwAAoY06FM"
+1 -1
View File
@@ -534,7 +534,7 @@ class TestUser:
assert user.mention_markdown() == expected.format(user.full_name, user.id)
assert user.mention_markdown("the_name*\u2022") == expected.format(
"the\\_name\\*\u2022", user.id
"the_name*\u2022", user.id
)
assert user.mention_markdown(user.username) == expected.format(user.username, user.id)