Compare commits

...

33 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
Hinrich Mahler df07148e2d Bump version to v20.0a2 2022-06-27 19:19:54 +02:00
Bibo-Joshi 2d2cede442 Documentation Improvements (#3103, #3121, #3098)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
Co-authored-by: David <dsb321mp@gmail.com>
Co-authored-by: Harshil Mehta <37377066+harshil21@users.noreply.github.com>
Co-authored-by: poolitzer <github@poolitzer.eu>
Co-authored-by: Alex <53974096+ExalFabu@users.noreply.github.com>
2022-06-27 18:58:51 +02:00
Poolitzer 08e223ba90 API 6.1 (#3112)
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2022-06-27 18:54:11 +02:00
Bibo-Joshi 01d643913e Stabilize CI (#3119) 2022-06-27 18:46:52 +02:00
Aditya Yadav 755945172d Add Additional Shortcut Methods to Chat (#3115) 2022-06-27 18:45:30 +02:00
dependabot[bot] 24b4de9f10 Bump pyupgrade from 2.32.1 to 2.34.0 (#3096)
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-06-25 11:52:39 +02:00
David 76bfe8ceff Mermaid-based Example State Diagrams (#3090)
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2022-06-24 18:15:10 +02:00
dependabot[bot] b498786d7c Bump furo from 2022.6.4 to 2022.6.4.1 (#3095)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-19 17:12:54 +02:00
dependabot[bot] bfe30048e8 Bump mypy from 0.960 to 0.961 (#3093)
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-06-19 17:12:26 +02:00
117 changed files with 6270 additions and 577 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.960
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.32.1
rev: v2.37.3
hooks:
- id: pyupgrade
files: ^(telegram|examples|tests)/.*\.py$
+3
View File
@@ -29,6 +29,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Balduro <https://github.com/Balduro>`_
- `Bibo-Joshi <https://github.com/Bibo-Joshi>`_
- `bimmlerd <https://github.com/bimmlerd>`_
- `cyc8 <https://github.com/cyc8>`_
- `d-qoi <https://github.com/d-qoi>`_
- `daimajia <https://github.com/daimajia>`_
- `Daniel Reed <https://github.com/nmlorg>`_
@@ -45,6 +46,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Evan Haberecht <https://github.com/habereet>`_
- `Evgeny Denisov <https://github.com/eIGato>`_
- `evgfilim1 <https://github.com/evgfilim1>`_
- `ExalFabu <https://github.com/ExalFabu>`_
- `franciscod <https://github.com/franciscod>`_
- `gamgi <https://github.com/gamgi>`_
- `Gauthamram Ravichandran <https://github.com/GauthamramRavichandran>`_
@@ -84,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>`_
+121
View File
@@ -2,6 +2,127 @@
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*
This is the technical changelog for version 20.0a2. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
Major Changes
-------------
- Full Support for API 6.1 (`#3112`_)
New Features
------------
- Add Additional Shortcut Methods to ``Chat`` (`#3115`_)
- Mermaid-based Example State Diagrams (`#3090`_)
Minor Changes, Documentation Improvements and CI
------------------------------------------------
- Documentation Improvements (`#3103`_, `#3121`_, `#3098`_)
- Stabilize CI (`#3119`_)
- Bump ``pyupgrade`` from 2.32.1 to 2.34.0 (`#3096`_)
- Bump ``furo`` from 2022.6.4 to 2022.6.4.1 (`#3095`_)
- Bump ``mypy`` from 0.960 to 0.961 (`#3093`_)
.. _`#3112`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3112
.. _`#3115`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3115
.. _`#3090`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3090
.. _`#3103`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3103
.. _`#3121`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3121
.. _`#3098`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3098
.. _`#3119`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3119
.. _`#3096`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3096
.. _`#3095`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3095
.. _`#3093`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3093
Version 20.0a1
==============
*Released 2022-06-09*
+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
+6 -5
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.0-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
@@ -34,7 +34,7 @@
:target: https://github.com/python-telegram-bot/python-telegram-bot/
:alt: Github Actions workflow
.. image:: https://app.codecov.io/gh/python-telegram-bot/python-telegram-bot/branch/master/graph/badge.svg
.. image:: https://codecov.io/gh/python-telegram-bot/python-telegram-bot/branch/master/graph/badge.svg
:target: https://app.codecov.io/gh/python-telegram-bot/python-telegram-bot
:alt: Code coverage
@@ -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.0** 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
===========
@@ -149,7 +150,7 @@ Resources
=========
- The `package documentation <https://docs.python-telegram-bot.org/>`_ is the technical reference for ``python-telegram-bot``.
It contains descriptions of all available classes, modules, methods and arguments.
It contains descriptions of all available classes, modules, methods and arguments as well as the `changelog <https://docs.python-telegram-bot.org/changelog.html>`_.
- The `wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ is home to number of more elaborate introductions of the different features of ``python-telegram-bot`` and other useful resources that go beyond the technical documentation.
- Our `examples section <https://docs.python-telegram-bot.org/examples.html>`_ contains several examples that showcase the different features of both the Bot API and ``python-telegram-bot``.
Even if it is not your approach for learning, please take a look at ``echobot.py``. It is the de facto base for most of the bots out there.
+4 -4
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.0-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
@@ -34,7 +34,7 @@
:target: https://github.com/python-telegram-bot/python-telegram-bot/
:alt: Github Actions workflow
.. image:: https://app.codecov.io/gh/python-telegram-bot/python-telegram-bot/branch/master/graph/badge.svg
.. image:: https://app.codecov.io/gh/python-telegram-bot/python-telegram-bot
:target: https://app.codecov.io/gh/python-telegram-bot/python-telegram-bot
:alt: Code coverage
@@ -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.0** are supported.
All types and methods of the Telegram Bot API **6.2** are supported.
Installing
==========
@@ -146,7 +146,7 @@ Resources
=========
- The `package documentation <https://docs.python-telegram-bot.org/>`_ is the technical reference for ``python-telegram-bot``.
It contains descriptions of all available classes, modules, methods and arguments.
It contains descriptions of all available classes, modules, methods and arguments as well as the `changelog <https://docs.python-telegram-bot.org/changelog.html>`_.
- The `wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ is home to number of more elaborate introductions of the different features of ``python-telegram-bot`` and other useful resources that go beyond the technical documentation.
- Our `examples section <https://docs.python-telegram-bot.org/examples.html>`_ contains several examples that showcase the different features of both the Bot API and ``python-telegram-bot``.
Even if it is not your approach for learning, please take a look at ``echobot.py``. It is the de facto base for most of the bots out there.
+4 -3
View File
@@ -1,4 +1,5 @@
sphinx==5.0.1
sphinx==5.1.1
sphinx-pypi-upload
furo==2022.6.4
sphinx-paramlinks==0.5.4
furo==2022.6.21
sphinx-paramlinks==0.5.4
sphinxcontrib-mermaid==0.7.1
@@ -0,0 +1,3 @@
.mermaid svg {
height: auto;
}
+12 -4
View File
@@ -29,9 +29,9 @@ author = "Leandro Toledo"
# built documents.
#
# The short X.Y version.
version = "20.0a1" # telegram.__version__[:3]
version = "20.0a4" # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = "20.0a1" # telegram.__version__
release = "20.0a4" # telegram.__version__
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "4.5.0"
@@ -45,6 +45,7 @@ extensions = [
"sphinx.ext.intersphinx",
"sphinx.ext.linkcode",
"sphinx_paramlinks",
"sphinxcontrib.mermaid",
]
# Use intersphinx to reference the python builtin library docs
@@ -211,7 +212,10 @@ html_favicon = "ptb-logo_1024.ico"
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_css_files = ["style_external_link.css"]
html_css_files = [
"style_external_link.css",
"style_mermaid_diagrams.css",
]
html_permalinks_icon = "" # Furo's default permalink icon is `#`` which doesn't look great imo.
# Output file base name for HTML help builder.
@@ -474,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]
@@ -482,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 -2
View File
@@ -10,5 +10,4 @@
State Diagram
-------------
.. image:: ../../examples/conversationbot.png
.. mermaid:: ../../examples/conversationbot.mmd
+1 -2
View File
@@ -10,5 +10,4 @@
State Diagram
-------------
.. image:: ../../examples/conversationbot2.png
.. mermaid:: ../../examples/conversationbot2.mmd
@@ -10,5 +10,4 @@
State Diagram
-------------
.. image:: ../../examples/nestedconversationbot.png
.. mermaid:: ../../examples/nestedconversationbot.mmd
+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
@@ -263,6 +265,8 @@
:align: left
:widths: 1 4
* - :meth:`~telegram.Bot.create_invoice_link`
- Used to generate an HTTP link for an invoice
* - :meth:`~telegram.Bot.close`
- Used for closing server instance when switching to another local server
* - :meth:`~telegram.Bot.log_out`
+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
+21
View File
@@ -0,0 +1,21 @@
flowchart TB
%% Documentation: https://mermaid-js.github.io/mermaid/#/flowchart
A(("/start")):::entryPoint -->|Hi! My name is Professor Bot...| B((GENDER)):::state
B --> |"- Boy <br /> - Girl <br /> - Other"|C("(choice)"):::userInput
C --> |I see! Please send me a photo...| D((PHOTO)):::state
D --> E("/skip"):::userInput
D --> F("(photo)"):::userInput
E --> |I bet you look great!| G[\ /]:::userInput
F --> |Gorgeous!| G[\ /]
G --> |"Now, send me your location .."| H((LOCATION)):::state
H --> I("/skip"):::userInput
H --> J("(location)"):::userInput
I --> |You seem a bit paranoid!| K[\" "/]:::userInput
J --> |Maybe I can visit...| K
K --> |"Tell me about yourself..."| L(("BIO")):::state
L --> M("(text)"):::userInput
M --> |"Thanks and bye!"| End(("END")):::termination
classDef userInput fill:#2a5279, color:#ffffff, stroke:#ffffff
classDef state fill:#222222, color:#ffffff, stroke:#ffffff
classDef entryPoint fill:#009c11, stroke:#42FF57, color:#ffffff
classDef termination fill:#bb0007, stroke:#E60109, color:#ffffff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

+17
View File
@@ -0,0 +1,17 @@
flowchart TB
%% Documentation: https://mermaid-js.github.io/mermaid/#/flowchart
A(("/start")):::entryPoint -->|Hi! My name is Doctor Botter...| B((CHOOSING)):::state
B --> C("Something else..."):::userInput
C --> |What category?| D((TYPING_CHOICE)):::state
D --> E("(text)"):::userInput
E --> |"[save choice] <br /> I'd love to hear about that!"| F((TYPING_REPLY)):::state
F --> G("(text)"):::userInput
G --> |"[save choice: text] <br /> Neat! <br /> (List of facts) <br /> More?"| B
B --> H("- Age <br /> - Favourite colour <br /> - Number of siblings"):::userInput
H --> |"[save choice] <br /> I'd love to hear about that!"| F
B --> I("Done"):::userInput
I --> |"I learned these facts about you: <br /> ..."| End(("END")):::termination
classDef userInput fill:#2a5279, color:#ffffff, stroke:#ffffff
classDef state fill:#222222, color:#ffffff, stroke:#ffffff
classDef entryPoint fill:#009c11, stroke:#42FF57, color:#ffffff
classDef termination fill:#bb0007, stroke:#E60109, color:#ffffff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

+43
View File
@@ -0,0 +1,43 @@
flowchart TB
%% Documentation: https://mermaid-js.github.io/mermaid/#/flowchart
A(("/start")):::entryPoint -->|Hi! I'm FamilyBot...| B((SELECTING_ACTION)):::state
B --> C("Show Data"):::userInput
C --> |"(List of gathered data)"| D((SHOWING)):::state
D --> E("Back"):::userInput
E --> B
B --> F("Add Yourself"):::userInput
F --> G(("DESCRIBING_SELF")):::state
G --> H("Add info"):::userInput
H --> I((SELECT_FEATURE)):::state
I --> |"Please select a feature to update. <br /> - Name <br /> - Age <br /> - Done"|J("(choice)"):::userInput
J --> |"Okay, tell me."| K((TYPING)):::state
K --> L("(text)"):::userInput
L --> |"[saving]"|I
I --> M("Done"):::userInput
M --> B
B --> N("Add family member"):::userInput
R --> I
W --> |"See you around!"|End(("END")):::termination
Y(("ANY STATE")):::state --> Z("/stop"):::userInput
Z -->|"Okay, bye."| End
B --> W("Done"):::userInput
subgraph nestedConversation[Nested Conversation: Add Family Member]
direction BT
N --> O(("SELECT_LEVEL")):::state
O --> |"Add... <br /> - Add Parent <br /> - Add Child <br />"|P("(choice)"):::userInput
P --> Q(("SELECT_GENDER")):::state
Q --> |"- Mother <br /> - Father <br /> / <br /> - Sister <br /> - Brother"| R("(choice)"):::userInput
Q --> V("Show Data"):::userInput
Q --> T(("SELECTING_ACTION")):::state
Q --> U("Back"):::userInput
U --> T
O --> U
O --> V
V --> S(("SHOWING")):::state
V --> T
end
classDef userInput fill:#2a5279, color:#ffffff, stroke:#ffffff
classDef state fill:#222222, color:#ffffff, stroke:#ffffff
classDef entryPoint fill:#009c11, stroke:#42FF57, color:#ffffff
classDef termination fill:#bb0007, stroke:#E60109, color:#ffffff
style nestedConversation fill:#999999, stroke-width:2px, stroke:#333333
Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

+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.960
pyupgrade==2.32.1
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",
)
+530 -90
View File
File diff suppressed because it is too large Load Diff
+260 -2
View File
@@ -124,6 +124,22 @@ class Chat(TelegramObject):
chats. Returned only in :meth:`telegram.Bot.get_chat`.
location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
join_to_send_messages (:obj:`bool`, optional): :obj:`True`, if users need to join the
supergroup before they can send messages. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
join_by_request (:obj:`bool`, optional): :obj:`True`, if all users directly 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
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes:
@@ -168,6 +184,21 @@ class Chat(TelegramObject):
chats. Returned only in :meth:`telegram.Bot.get_chat`.
location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
join_to_send_messages (:obj:`bool`): Optional. :obj:`True`, if users need to join
the supergroup before they can send messages. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
join_by_request (:obj:`bool`): Optional. :obj:`True`, if all users directly
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
"""
@@ -193,6 +224,9 @@ class Chat(TelegramObject):
"message_auto_delete_time",
"has_protected_content",
"has_private_forwards",
"join_to_send_messages",
"join_by_request",
"has_restricted_voice_and_video_messages",
)
SENDER: ClassVar[str] = constants.ChatType.SENDER
@@ -232,6 +266,9 @@ class Chat(TelegramObject):
message_auto_delete_time: int = None,
has_private_forwards: bool = None,
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
@@ -260,6 +297,9 @@ class Chat(TelegramObject):
self.can_set_sticker_set = can_set_sticker_set
self.linked_chat_id = linked_chat_id
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,)
@@ -790,6 +830,144 @@ class Chat(TelegramObject):
api_kwargs=api_kwargs,
)
async def set_photo(
self,
photo: FileInput,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
await bot.set_chat_photo(
chat_id=update.effective_chat.id, *args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.set_chat_photo`.
.. versionadded:: 20.0
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().set_chat_photo(
chat_id=self.id,
photo=photo,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def delete_photo(
self,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
await bot.delete_chat_photo(
chat_id=update.effective_chat.id, *args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.delete_chat_photo`.
.. versionadded:: 20.0
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().delete_chat_photo(
chat_id=self.id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def set_title(
self,
title: 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,
) -> bool:
"""Shortcut for::
await bot.set_chat_title(
chat_id=update.effective_chat.id, *args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.set_chat_title`.
.. versionadded:: 20.0
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().set_chat_title(
chat_id=self.id,
title=title,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def set_description(
self,
description: str = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
await bot.set_chat_description(
chat_id=update.effective_chat.id, *args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.set_chat_description`.
.. versionadded:: 20.0
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().set_chat_description(
chat_id=self.id,
description=description,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def pin_message(
self,
message_id: int,
@@ -1894,6 +2072,86 @@ class Chat(TelegramObject):
protect_content=protect_content,
)
async def forward_from(
self,
from_chat_id: Union[str, int],
message_id: int,
disable_notification: DVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> "Message":
"""Shortcut for::
await bot.forward_message(chat_id=update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`.
.. seealso:: :meth:`forward_to`
.. versionadded:: 20.0
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return await self.get_bot().forward_message(
chat_id=self.id,
from_chat_id=from_chat_id,
message_id=message_id,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
)
async def forward_to(
self,
chat_id: Union[int, str],
message_id: int,
disable_notification: DVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> "Message":
"""Shortcut for::
await bot.forward_message(from_chat_id=update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`.
.. seealso:: :meth:`forward_from`
.. versionadded:: 20.0
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return await self.get_bot().forward_message(
from_chat_id=self.id,
chat_id=chat_id,
message_id=message_id,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
)
async def export_invite_link(
self,
*,
@@ -2131,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
@@ -2167,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:
+57 -5
View File
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, List, Optional
from telegram import constants
from telegram._files._basethumbedmedium import _BaseThumbedMedium
from telegram._files.file import File
from telegram._files.photosize import PhotoSize
from telegram._telegramobject import TelegramObject
from telegram._utils.types import JSONDict
@@ -53,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
@@ -62,6 +68,14 @@ 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): 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.
Attributes:
@@ -75,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.
@@ -83,7 +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. 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__ = (
@@ -94,6 +120,9 @@ class Sticker(_BaseThumbedMedium):
"mask_position",
"set_name",
"width",
"premium_animation",
"type",
"custom_emoji_id",
)
def __init__(
@@ -104,12 +133,15 @@ 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,
set_name: str = None,
mask_position: "MaskPosition" = None,
bot: "Bot" = None,
premium_animation: "File" = None,
custom_emoji_id: str = None,
**_kwargs: Any,
):
super().__init__(
@@ -124,10 +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"]:
@@ -139,6 +181,7 @@ class Sticker(_BaseThumbedMedium):
data["thumb"] = PhotoSize.de_json(data.get("thumb"), bot)
data["mask_position"] = MaskPosition.de_json(data.get("mask_position"), bot)
data["premium_animation"] = File.de_json(data.get("premium_animation"), bot)
return cls(bot=bot, **data)
@@ -154,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.
@@ -161,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.
@@ -173,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__(
@@ -195,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,
):
@@ -205,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
+7 -3
View File
@@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineKeyboardButton."""
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any, Optional, Union
from telegram._games.callbackgame import CallbackGame
from telegram._loginurl import LoginUrl
@@ -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.
@@ -163,7 +167,7 @@ class InlineKeyboardButton(TelegramObject):
self,
text: str,
url: str = None,
callback_data: object = None,
callback_data: Union[str, object] = None,
switch_inline_query: str = None,
switch_inline_query_current_chat: str = None,
callback_game: CallbackGame = None,
@@ -214,7 +218,7 @@ class InlineKeyboardButton(TelegramObject):
return cls(**data)
def update_callback_data(self, callback_data: object) -> None:
def update_callback_data(self, callback_data: Union[str, object]) -> None:
"""
Sets :attr:`callback_data` to the passed object. Intended to be used by
:class:`telegram.ext.CallbackDataCache`.
+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.
+21 -11
View File
@@ -39,9 +39,14 @@ class InputInvoiceMessageContent(InputMessageContent):
.. versionadded:: 13.5
Args:
title (:obj:`str`): Product name, 1-32 characters
description (:obj:`str`): Product description, 1-255 characters
payload (:obj:`str`):Bot-defined invoice payload, 1-128 bytes. This will not be displayed
title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
:tg-const:`telegram.Invoice.MAX_TITLE_LENGTH` characters.
description (:obj:`str`): Product description.
:tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`-
:tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters.
payload (:obj:`str`): Bot-defined invoice payload.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed
to the user, use for your internal processes.
provider_token (:obj:`str`): Payment provider token, obtained via
`@Botfather <https://t.me/Botfather>`_.
@@ -50,15 +55,15 @@ class InputInvoiceMessageContent(InputMessageContent):
prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a list of
components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus,
etc.)
max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the smallest
units of the currency (integer, not float/double). For example, for a maximum tip of
US$ 1.45 pass ``max_tip_amount = 145``. See the ``exp`` parameter in
max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the
*smallest* units of the currency (integer, **not** float/double). For example, for a
maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the ``exp`` parameter in
`currencies.json <https://core.telegram.org/bots/payments/currencies.json>`_, it
shows the number of digits past the decimal point for each currency (2 for the majority
of currencies). Defaults to ``0``.
suggested_tip_amounts (List[:obj:`int`], optional): An array of suggested
amounts of tip in the smallest units of the currency (integer, not float/double). At
most 4 suggested tip amounts can be specified. The suggested tip amounts must be
amounts of tip in the *smallest* units of the currency (integer, **not** float/double).
At most 4 suggested tip amounts can be specified. The suggested tip amounts must be
positive, passed in a strictly increased order and must not exceed
:attr:`max_tip_amount`.
provider_data (:obj:`str`, optional): An object for data about the invoice,
@@ -87,9 +92,14 @@ class InputInvoiceMessageContent(InputMessageContent):
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes:
title (:obj:`str`): Product name, 1-32 characters
description (:obj:`str`): Product description, 1-255 characters
payload (:obj:`str`):Bot-defined invoice payload, 1-128 bytes. This will not be displayed
title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
:tg-const:`telegram.Invoice.MAX_TITLE_LENGTH` characters.
description (:obj:`str`): Product description.
:tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`-
:tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters.
payload (:obj:`str`): Bot-defined invoice payload.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed
to the user, use for your internal processes.
provider_token (:obj:`str`): Payment provider token, obtained via
`@Botfather <https://t.me/Botfather>`_.
@@ -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
+2 -2
View File
@@ -39,7 +39,7 @@ class LoginUrl(TelegramObject):
`Checking authorization <https://core.telegram.org/widgets/login#checking-authorization>`_
Args:
url (:obj:`str`): An HTTP URL to be opened with user authorization data added to the query
url (:obj:`str`): An HTTPS URL to be opened with user authorization data added to the query
string when the button is pressed. If the user refuses to provide authorization data,
the original URL without information about the user will be opened. The data added is
the same as described in
@@ -59,7 +59,7 @@ class LoginUrl(TelegramObject):
for your bot to send messages to the user.
Attributes:
url (:obj:`str`): An HTTP URL to be opened with user authorization data.
url (:obj:`str`): An HTTPS URL to be opened with user authorization data.
forward_text (:obj:`str`): Optional. New text of the button in forwarded messages.
bot_username (:obj:`str`): Optional. Username of a bot, which will be used for user
authorization.
+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."""
+33 -1
View File
@@ -18,8 +18,9 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Invoice."""
from typing import Any
from typing import Any, ClassVar
from telegram import constants
from telegram._telegramobject import TelegramObject
@@ -83,3 +84,34 @@ class Invoice(TelegramObject):
self.currency,
self.total_amount,
)
MIN_TITLE_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_TITLE_LENGTH
""":const:`telegram.constants.InvoiceLimit.MIN_TITLE_LENGTH`
.. versionadded:: 20.0
"""
MAX_TITLE_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_TITLE_LENGTH
""":const:`telegram.constants.InvoiceLimit.MAX_TITLE_LENGTH`
.. versionadded:: 20.0
"""
MIN_DESCRIPTION_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH
""":const:`telegram.constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH`
.. versionadded:: 20.0
"""
MAX_DESCRIPTION_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH
""":const:`telegram.constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH`
.. versionadded:: 20.0
"""
MIN_PAYLOAD_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_PAYLOAD_LENGTH
""":const:`telegram.constants.InvoiceLimit.MIN_PAYLOAD_LENGTH`
.. versionadded:: 20.0
"""
MAX_PAYLOAD_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_PAYLOAD_LENGTH
""":const:`telegram.constants.InvoiceLimit.MAX_PAYLOAD_LENGTH`
.. versionadded:: 20.0
"""
+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.
+22 -2
View File
@@ -83,6 +83,13 @@ class User(TelegramObject):
supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline
queries. Returned only in :attr:`telegram.Bot.get_me` requests.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
is_premium (:obj:`bool`, optional): :obj:`True`, if this user is a Telegram Premium user.
.. versionadded:: 20.0
added_to_attachment_menu (:obj:`bool`, optional): :obj:`True`, if this user added
the bot to the attachment menu.
.. versionadded:: 20.0
Attributes:
id (:obj:`int`): Unique identifier for this user or bot.
@@ -98,7 +105,14 @@ class User(TelegramObject):
supports_inline_queries (:obj:`str`): Optional. :obj:`True`, if the bot supports inline
queries. Returned only in :attr:`telegram.Bot.get_me` requests.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
is_premium (:obj:`bool`): Optional. :obj:`True`, if this user is a Telegram
Premium user.
.. versionadded:: 20.0
added_to_attachment_menu (:obj:`bool`): Optional. :obj:`True`, if this user added
the bot to the attachment menu.
.. versionadded:: 20.0
"""
__slots__ = (
@@ -111,6 +125,8 @@ class User(TelegramObject):
"supports_inline_queries",
"id",
"language_code",
"is_premium",
"added_to_attachment_menu",
)
def __init__(
@@ -125,6 +141,8 @@ class User(TelegramObject):
can_read_all_group_messages: bool = None,
supports_inline_queries: bool = None,
bot: "Bot" = None,
is_premium: bool = None,
added_to_attachment_menu: bool = None,
**_kwargs: Any,
):
# Required
@@ -138,6 +156,8 @@ class User(TelegramObject):
self.can_join_groups = can_join_groups
self.can_read_all_group_messages = can_read_all_group_messages
self.supports_inline_queries = supports_inline_queries
self.is_premium = is_premium
self.added_to_attachment_menu = added_to_attachment_menu
self.set_bot(bot)
self._id_attrs = (self.id,)
@@ -1443,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
@@ -1476,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=1)
__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:
+84 -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",
@@ -45,6 +47,7 @@ __all__ = [
"InlineQueryLimit",
"InlineQueryResultType",
"InputMediaType",
"InvoiceLimit",
"LocationLimit",
"MaskPosition",
"MenuButtonType",
@@ -56,13 +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):
@@ -90,7 +94,7 @@ class _BotAPIVersion(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=0)
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__`.
@@ -275,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
@@ -603,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):
@@ -716,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.
@@ -809,3 +851,41 @@ class UpdateType(StringEnum):
""":obj:`str`: Updates with :attr:`telegram.Update.chat_member`."""
CHAT_JOIN_REQUEST = "chat_join_request"
""":obj:`str`: Updates with :attr:`telegram.Update.chat_join_request`."""
class InvoiceLimit(IntEnum):
"""This enum contains limitations for :meth:`telegram.Bot.create_invoice_link`. The enum
members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 20.0
"""
__slots__ = ()
MIN_TITLE_LENGTH = 1
""":obj:`int`: Minimum number of characters of the invoice title."""
MAX_TITLE_LENGTH = 32
""":obj:`int`: Maximum number of characters of the invoice title."""
MIN_DESCRIPTION_LENGTH = 1
""":obj:`int`: Minimum number of characters of the invoice description."""
MAX_DESCRIPTION_LENGTH = 255
""":obj:`int`: Maximum number of characters of the invoice description."""
MIN_PAYLOAD_LENGTH = 1
""":obj:`int`: Minimum amount of bytes for the internal payload."""
MAX_PAYLOAD_LENGTH = 128
""":obj:`int`: Maximum amount of bytes for the internal payload."""
class WebhookLimit(IntEnum):
"""This enum contains limitations for :paramref:`telegram.Bot.set_webhook.secret_token`. The
enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 20.0
"""
__slots__ = ()
MIN_SECRET_TOKEN_LENGTH = 1
""":obj:`int`: Minimum length of the secret token."""
MAX_SECRET_TOKEN_LENGTH = 256
""":obj:`int`: Maximum length of the secret token."""
+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()
+38 -4
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
@@ -359,7 +367,11 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
* :attr:`bot` by calling :meth:`telegram.Bot.shutdown`
* :attr:`updater` by calling :meth:`telegram.ext.Updater.shutdown`
* :attr:`persistence` by calling :meth:`update_persistence` and :meth`persistence.flush`
* :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`
@@ -572,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`
@@ -662,6 +677,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
max_connections: int = 40,
close_loop: bool = True,
stop_signals: ODVInput[Sequence[int]] = DEFAULT_NONE,
secret_token: str = None,
) -> None:
"""Convenience method that takes care of initializing and starting the app,
polling updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and
@@ -681,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`
@@ -689,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.
@@ -724,6 +744,16 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
:meth:`asyncio.loop.add_signal_handler`. Most notably, the standard event loop
on Windows, :class:`asyncio.ProactorEventLoop`, does not implement this method.
If this method is not available, stop signals can not be set.
secret_token (:obj:`str`, optional): Secret token to ensure webhook requests originate
from Telegram. See :paramref:`telegram.Bot.set_webhook.secret_token` for more
details.
When added, the web server started by this call will expect the token to be set in
the ``X-Telegram-Bot-Api-Secret-Token`` header of an incoming request and will
raise a :class:`http.HTTPStatus.FORBIDDEN <http.HTTPStatus>` error if either the
header isn't set or it is set to a wrong token.
.. versionadded:: 20.0
"""
if not self.updater:
raise RuntimeError(
@@ -743,6 +773,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
allowed_updates=allowed_updates,
ip_address=ip_address,
max_connections=max_connections,
secret_token=secret_token,
),
close_loop=close_loop,
stop_signals=stop_signals,
@@ -800,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()
@@ -1386,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
+7 -1
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:
@@ -369,7 +374,8 @@ class CallbackDataCache:
time_cutoff (:obj:`float` | :obj:`datetime.datetime`, optional): Pass a UNIX timestamp
or a :obj:`datetime.datetime` to clear only entries which are older.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used.
bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is
used.
"""
self.__clear(self._keyboard_data, time_cutoff=time_cutoff)
+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
+25 -8
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")
@@ -148,11 +153,13 @@ class JobQueue:
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
which the job should run. If the timezone (:attr:`datetime.datetime.tzinfo`) is
:obj:`None`, the default timezone of the bot will be used.
:obj:`None`, the default timezone of the bot will be used, which is UTC unless
:attr:`telegram.ext.Defaults.tzinfo` is used.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
tomorrow. If the timezone (:attr:`datetime.time.tzinfo`) is :obj:`None`, the
default timezone of the bot will be used.
default timezone of the bot will be used, which is UTC unless
:attr:`telegram.ext.Defaults.tzinfo` is used.
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
@@ -246,7 +253,8 @@ class JobQueue:
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
tomorrow. If the timezone (:attr:`datetime.time.tzinfo`) is :obj:`None`, the
default timezone of the bot will be used.
default timezone of the bot will be used, which is UTC unless
:attr:`telegram.ext.Defaults.tzinfo` is used.
Defaults to :paramref:`interval`
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
@@ -256,7 +264,7 @@ class JobQueue:
If :paramref:`last` is :obj:`datetime.datetime` or :obj:`datetime.time` type
and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be
assumed.
assumed, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
Defaults to :obj:`None`.
data (:obj:`object`, optional): Additional data needed for the callback function.
@@ -339,7 +347,8 @@ class JobQueue:
async def callback(context: CallbackContext)
when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
(``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used,
which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
day (:obj:`int`): Defines the day of the month whereby the job would run. It should
be within the range of ``1`` and ``31``, inclusive. If a month has fewer days than
this number, the job will not run in this month. Passing ``-1`` leads to the job
@@ -419,7 +428,7 @@ class JobQueue:
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(:obj:`datetime.time.tzinfo`) is :obj:`None`, the default timezone of the bot will
be used.
be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
run (where ``0-6`` correspond to sunday - saturday). By default, the job will run
every day.
@@ -563,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()
@@ -572,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)
@@ -591,6 +607,7 @@ class Job:
this :class:`telegram.ext.Job` to be useful.
.. versionchanged:: 20.0
* Removed argument and attribute ``job_queue``.
* Renamed ``Job.context`` to :attr:`Job.data`.
+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
+21 -2
View File
@@ -369,6 +369,7 @@ class Updater(AbstractAsyncContextManager):
drop_pending_updates: bool = None,
ip_address: str = None,
max_connections: int = 40,
secret_token: str = None,
) -> asyncio.Queue:
"""
Starts a small http server to listen for updates via webhook. If :paramref:`cert`
@@ -388,13 +389,15 @@ 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.
key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file.
drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
Telegram servers before actually starting to poll. Default is :obj:`False`.
.. versionadded :: 13.4
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
@@ -407,12 +410,23 @@ class Updater(AbstractAsyncContextManager):
:paramref:`port`, :paramref:`url_path`, :paramref:`cert`, and :paramref:`key`.
ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`.
Defaults to :obj:`None`.
.. versionadded :: 13.4
allowed_updates (List[:obj:`str`], optional): Passed to
:meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`.
max_connections (:obj:`int`, optional): Passed to
:meth:`telegram.Bot.set_webhook`. Defaults to ``40``.
.. versionadded:: 13.6
secret_token (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`.
Defaults to :obj:`None`.
When added, the web server started by this call will expect the token to be set in
the ``X-Telegram-Bot-Api-Secret-Token`` header of an incoming request and will
raise a :class:`http.HTTPStatus.FORBIDDEN <http.HTTPStatus>` error if either the
header isn't set or it is set to a wrong token.
.. versionadded:: 20.0
Returns:
:class:`queue.Queue`: The update queue that can be filled from the main thread.
@@ -444,6 +458,7 @@ class Updater(AbstractAsyncContextManager):
ready=webhook_ready,
ip_address=ip_address,
max_connections=max_connections,
secret_token=secret_token,
)
self._logger.debug("Waiting for webhook server to start")
@@ -470,6 +485,7 @@ class Updater(AbstractAsyncContextManager):
ready: asyncio.Event = None,
ip_address: str = None,
max_connections: int = 40,
secret_token: str = None,
) -> None:
self._logger.debug("Updater thread started (webhook)")
@@ -477,7 +493,7 @@ class Updater(AbstractAsyncContextManager):
url_path = f"/{url_path}"
# Create Tornado app instance
app = WebhookAppClass(url_path, self.bot, self.update_queue)
app = WebhookAppClass(url_path, self.bot, self.update_queue, secret_token)
# Form SSL Context
# An SSLError is raised if the private key does not match with the certificate
@@ -517,6 +533,7 @@ class Updater(AbstractAsyncContextManager):
allowed_updates=allowed_updates,
ip_address=ip_address,
max_connections=max_connections,
secret_token=secret_token,
)
await self._httpd.serve_forever(ready=ready)
@@ -591,6 +608,7 @@ class Updater(AbstractAsyncContextManager):
bootstrap_interval: float = 1,
ip_address: str = None,
max_connections: int = 40,
secret_token: str = None,
) -> None:
"""Prepares the setup for fetching updates: delete or set the webhook and drop pending
updates if appropriate. If there are unsuccessful attempts, this will retry as specified by
@@ -616,6 +634,7 @@ class Updater(AbstractAsyncContextManager):
ip_address=ip_address,
drop_pending_updates=drop_pending_updates,
max_connections=max_connections,
secret_token=secret_token,
)
return False
+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"""
+28 -4
View File
@@ -83,8 +83,14 @@ class WebhookServer:
class WebhookAppClass(tornado.web.Application):
"""Application used in the Webserver"""
def __init__(self, webhook_path: str, bot: "Bot", update_queue: asyncio.Queue):
self.shared_objects = {"bot": bot, "update_queue": update_queue}
def __init__(
self, webhook_path: str, bot: "Bot", update_queue: asyncio.Queue, secret_token: str = None
):
self.shared_objects = {
"bot": bot,
"update_queue": update_queue,
"secret_token": secret_token,
}
handlers = [(rf"{webhook_path}/?", TelegramHandler, self.shared_objects)] # noqa
tornado.web.Application.__init__(self, handlers) # type: ignore
@@ -96,16 +102,21 @@ class WebhookAppClass(tornado.web.Application):
class TelegramHandler(tornado.web.RequestHandler):
"""BaseHandler that processes incoming requests from Telegram"""
__slots__ = ("bot", "update_queue", "_logger")
__slots__ = ("bot", "update_queue", "_logger", "secret_token")
SUPPORTED_METHODS = ("POST",) # type: ignore[assignment]
def initialize(self, bot: "Bot", update_queue: asyncio.Queue) -> None:
def initialize(self, bot: "Bot", update_queue: asyncio.Queue, secret_token: str) -> None:
"""Initialize for each request - that's the interface provided by tornado"""
# pylint: disable=attribute-defined-outside-init
self.bot = bot
self.update_queue = update_queue
self._logger = logging.getLogger(__name__)
self.secret_token = secret_token
if secret_token:
self._logger.debug(
"The webhook server has a secret token, " "expecting it in incoming requests now"
)
def set_default_headers(self) -> None:
"""Sets default headers"""
@@ -144,6 +155,19 @@ class TelegramHandler(tornado.web.RequestHandler):
ct_header = self.request.headers.get("Content-Type", None)
if ct_header != "application/json":
raise tornado.web.HTTPError(HTTPStatus.FORBIDDEN)
# verifying that the secret token is the one the user set when the user set one
if self.secret_token is not None:
token = self.request.headers.get("X-Telegram-Bot-Api-Secret-Token")
if not token:
self._logger.debug("Request did not include the secret token")
raise tornado.web.HTTPError(
HTTPStatus.FORBIDDEN, reason="Request did not include the secret token"
)
if token != self.secret_token:
self._logger.debug("Request had the wrong secret token: %s", token)
raise tornado.web.HTTPError(
HTTPStatus.FORBIDDEN, reason="Request had the wrong secret token"
)
def log_exception(
self,
+53
View File
@@ -76,6 +76,8 @@ __all__ = (
"TEXT",
"Text",
"USER",
"USER_ATTACHMENT",
"PREMIUM_USER",
"UpdateFilter",
"UpdateType",
"User",
@@ -1949,6 +1951,22 @@ class Sticker:
.. versionadded:: 20.0
"""
class _Premium(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.sticker) and bool(
message.sticker.premium_animation # type: ignore
)
PREMIUM = _Premium(name="filters.Sticker.PREMIUM")
"""Messages that contain :attr:`telegram.Message.sticker` and have a
:attr:`premium animation <telegram.Sticker.premium_animation>`.
.. versionadded:: 20.0
"""
# neither mask nor emoji can be a message.sticker, so no filters for them
class _SuccessfulPayment(MessageFilter):
__slots__ = ()
@@ -2185,6 +2203,41 @@ USER = _User(name="filters.USER")
"""This filter filters *any* message that has a :attr:`telegram.Message.from_user`."""
class _UserAttachment(UpdateFilter):
__slots__ = ()
def filter(self, update: Update) -> bool:
return bool(update.effective_user) and bool(
update.effective_user.added_to_attachment_menu # type: ignore
)
USER_ATTACHMENT = _UserAttachment(name="filters.USER_ATTACHMENT")
"""This filter filters *any* message that have a user who added the bot to their
:attr:`attachment menu <telegram.User.added_to_attachment_menu>` as
:attr:`telegram.Update.effective_user`.
.. versionadded:: 20.0
"""
class _UserPremium(UpdateFilter):
__slots__ = ()
def filter(self, update: Update) -> bool:
return bool(update.effective_user) and bool(
update.effective_user.is_premium # type: ignore
)
PREMIUM_USER = _UserPremium(name="filters.PREMIUM_USER")
"""This filter filters *any* message from a
:attr:`Telegram Premium user <telegram.User.is_premium>` as :attr:`telegram.Update.effective_user`.
.. versionadded:: 20.0
"""
class _Venue(MessageFilter):
__slots__ = ()
+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
+15 -3
View File
@@ -250,13 +250,16 @@ def class_thumb_file():
f.close()
def make_bot(bot_info, **kwargs):
def make_bot(bot_info=None, **kwargs):
"""
Tests are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot
"""
token = kwargs.pop("token", (bot_info or {}).get("token"))
private_key = kwargs.pop("private_key", PRIVATE_KEY)
kwargs.pop("token", None)
_bot = DictExtBot(
bot_info["token"],
private_key=PRIVATE_KEY,
token=token,
private_key=private_key,
request=TestHttpxRequest(8),
get_updates_request=TestHttpxRequest(1),
**kwargs,
@@ -555,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:
@@ -562,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)
@@ -827,10 +836,13 @@ async def send_webhook_message(
content_len: int = -1,
content_type: str = "application/json",
get_method: str = None,
secret_token: str = None,
) -> Response:
headers = {
"content-type": content_type,
}
if secret_token:
headers["X-Telegram-Bot-Api-Secret-Token"] = secret_token
if not payload_str:
content_len = None
+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()
+128 -89
View File
@@ -75,6 +75,7 @@ from tests.conftest import (
check_defaults_handling,
data_file,
expect_bad_request,
make_bot,
)
@@ -136,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
@@ -154,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"]
@@ -278,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 Bot("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):
@@ -335,9 +350,9 @@ class TestBot:
await bot.shutdown()
async def test_equality(self):
async with Bot(FALLBACKS[0]["token"]) as a, Bot(FALLBACKS[0]["token"]) as b, Bot(
FALLBACKS[1]["token"]
) as c:
async with make_bot(token=FALLBACKS[0]["token"]) as a, make_bot(
token=FALLBACKS[0]["token"]
) as b, make_bot(token=FALLBACKS[1]["token"]) as c:
d = Update(123456789)
assert a == b
@@ -365,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:
@@ -407,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)
@@ -482,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"}})
@@ -957,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):
@@ -1633,6 +1620,35 @@ class TestBot:
assert await bot.set_webhook("", drop_pending_updates=drop_pending_updates)
assert await bot.delete_webhook(drop_pending_updates=drop_pending_updates)
async def test_set_webhook_params(self, bot, monkeypatch):
# actually making calls to TG is done in
# test_set_webhook_get_webhook_info_and_delete_webhook. Sadly secret_token can't be tested
# there so we have this function \o/
async def make_assertion(*args, **_):
kwargs = args[1]
return (
kwargs["url"] == "example.com"
and kwargs["certificate"].input_file_content
== data_file("sslcert.pem").read_bytes()
and kwargs["max_connections"] == 7
and kwargs["allowed_updates"] == ["messages"]
and kwargs["ip_address"] == "127.0.0.1"
and kwargs["drop_pending_updates"]
and kwargs["secret_token"] == "SoSecretToken"
)
monkeypatch.setattr(bot, "_post", make_assertion)
assert await bot.set_webhook(
"example.com",
data_file("sslcert.pem").read_bytes(),
7,
["messages"],
"127.0.0.1",
True,
"SoSecretToken",
)
@flaky(3, 1)
async def test_leave_chat(self, bot):
with pytest.raises(BadRequest, match="Chat not found"):
@@ -1832,7 +1848,7 @@ class TestBot:
# We assume that the other game score tests ran within 20 sec
assert high_scores[0].score == BASE_GAME_SCORE - 10
# send_invoice is tested in test_invoice
# send_invoice and create_invoice_link is tested in test_invoice
# TODO: Needs improvement. Need incoming shipping queries to test
async def test_answer_shipping_query_ok(self, monkeypatch, bot):
@@ -2223,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
@@ -2893,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):
+106
View File
@@ -42,6 +42,9 @@ def chat(bot):
location=TestChat.location,
has_private_forwards=True,
has_protected_content=True,
join_to_send_messages=True,
join_by_request=True,
has_restricted_voice_and_video_messages=True,
)
@@ -64,6 +67,9 @@ class TestChat:
location = ChatLocation(Location(123, 456), "Barbie World")
has_protected_content = True
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__:
@@ -86,6 +92,11 @@ class TestChat:
"has_private_forwards": self.has_private_forwards,
"linked_chat_id": self.linked_chat_id,
"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)
@@ -104,6 +115,12 @@ class TestChat:
assert chat.linked_chat_id == self.linked_chat_id
assert chat.location.location == self.location.location
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()
@@ -121,6 +138,12 @@ class TestChat:
assert chat_dict["has_protected_content"] == chat.has_protected_content
assert chat_dict["linked_chat_id"] == chat.linked_chat_id
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")
@@ -373,6 +396,61 @@ class TestChat:
monkeypatch.setattr("telegram.Bot.set_chat_administrator_custom_title", make_assertion)
assert await chat.set_administrator_custom_title(user_id=42, custom_title="custom_title")
async def test_set_photo(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == chat.id
photo = kwargs["photo"] == "test_photo"
return chat_id, photo
assert check_shortcut_signature(Chat.set_photo, Bot.set_chat_photo, ["chat_id"], [])
assert await check_shortcut_call(chat.set_photo, chat.get_bot(), "set_chat_photo")
assert await check_defaults_handling(chat.set_photo, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "set_chat_photo", make_assertion)
assert await chat.set_photo(photo="test_photo")
async def test_delete_photo(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == chat.id
return chat_id
assert check_shortcut_signature(Chat.delete_photo, Bot.delete_chat_photo, ["chat_id"], [])
assert await check_shortcut_call(chat.delete_photo, chat.get_bot(), "delete_chat_photo")
assert await check_defaults_handling(chat.delete_photo, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "delete_chat_photo", make_assertion)
assert await chat.delete_photo()
async def test_set_title(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == chat.id
title = kwargs["title"] == "test_title"
return chat_id, title
assert check_shortcut_signature(Chat.set_title, Bot.set_chat_title, ["chat_id"], [])
assert await check_shortcut_call(chat.set_title, chat.get_bot(), "set_chat_title")
assert await check_defaults_handling(chat.set_title, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "set_chat_title", make_assertion)
assert await chat.set_title(title="test_title")
async def test_set_description(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == chat.id
description = kwargs["description"] == "test_descripton"
return chat_id, description
assert check_shortcut_signature(
Chat.set_description, Bot.set_chat_description, ["chat_id"], []
)
assert await check_shortcut_call(
chat.set_description, chat.get_bot(), "set_chat_description"
)
assert await check_defaults_handling(chat.set_description, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "set_chat_description", make_assertion)
assert await chat.set_description(description="test_description")
async def test_pin_message(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return kwargs["chat_id"] == chat.id and kwargs["message_id"] == 42
@@ -643,6 +721,34 @@ class TestChat:
monkeypatch.setattr(chat.get_bot(), "copy_message", make_assertion)
assert await chat.copy_message(chat_id="test_copy", message_id=42)
async def test_instance_method_forward_from(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == chat.id
message_id = kwargs["message_id"] == 42
from_chat_id = kwargs["from_chat_id"] == "test_forward"
return from_chat_id and message_id and chat_id
assert check_shortcut_signature(Chat.forward_from, Bot.forward_message, ["chat_id"], [])
assert await check_shortcut_call(chat.forward_from, chat.get_bot(), "forward_message")
assert await check_defaults_handling(chat.forward_from, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "forward_message", make_assertion)
assert await chat.forward_from(from_chat_id="test_forward", message_id=42)
async def test_instance_method_forward_to(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
from_chat_id = kwargs["from_chat_id"] == chat.id
message_id = kwargs["message_id"] == 42
chat_id = kwargs["chat_id"] == "test_forward"
return from_chat_id and message_id and chat_id
assert check_shortcut_signature(Chat.forward_to, Bot.forward_message, ["from_chat_id"], [])
assert await check_shortcut_call(chat.forward_to, chat.get_bot(), "forward_message")
assert await check_defaults_handling(chat.forward_to, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "forward_message", make_assertion)
assert await chat.forward_to(chat_id="test_forward", message_id=42)
async def test_export_invite_link(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return kwargs["chat_id"] == chat.id

Some files were not shown because too many files have changed in this diff Show More