mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-20 08:05:27 +00:00
Compare commits
22 Commits
v21.10
...
feature/fsm
| Author | SHA1 | Date | |
|---|---|---|---|
| b1fff6d90a | |||
| 31af1a9db8 | |||
| 4441543043 | |||
| 646ba37391 | |||
| 817b71d914 | |||
| 434cbfade8 | |||
| 34832d9db9 | |||
| 07225b9a02 | |||
| 0c06ba0a90 | |||
| dfb0ae3747 | |||
| 64006aa7ae | |||
| 69ddc47a6e | |||
| a2150b3751 | |||
| 79acc1ae53 | |||
| 4cdb1a0cf7 | |||
| 6319f4bae1 | |||
| d7e063dbad | |||
| 5dd7b8f1e2 | |||
| 61b87ba318 | |||
| dd592cdd7c | |||
| f57dd52100 | |||
| 16605c54d7 |
@@ -4,6 +4,8 @@ on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
process-dependabot-prs:
|
||||
permissions:
|
||||
@@ -16,7 +18,7 @@ jobs:
|
||||
|
||||
- name: Fetch Dependabot metadata
|
||||
id: dependabot-metadata
|
||||
uses: dependabot/fetch-metadata@dbb049abf0d677abbd7f7eee0375145b417fdd34 # v2.2.0
|
||||
uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Check Links in Documentation
|
||||
on:
|
||||
schedule:
|
||||
# First day of month at 05:46 in every 2nd month
|
||||
- cron: '46 5 1 */2 *'
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/docs-linkcheck.yml
|
||||
|
||||
jobs:
|
||||
test-sphinx-build:
|
||||
name: test-sphinx-linkcheck
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10']
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
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-dev-all.txt
|
||||
- name: Check Links
|
||||
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto -b linkcheck
|
||||
@@ -1,48 +0,0 @@
|
||||
name: Test Documentation Build
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- docs/**
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test-sphinx-build:
|
||||
name: test-sphinx-build
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10']
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements*.txt'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install -r requirements-dev-all.txt
|
||||
- name: Test autogeneration of admonitions
|
||||
run: pytest -v --tb=short tests/docs/admonition_inserter.py
|
||||
- name: Build docs
|
||||
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto
|
||||
- name: Upload docs
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: HTML Docs
|
||||
retention-days: 7
|
||||
path: |
|
||||
# Exclude the .doctrees folder and .buildinfo file from the artifact
|
||||
# since they are not needed and add to the size
|
||||
docs/build/html/*
|
||||
!docs/build/html/.doctrees
|
||||
!docs/build/html/.buildinfo
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Security Analysis with zizmor
|
||||
@@ -19,13 +21,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0
|
||||
uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --persona=pedantic --format sarif . > results.sarif
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
@@ -4,6 +4,8 @@ on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
pre-commit-ci:
|
||||
permissions:
|
||||
|
||||
@@ -4,9 +4,15 @@ on:
|
||||
schedule:
|
||||
- cron: '8 4 * * *'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# For locking the threads
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
|
||||
@@ -4,19 +4,24 @@ on:
|
||||
# manually trigger the workflow
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Distribution
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
TAG: ${{ steps.get_tag.outputs.TAG }}
|
||||
permissions:
|
||||
# for uploading artifacts
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install pypa/build
|
||||
@@ -46,6 +51,7 @@ jobs:
|
||||
url: https://pypi.org/p/python-telegram-bot
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: mandatory for trusted publishing
|
||||
actions: read # for downloading artifacts
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
@@ -64,6 +70,7 @@ jobs:
|
||||
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: mandatory for sigstore
|
||||
actions: write # for up/downloading artifacts
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
@@ -100,6 +107,7 @@ jobs:
|
||||
|
||||
permissions:
|
||||
contents: write # IMPORTANT: mandatory for making GitHub Releases
|
||||
actions: read # for downloading artifacts
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
|
||||
@@ -4,19 +4,24 @@ on:
|
||||
# manually trigger the workflow
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Distribution
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
TAG: ${{ steps.get_tag.outputs.TAG }}
|
||||
permissions:
|
||||
# for uploading artifacts
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install pypa/build
|
||||
@@ -46,6 +51,7 @@ jobs:
|
||||
url: https://test.pypi.org/p/python-telegram-bot
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: mandatory for trusted publishing
|
||||
actions: read # for downloading artifacts
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
@@ -66,6 +72,7 @@ jobs:
|
||||
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: mandatory for sigstore
|
||||
actions: write # for up/downloading artifacts
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
@@ -102,6 +109,7 @@ jobs:
|
||||
|
||||
permissions:
|
||||
contents: write # IMPORTANT: mandatory for making GitHub Releases
|
||||
actions: read # for downloading artifacts
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
|
||||
@@ -3,11 +3,16 @@ on:
|
||||
schedule:
|
||||
- cron: '42 2 * * *'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# For adding labels and closing
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
with:
|
||||
# PRs never get stale
|
||||
days-before-stale: 3
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Bot API Tests
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- tests/**
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
# Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions
|
||||
- cron: '7 3 * * 1,5'
|
||||
|
||||
jobs:
|
||||
check-conformity:
|
||||
name: check-conformity
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.11]
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install .[all]
|
||||
python -W ignore -m pip install -r requirements-unit-tests.txt
|
||||
- name: Compare to official api
|
||||
run: |
|
||||
pytest -v tests/test_official/test_official.py --junit-xml=.test_report_official.xml
|
||||
exit $?
|
||||
env:
|
||||
TEST_OFFICIAL: "true"
|
||||
shell: bash --noprofile --norc {0}
|
||||
|
||||
- name: Test Summary
|
||||
id: test_summary
|
||||
uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
|
||||
if: always() # always run, even if tests fail
|
||||
with:
|
||||
paths: .test_report_official.xml
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Check Type Completeness
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- pyproject.toml
|
||||
- .github/workflows/type_completeness.yml
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test-type-completeness:
|
||||
name: test-type-completeness
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1
|
||||
with:
|
||||
package-name: telegram
|
||||
python-version: 3.12
|
||||
pyright-version: ~=1.1.367
|
||||
@@ -4,6 +4,8 @@ on:
|
||||
# Run first friday of the month at 03:17 - odd time to spread load on GitHub Actions
|
||||
- cron: '17 3 1-7 * 5'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test-type-completeness:
|
||||
name: test-type-completeness
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -67,6 +67,7 @@ docs/_build/
|
||||
# PyBuilder
|
||||
target/
|
||||
.idea/
|
||||
.run/
|
||||
|
||||
# Sublime Text 2
|
||||
*.sublime*
|
||||
|
||||
@@ -7,7 +7,7 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.5.6'
|
||||
rev: 'v0.8.6'
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: ruff
|
||||
@@ -18,18 +18,18 @@ repos:
|
||||
- cachetools>=5.3.3,<5.5.0
|
||||
- aiolimiter~=1.1,<1.3
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.4.2
|
||||
rev: 24.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
- --diff
|
||||
- --check
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.1.0
|
||||
rev: 7.1.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/PyCQA/pylint
|
||||
rev: v3.3.2
|
||||
rev: v3.3.3
|
||||
hooks:
|
||||
- id: pylint
|
||||
files: ^(?!(tests|docs)).*\.py$
|
||||
@@ -41,7 +41,7 @@ repos:
|
||||
- aiolimiter~=1.1,<1.3
|
||||
- . # this basically does `pip install -e .`
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.10.1
|
||||
rev: v1.14.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy-ptb
|
||||
@@ -68,7 +68,7 @@ repos:
|
||||
- cachetools>=5.3.3,<5.5.0
|
||||
- . # this basically does `pip install -e .`
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.16.0
|
||||
rev: v3.19.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ The following wonderful people contributed directly or indirectly to this projec
|
||||
- `Rahiel Kasim <https://github.com/rahiel>`_
|
||||
- `Riko Naka <https://github.com/rikonaka>`_
|
||||
- `Rizlas <https://github.com/rizlas>`_
|
||||
- `Snehashish Biswas <https://github.com/Snehashish06>`_
|
||||
- Snehashish Biswas
|
||||
- `Sahil Sharma <https://github.com/sahilsharma811>`_
|
||||
- `Sam Mosleh <https://github.com/sam-mosleh>`_
|
||||
- `Sascha <https://github.com/saschalalala>`_
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ Major Changes
|
||||
Documentation Improvements
|
||||
--------------------------
|
||||
|
||||
- Documentation Improvements (:pr:`4565` by `Snehashish06 <https://github.com/Snehashish06>`_, :pr:`4573`)
|
||||
- Documentation Improvements (:pr:`4565` by Snehashish06, :pr:`4573`)
|
||||
|
||||
Version 21.7
|
||||
============
|
||||
|
||||
+201
-226
@@ -16,18 +16,55 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import collections.abc
|
||||
import contextlib
|
||||
import inspect
|
||||
import re
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterator
|
||||
from typing import Any, Union
|
||||
from socket import socket
|
||||
from types import FunctionType
|
||||
from typing import Union
|
||||
|
||||
from apscheduler.job import Job as APSJob
|
||||
|
||||
import telegram
|
||||
import telegram._utils.defaultvalue
|
||||
import telegram._utils.types
|
||||
import telegram.ext
|
||||
import telegram.ext._utils.types
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
# Define the namespace for type resolution. This helps dealing with the internal imports that
|
||||
# we do in many places
|
||||
# The .copy() is important to avoid modifying the original namespace
|
||||
TG_NAMESPACE = vars(telegram).copy()
|
||||
TG_NAMESPACE.update(vars(telegram._utils.types))
|
||||
TG_NAMESPACE.update(vars(telegram._utils.defaultvalue))
|
||||
TG_NAMESPACE.update(vars(telegram.ext))
|
||||
TG_NAMESPACE.update(vars(telegram.ext._utils.types))
|
||||
TG_NAMESPACE.update(vars(telegram.ext._applicationbuilder))
|
||||
TG_NAMESPACE.update({"socket": socket, "APSJob": APSJob})
|
||||
|
||||
|
||||
def _iter_own_public_methods(cls: type) -> Iterator[tuple[str, type]]:
|
||||
class PublicMethod(typing.NamedTuple):
|
||||
name: str
|
||||
method: FunctionType
|
||||
|
||||
|
||||
def _is_inherited_method(cls: type, method_name: str) -> bool:
|
||||
"""Checks if a method is inherited from a parent class.
|
||||
Inheritance is not considered if the parent class is private.
|
||||
Recurses through all direcot or indirect parent classes.
|
||||
"""
|
||||
# The [1:] slice is used to exclude the class itself from the MRO.
|
||||
for base in cls.__mro__[1:]:
|
||||
if method_name in base.__dict__ and not base.__name__.startswith("_"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _iter_own_public_methods(cls: type) -> Iterator[PublicMethod]:
|
||||
"""Iterates over methods of a class that are not protected/private,
|
||||
not camelCase and not inherited from the parent class.
|
||||
|
||||
@@ -35,13 +72,15 @@ def _iter_own_public_methods(cls: type) -> Iterator[tuple[str, type]]:
|
||||
|
||||
This function is defined outside the class because it is used to create class constants.
|
||||
"""
|
||||
return (
|
||||
m
|
||||
for m in inspect.getmembers(cls, predicate=inspect.isfunction) # not .ismethod
|
||||
if not m[0].startswith("_")
|
||||
and m[0].islower() # to avoid camelCase methods
|
||||
and m[0] in cls.__dict__ # method is not inherited from parent class
|
||||
)
|
||||
|
||||
# Use .isfunction() instead of .ismethod() because we want to include static methods.
|
||||
for m in inspect.getmembers(cls, predicate=inspect.isfunction):
|
||||
if (
|
||||
not m[0].startswith("_")
|
||||
and m[0].islower() # to avoid camelCase methods
|
||||
and not _is_inherited_method(cls, m[0])
|
||||
):
|
||||
yield PublicMethod(m[0], m[1])
|
||||
|
||||
|
||||
class AdmonitionInserter:
|
||||
@@ -58,18 +97,12 @@ class AdmonitionInserter:
|
||||
start and end markers.
|
||||
"""
|
||||
|
||||
FORWARD_REF_SKIP_PATTERN = re.compile(r"^ForwardRef\('DefaultValue\[\w+]'\)$")
|
||||
"""A pattern that will be used to skip known ForwardRef's that need not be resolved
|
||||
to a Telegram class, e.g.:
|
||||
ForwardRef('DefaultValue[None]')
|
||||
ForwardRef('DefaultValue[DVValueType]')
|
||||
"""
|
||||
|
||||
METHOD_NAMES_FOR_BOT_AND_APPBUILDER: typing.ClassVar[dict[type, str]] = {
|
||||
cls: tuple(m[0] for m in _iter_own_public_methods(cls)) # m[0] means we take only names
|
||||
for cls in (telegram.Bot, telegram.ext.ApplicationBuilder)
|
||||
METHOD_NAMES_FOR_BOT_APP_APPBUILDER: typing.ClassVar[dict[type, str]] = {
|
||||
cls: tuple(m.name for m in _iter_own_public_methods(cls))
|
||||
for cls in (telegram.Bot, telegram.ext.ApplicationBuilder, telegram.ext.Application)
|
||||
}
|
||||
"""A dictionary mapping Bot and ApplicationBuilder classes to their relevant methods that will
|
||||
"""A dictionary mapping Bot, Application & ApplicationBuilder classes to their relevant methods
|
||||
that will
|
||||
be mentioned in 'Returned in' and 'Use in' admonitions in other classes' docstrings.
|
||||
Methods must be public, not aliases, not inherited from TelegramObject.
|
||||
"""
|
||||
@@ -83,13 +116,20 @@ class AdmonitionInserter:
|
||||
"""Dictionary with admonitions. Contains sub-dictionaries, one per admonition type.
|
||||
Each sub-dictionary matches bot methods (for "Shortcuts") or telegram classes (for other
|
||||
admonition types) to texts of admonitions, e.g.:
|
||||
|
||||
```
|
||||
{
|
||||
"use_in": {<class 'telegram._chatinvitelink.ChatInviteLink'>:
|
||||
<"Use in" admonition for ChatInviteLink>, ...},
|
||||
"available_in": {<class 'telegram._chatinvitelink.ChatInviteLink'>:
|
||||
<"Available in" admonition">, ...},
|
||||
"returned_in": {...}
|
||||
"use_in": {
|
||||
<class 'telegram._chatinvitelink.ChatInviteLink'>:
|
||||
<"Use in" admonition for ChatInviteLink>,
|
||||
...
|
||||
},
|
||||
"available_in": {
|
||||
<class 'telegram._chatinvitelink.ChatInviteLink'>:
|
||||
<"Available in" admonition">,
|
||||
...
|
||||
},
|
||||
"returned_in": {...}
|
||||
}
|
||||
```
|
||||
"""
|
||||
@@ -128,34 +168,6 @@ class AdmonitionInserter:
|
||||
# i.e. {telegram._files.sticker.Sticker: {":attr:`telegram.Message.sticker`", ...}}
|
||||
attrs_for_class = defaultdict(set)
|
||||
|
||||
# The following regex is supposed to capture a class name in a line like this:
|
||||
# media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send.
|
||||
#
|
||||
# Note that even if such typing description spans over multiple lines but each line ends
|
||||
# with a backslash (otherwise Sphinx will throw an error)
|
||||
# (e.g. EncryptedPassportElement.data), then Sphinx will combine these lines into a single
|
||||
# line automatically, and it will contain no backslash (only some extra many whitespaces
|
||||
# from the indentation).
|
||||
|
||||
attr_docstr_pattern = re.compile(
|
||||
r"^\s*(?P<attr_name>[a-z_]+)" # Any number of spaces, named group for attribute
|
||||
r"\s?\(" # Optional whitespace, opening parenthesis
|
||||
r".*" # Any number of characters (that could denote a built-in type)
|
||||
r":(class|obj):`.+`" # Marker of a classref, class name in backticks
|
||||
r".*\):" # Any number of characters, closing parenthesis, colon.
|
||||
# The ^ colon above along with parenthesis is important because it makes sure that
|
||||
# the class is mentioned in the attribute description, not in free text.
|
||||
r".*$", # Any number of characters, end of string (end of line)
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
# for properties: there is no attr name in docstring. Just check if there's a class name.
|
||||
prop_docstring_pattern = re.compile(r":(class|obj):`.+`.*:")
|
||||
|
||||
# pattern for iterating over potentially many class names in docstring for one attribute.
|
||||
# Tilde is optional (sometimes it is in the docstring, sometimes not).
|
||||
single_class_name_pattern = re.compile(r":(class|obj):`~?(?P<class_name>[\w.]*)`")
|
||||
|
||||
classes_to_inspect = inspect.getmembers(telegram, inspect.isclass) + inspect.getmembers(
|
||||
telegram.ext, inspect.isclass
|
||||
)
|
||||
@@ -166,40 +178,31 @@ class AdmonitionInserter:
|
||||
# docstrings.
|
||||
name_of_inspected_class_in_docstr = self._generate_class_name_for_link(inspected_class)
|
||||
|
||||
# Parsing part of the docstring with attributes (parsing of properties follows later)
|
||||
docstring_lines = inspect.getdoc(inspected_class).splitlines()
|
||||
lines_with_attrs = []
|
||||
for idx, line in enumerate(docstring_lines):
|
||||
if line.strip() == "Attributes:":
|
||||
lines_with_attrs = docstring_lines[idx + 1 :]
|
||||
break
|
||||
# Writing to dictionary: matching the class found in the type hint
|
||||
# and its subclasses to the attribute of the class being inspected.
|
||||
# The class in the attribute typehint (or its subclass) is the key,
|
||||
# ReST link to attribute of the class currently being inspected is the value.
|
||||
|
||||
for line in lines_with_attrs:
|
||||
if not (line_match := attr_docstr_pattern.match(line)):
|
||||
continue
|
||||
|
||||
target_attr = line_match.group("attr_name")
|
||||
# a typing description of one attribute can contain multiple classes
|
||||
for match in single_class_name_pattern.finditer(line):
|
||||
name_of_class_in_attr = match.group("class_name")
|
||||
|
||||
# Writing to dictionary: matching the class found in the docstring
|
||||
# and its subclasses to the attribute of the class being inspected.
|
||||
# The class in the attribute docstring (or its subclass) is the key,
|
||||
# ReST link to attribute of the class currently being inspected is the value.
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
arg=name_of_class_in_attr,
|
||||
dict_of_methods_for_class=attrs_for_class,
|
||||
link=f":attr:`{name_of_inspected_class_in_docstr}.{target_attr}`",
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Available in' admonition "
|
||||
f"(admonition_inserter.py). Class {name_of_class_in_attr} present in "
|
||||
f"attribute {target_attr} of class {name_of_inspected_class_in_docstr}"
|
||||
f" could not be resolved. {e!s}"
|
||||
) from e
|
||||
# best effort - args of __init__ means not all attributes are covered, but there is no
|
||||
# other way to get type hints of all attributes, other than doing ast parsing maybe.
|
||||
# (Docstring parsing was discontinued with the closing of #4414)
|
||||
type_hints = typing.get_type_hints(inspected_class.__init__, localns=TG_NAMESPACE)
|
||||
class_attrs = [slot for slot in mro_slots(inspected_class) if not slot.startswith("_")]
|
||||
for target_attr in class_attrs:
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
dict_of_methods_for_class=attrs_for_class,
|
||||
link=f":attr:`{name_of_inspected_class_in_docstr}.{target_attr}`",
|
||||
type_hints={target_attr: type_hints.get(target_attr)},
|
||||
resolve_nested_type_vars=False,
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Available in' admonition "
|
||||
f"(admonition_inserter.py). Class {inspected_class} present in "
|
||||
f"attribute {target_attr} of class {name_of_inspected_class_in_docstr}"
|
||||
f" could not be resolved. {e!s}"
|
||||
) from e
|
||||
|
||||
# Properties need to be parsed separately because they act like attributes but not
|
||||
# listed as attributes.
|
||||
@@ -210,39 +213,29 @@ class AdmonitionInserter:
|
||||
if prop_name not in inspected_class.__dict__:
|
||||
continue
|
||||
|
||||
# 1. Can't use typing.get_type_hints because double-quoted type hints
|
||||
# (like "Application") will throw a NameError
|
||||
# 2. Can't use inspect.signature because return annotations of properties can be
|
||||
# hard to parse (like "(self) -> BD").
|
||||
# 3. fget is used to access the actual function under the property wrapper
|
||||
docstring = inspect.getdoc(getattr(inspected_class, prop_name).fget)
|
||||
if docstring is None:
|
||||
continue
|
||||
# fget is used to access the actual function under the property wrapper
|
||||
type_hints = typing.get_type_hints(
|
||||
getattr(inspected_class, prop_name).fget, localns=TG_NAMESPACE
|
||||
)
|
||||
|
||||
first_line = docstring.splitlines()[0]
|
||||
if not prop_docstring_pattern.match(first_line):
|
||||
continue
|
||||
|
||||
for match in single_class_name_pattern.finditer(first_line):
|
||||
name_of_class_in_prop = match.group("class_name")
|
||||
|
||||
# Writing to dictionary: matching the class found in the docstring and its
|
||||
# subclasses to the property of the class being inspected.
|
||||
# The class in the property docstring (or its subclass) is the key,
|
||||
# ReST link to property of the class currently being inspected is the value.
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
arg=name_of_class_in_prop,
|
||||
dict_of_methods_for_class=attrs_for_class,
|
||||
link=f":attr:`{name_of_inspected_class_in_docstr}.{prop_name}`",
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Available in' admonition "
|
||||
f"(admonition_inserter.py). Class {name_of_class_in_prop} present in "
|
||||
f"property {prop_name} of class {name_of_inspected_class_in_docstr}"
|
||||
f" could not be resolved. {e!s}"
|
||||
) from e
|
||||
# Writing to dictionary: matching the class found in the docstring and its
|
||||
# subclasses to the property of the class being inspected.
|
||||
# The class in the property docstring (or its subclass) is the key,
|
||||
# ReST link to property of the class currently being inspected is the value.
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
dict_of_methods_for_class=attrs_for_class,
|
||||
link=f":attr:`{name_of_inspected_class_in_docstr}.{prop_name}`",
|
||||
type_hints={prop_name: type_hints.get("return")},
|
||||
resolve_nested_type_vars=False,
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Available in' admonition "
|
||||
f"(admonition_inserter.py). Class {inspected_class} present in "
|
||||
f"property {prop_name} of class {name_of_inspected_class_in_docstr}"
|
||||
f" could not be resolved. {e!s}"
|
||||
) from e
|
||||
|
||||
return self._generate_admonitions(attrs_for_class, admonition_type="available_in")
|
||||
|
||||
@@ -250,29 +243,28 @@ class AdmonitionInserter:
|
||||
"""Creates a dictionary with 'Returned in' admonitions for classes that are returned
|
||||
in Bot's and ApplicationBuilder's methods.
|
||||
"""
|
||||
|
||||
# Generate a mapping of classes to ReST links to Bot methods which return it,
|
||||
# i.e. {<class 'telegram._message.Message'>: {:meth:`telegram.Bot.send_message`, ...}}
|
||||
methods_for_class = defaultdict(set)
|
||||
|
||||
for cls, method_names in self.METHOD_NAMES_FOR_BOT_AND_APPBUILDER.items():
|
||||
for cls, method_names in self.METHOD_NAMES_FOR_BOT_APP_APPBUILDER.items():
|
||||
for method_name in method_names:
|
||||
sig = inspect.signature(getattr(cls, method_name))
|
||||
ret_annot = sig.return_annotation
|
||||
|
||||
method_link = self._generate_link_to_method(method_name, cls)
|
||||
arg = getattr(cls, method_name)
|
||||
ret_type_hint = typing.get_type_hints(arg, localns=TG_NAMESPACE)
|
||||
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
arg=ret_annot,
|
||||
dict_of_methods_for_class=methods_for_class,
|
||||
link=method_link,
|
||||
type_hints={"return": ret_type_hint.get("return")},
|
||||
resolve_nested_type_vars=False,
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Returned in' admonition "
|
||||
f"(admonition_inserter.py). {cls}, method {method_name}. "
|
||||
f"Couldn't resolve type hint in return annotation {ret_annot}. {e!s}"
|
||||
f"Couldn't resolve type hint in return annotation {ret_type_hint}. {e!s}"
|
||||
) from e
|
||||
|
||||
return self._generate_admonitions(methods_for_class, admonition_type="returned_in")
|
||||
@@ -299,8 +291,13 @@ class AdmonitionInserter:
|
||||
# inspect methods of all telegram classes for return statements that indicate
|
||||
# that this given method is a shortcut for a Bot method
|
||||
for _class_name, cls in inspect.getmembers(telegram, predicate=inspect.isclass):
|
||||
# no need to inspect Bot's own methods, as Bot can't have shortcuts in Bot
|
||||
if not cls.__module__.startswith("telegram"):
|
||||
# For some reason inspect.getmembers() also yields some classes that are
|
||||
# imported in the namespace but not part of the telegram module.
|
||||
continue
|
||||
|
||||
if cls is telegram.Bot:
|
||||
# no need to inspect Bot's own methods, as Bot can't have shortcuts in Bot
|
||||
continue
|
||||
|
||||
for method_name, method in _iter_own_public_methods(cls):
|
||||
@@ -310,9 +307,7 @@ class AdmonitionInserter:
|
||||
continue
|
||||
|
||||
bot_method = getattr(telegram.Bot, bot_method_match.group())
|
||||
|
||||
link_to_shortcut_method = self._generate_link_to_method(method_name, cls)
|
||||
|
||||
shortcuts_for_bot_method[bot_method].add(link_to_shortcut_method)
|
||||
|
||||
return self._generate_admonitions(shortcuts_for_bot_method, admonition_type="shortcuts")
|
||||
@@ -327,26 +322,24 @@ class AdmonitionInserter:
|
||||
# {:meth:`telegram.Bot.answer_inline_query`, ...}}
|
||||
methods_for_class = defaultdict(set)
|
||||
|
||||
for cls, method_names in self.METHOD_NAMES_FOR_BOT_AND_APPBUILDER.items():
|
||||
for cls, method_names in self.METHOD_NAMES_FOR_BOT_APP_APPBUILDER.items():
|
||||
for method_name in method_names:
|
||||
method_link = self._generate_link_to_method(method_name, cls)
|
||||
|
||||
sig = inspect.signature(getattr(cls, method_name))
|
||||
parameters = sig.parameters
|
||||
|
||||
for param in parameters.values():
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
arg=param.annotation,
|
||||
dict_of_methods_for_class=methods_for_class,
|
||||
link=method_link,
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Use in' admonition "
|
||||
f"(admonition_inserter.py). {cls}, method {method_name}, parameter "
|
||||
f"{param}: Couldn't resolve type hint {param.annotation}. {e!s}"
|
||||
) from e
|
||||
arg = getattr(cls, method_name)
|
||||
param_type_hints = typing.get_type_hints(arg, localns=TG_NAMESPACE)
|
||||
param_type_hints.pop("return", None)
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
dict_of_methods_for_class=methods_for_class,
|
||||
link=method_link,
|
||||
type_hints=param_type_hints,
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Use in' admonition "
|
||||
f"(admonition_inserter.py). {cls}, method {method_name}, parameter "
|
||||
) from e
|
||||
|
||||
return self._generate_admonitions(methods_for_class, admonition_type="use_in")
|
||||
|
||||
@@ -362,7 +355,7 @@ class AdmonitionInserter:
|
||||
for idx, value in list(enumerate(lines)):
|
||||
if value.startswith(
|
||||
(
|
||||
".. seealso:",
|
||||
# ".. seealso:",
|
||||
# The docstring contains heading "Examples:", but Sphinx will have it converted
|
||||
# to ".. admonition: Examples":
|
||||
".. admonition:: Examples",
|
||||
@@ -435,12 +428,12 @@ class AdmonitionInserter:
|
||||
return admonition_for_class
|
||||
|
||||
@staticmethod
|
||||
def _generate_class_name_for_link(cls: type) -> str:
|
||||
def _generate_class_name_for_link(cls_: type) -> str:
|
||||
"""Generates class name that can be used in a ReST link."""
|
||||
|
||||
# Check for potential presence of ".ext.", we will need to keep it.
|
||||
ext = ".ext" if ".ext." in str(cls) else ""
|
||||
return f"telegram{ext}.{cls.__name__}"
|
||||
ext = ".ext" if ".ext." in str(cls_) else ""
|
||||
return f"telegram{ext}.{cls_.__name__}"
|
||||
|
||||
def _generate_link_to_method(self, method_name: str, cls: type) -> str:
|
||||
"""Generates a ReST link to a method of a telegram class."""
|
||||
@@ -448,19 +441,22 @@ class AdmonitionInserter:
|
||||
return f":meth:`{self._generate_class_name_for_link(cls)}.{method_name}`"
|
||||
|
||||
@staticmethod
|
||||
def _iter_subclasses(cls: type) -> Iterator:
|
||||
def _iter_subclasses(cls_: type) -> Iterator:
|
||||
if not hasattr(cls_, "__subclasses__") or cls_ is telegram.TelegramObject:
|
||||
return iter([])
|
||||
return (
|
||||
# exclude private classes
|
||||
c
|
||||
for c in cls.__subclasses__()
|
||||
for c in cls_.__subclasses__()
|
||||
if not str(c).split(".")[-1].startswith("_")
|
||||
)
|
||||
|
||||
def _resolve_arg_and_add_link(
|
||||
self,
|
||||
arg: Any,
|
||||
dict_of_methods_for_class: defaultdict,
|
||||
link: str,
|
||||
type_hints: dict[str, type],
|
||||
resolve_nested_type_vars: bool = True,
|
||||
) -> None:
|
||||
"""A helper method. Tries to resolve the arg into a valid class. In case of success,
|
||||
adds the link (to a method, attribute, or property) for that class' and its subclasses'
|
||||
@@ -468,7 +464,9 @@ class AdmonitionInserter:
|
||||
|
||||
**Modifies dictionary in place.**
|
||||
"""
|
||||
for cls in self._resolve_arg(arg):
|
||||
type_hints.pop("self", None)
|
||||
|
||||
for cls in self._resolve_arg(type_hints, resolve_nested_type_vars):
|
||||
# When trying to resolve an argument from args or return annotation,
|
||||
# the method _resolve_arg returns None if nothing could be resolved.
|
||||
# Also, if class was resolved correctly, "telegram" will definitely be in its str().
|
||||
@@ -480,88 +478,67 @@ class AdmonitionInserter:
|
||||
for subclass in self._iter_subclasses(cls):
|
||||
dict_of_methods_for_class[subclass].add(link)
|
||||
|
||||
def _resolve_arg(self, arg: Any) -> Iterator[Union[type, None]]:
|
||||
def _resolve_arg(
|
||||
self,
|
||||
type_hints: dict[str, type],
|
||||
resolve_nested_type_vars: bool,
|
||||
) -> list[type]:
|
||||
"""Analyzes an argument of a method and recursively yields classes that the argument
|
||||
or its sub-arguments (in cases like Union[...]) belong to, if they can be resolved to
|
||||
telegram or telegram.ext classes.
|
||||
|
||||
Args:
|
||||
type_hints: A dictionary of argument names and their types.
|
||||
resolve_nested_type_vars: If True, nested type variables (like Application[BT, …])
|
||||
will be resolved to their actual classes. If False, only the outermost type
|
||||
variable will be resolved. *Only* affects ptb classes, not built-in types.
|
||||
Useful for checking the return type of methods, where nested type variables
|
||||
are not really useful.
|
||||
|
||||
Raises `NotImplementedError`.
|
||||
"""
|
||||
|
||||
origin = typing.get_origin(arg)
|
||||
def _is_ptb_class(cls: type) -> bool:
|
||||
if not hasattr(cls, "__module__"):
|
||||
return False
|
||||
return cls.__module__.startswith("telegram")
|
||||
|
||||
if (
|
||||
origin in (collections.abc.Callable, typing.IO)
|
||||
or arg is None
|
||||
# no other check available (by type or origin) for these:
|
||||
or str(type(arg)) in ("<class 'typing._SpecialForm'>", "<class 'ellipsis'>")
|
||||
):
|
||||
pass
|
||||
# will be edited in place
|
||||
telegram_classes = set()
|
||||
|
||||
# RECURSIVE CALLS
|
||||
# for cases like Union[Sequence....
|
||||
elif origin in (
|
||||
Union,
|
||||
collections.abc.Coroutine,
|
||||
collections.abc.Sequence,
|
||||
):
|
||||
for sub_arg in typing.get_args(arg):
|
||||
yield from self._resolve_arg(sub_arg)
|
||||
def recurse_type(type_, is_recursed_from_ptb_class: bool):
|
||||
next_is_recursed_from_ptb_class = is_recursed_from_ptb_class or _is_ptb_class(type_)
|
||||
|
||||
elif isinstance(arg, typing.TypeVar):
|
||||
# gets access to the "bound=..." parameter
|
||||
yield from self._resolve_arg(arg.__bound__)
|
||||
# END RECURSIVE CALLS
|
||||
if hasattr(type_, "__origin__"): # For generic types like Union, List, etc.
|
||||
# Make sure it's not a telegram.ext generic type (e.g. ContextTypes[...])
|
||||
org = typing.get_origin(type_)
|
||||
if "telegram.ext" in str(org):
|
||||
telegram_classes.add(org)
|
||||
|
||||
elif isinstance(arg, typing.ForwardRef):
|
||||
m = self.FORWARD_REF_PATTERN.match(str(arg))
|
||||
# We're sure it's a ForwardRef, so, unless it belongs to known exceptions,
|
||||
# the class must be resolved.
|
||||
# If it isn't resolved, we'll have the program throw an exception to be sure.
|
||||
try:
|
||||
cls = self._resolve_class(m.group("class_name"))
|
||||
except AttributeError as exc:
|
||||
# skip known ForwardRef's that need not be resolved to a Telegram class
|
||||
if self.FORWARD_REF_SKIP_PATTERN.match(str(arg)):
|
||||
pass
|
||||
else:
|
||||
raise NotImplementedError(f"Could not process ForwardRef: {arg}") from exc
|
||||
else:
|
||||
yield cls
|
||||
args = typing.get_args(type_)
|
||||
for arg in args:
|
||||
recurse_type(arg, next_is_recursed_from_ptb_class)
|
||||
elif isinstance(type_, typing.TypeVar) and (
|
||||
resolve_nested_type_vars or not is_recursed_from_ptb_class
|
||||
):
|
||||
# gets access to the "bound=..." parameter
|
||||
recurse_type(type_.__bound__, next_is_recursed_from_ptb_class)
|
||||
elif inspect.isclass(type_) and "telegram" in inspect.getmodule(type_).__name__:
|
||||
telegram_classes.add(type_)
|
||||
elif isinstance(type_, typing.ForwardRef):
|
||||
# Resolving ForwardRef is not easy. https://peps.python.org/pep-0749/ will
|
||||
# hopefully make it better by introducing typing.resolve_forward_ref() in py3.14
|
||||
# but that's not there yet
|
||||
# So for now we fall back to a best effort approach of guessing if the class is
|
||||
# available in tg or tg.ext
|
||||
with contextlib.suppress(AttributeError):
|
||||
telegram_classes.add(self._resolve_class(type_.__forward_arg__))
|
||||
|
||||
# For custom generics like telegram.ext._application.Application[~BT, ~CCT, ~UD...].
|
||||
# This must come before the check for isinstance(type) because GenericAlias can also be
|
||||
# recognized as type if it belongs to <class 'types.GenericAlias'>.
|
||||
elif str(type(arg)) in (
|
||||
"<class 'typing._GenericAlias'>",
|
||||
"<class 'types.GenericAlias'>",
|
||||
"<class 'typing._LiteralGenericAlias'>",
|
||||
):
|
||||
if "telegram" in str(arg):
|
||||
# get_origin() of telegram.ext._application.Application[~BT, ~CCT, ~UD...]
|
||||
# will produce <class 'telegram.ext._application.Application'>
|
||||
yield origin
|
||||
for type_hint in type_hints.values():
|
||||
if type_hint is not None:
|
||||
recurse_type(type_hint, False)
|
||||
|
||||
elif isinstance(arg, type):
|
||||
if "telegram" in str(arg):
|
||||
yield arg
|
||||
|
||||
# For some reason "InlineQueryResult", "InputMedia" & some others are currently not
|
||||
# recognized as ForwardRefs and are identified as plain strings.
|
||||
elif isinstance(arg, str):
|
||||
# args like "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]" can be recognized as strings.
|
||||
# Remove whatever is in the square brackets because it doesn't need to be parsed.
|
||||
arg = re.sub(r"\[.+]", "", arg)
|
||||
|
||||
cls = self._resolve_class(arg)
|
||||
# Here we don't want an exception to be thrown since we're not sure it's ForwardRef
|
||||
if cls is not None:
|
||||
yield cls
|
||||
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Cannot process argument {arg} of type {type(arg)} (origin {origin})"
|
||||
)
|
||||
return list(telegram_classes)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_class(name: str) -> Union[type, None]:
|
||||
@@ -581,16 +558,14 @@ class AdmonitionInserter:
|
||||
f"telegram.ext.{name}",
|
||||
f"telegram.ext.filters.{name}",
|
||||
):
|
||||
try:
|
||||
return eval(option)
|
||||
# NameError will be raised if trying to eval just name and it doesn't work, e.g.
|
||||
# "Name 'ApplicationBuilder' is not defined".
|
||||
# AttributeError will be raised if trying to e.g. eval f"telegram.{name}" when the
|
||||
# class denoted by `name` actually belongs to `telegram.ext`:
|
||||
# "module 'telegram' has no attribute 'ApplicationBuilder'".
|
||||
# If neither option works, this is not a PTB class.
|
||||
except (NameError, AttributeError):
|
||||
continue
|
||||
with contextlib.suppress(NameError, AttributeError):
|
||||
return eval(option)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -61,5 +61,5 @@
|
||||
}
|
||||
.admonition.returned-in > ul, .admonition.available-in > ul, .admonition.use-in > ul, .admonition.shortcuts > ul {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -96,4 +96,6 @@
|
||||
|
||||
.. |allow_paid_broadcast| replace:: Pass True to allow up to :tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second, ignoring `broadcasting limits <https://core.telegram.org/bots/faq#how-can-i-message-all-of-my-bot-39s-subscribers-at-once>`__ for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.
|
||||
|
||||
.. |tz-naive-dtms| replace:: For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
.. |tz-naive-dtms| replace:: For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable=unused-argument
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
"""Simple state machine to handle user support.
|
||||
One admin is supported. The admin can have one active conversation at a time. Other users
|
||||
are put on hold until the admin finishes the current conversation.
|
||||
In each conversation, the admin and the user take turns to send messages.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CommandHandler,
|
||||
ContextTypes,
|
||||
FiniteStateMachine,
|
||||
MessageHandler,
|
||||
State,
|
||||
StateInfo,
|
||||
filters,
|
||||
)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram.ext.Application").setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserSupportMachine(FiniteStateMachine[Optional[int]]):
|
||||
|
||||
HOLD = State("HOLD")
|
||||
WELCOMING = State("WELCOMING")
|
||||
WAITING_FOR_REPLY = State("WAITING_FOR_REPLY")
|
||||
WRITING = State("WRITING")
|
||||
|
||||
def __init__(self, admin_id: int):
|
||||
self.admin_id = admin_id
|
||||
super().__init__()
|
||||
|
||||
def _get_admin_state(self) -> tuple[State, int]:
|
||||
return self._states[self.admin_id]
|
||||
|
||||
def get_state_info(self, update: object) -> StateInfo[Optional[int]]:
|
||||
if not isinstance(update, Update) or not (user := update.effective_user):
|
||||
key = None
|
||||
state, version = self.states[key]
|
||||
return StateInfo(key=key, state=state, version=version)
|
||||
|
||||
# Admin is easy - just return the state
|
||||
admin_state, admin_version = self._get_admin_state()
|
||||
if user.id == self.admin_id:
|
||||
logging.debug("Returning admin state: %s", admin_state)
|
||||
return StateInfo(self.admin_id, admin_state, admin_version)
|
||||
|
||||
# If the user state is active in the conversation, we can just return that state
|
||||
user_state, user_version = self._states[user.id]
|
||||
if user_state.matches(self.WELCOMING | self.WRITING | self.WAITING_FOR_REPLY):
|
||||
logging.debug("Returning user state: %s", user_state)
|
||||
return StateInfo(user.id, user_state, user_version)
|
||||
|
||||
# On first interaction, we need to determine what to do with the user
|
||||
# if the admin is not idle, we put the user on hold. Otherwise, they may send the first
|
||||
# message, and we put the admin in waiting for reply to avoid another user occupying the
|
||||
# admin first
|
||||
effective_user_state = self.HOLD if admin_state != State.IDLE else self.WELCOMING
|
||||
self._do_set_state(user.id, effective_user_state, user_version)
|
||||
if effective_user_state == self.WELCOMING:
|
||||
self._do_set_state(self.admin_id, self.WAITING_FOR_REPLY)
|
||||
|
||||
logging.debug("Returning user state: %s", effective_user_state)
|
||||
return StateInfo(user.id, effective_user_state, user_version)
|
||||
|
||||
|
||||
async def welcome_user(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.forward(context.bot_data["admin_id"])
|
||||
suffix = ""
|
||||
if UserSupportMachine.HOLD in context.fsm.get_state_history(context.fsm_state_info.key)[:-1]:
|
||||
suffix = " Thank you for patiently waiting. We hope you enjoyed the music."
|
||||
|
||||
await update.effective_message.reply_text(
|
||||
"Welcome! Your message has been forwarded to the admin. "
|
||||
f"They will get back to you soon.{suffix}"
|
||||
)
|
||||
await context.set_state(UserSupportMachine.WAITING_FOR_REPLY)
|
||||
await context.fsm.set_state(context.bot_data["admin_id"], UserSupportMachine.WRITING)
|
||||
context.bot_data["active_user"] = update.effective_user.id
|
||||
|
||||
|
||||
async def conversation_timeout(context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
active_user = context.bot_data.get("active_user")
|
||||
admin_id = context.bot_data["admin_id"]
|
||||
|
||||
async def handle(user_id: int) -> None:
|
||||
await context.bot.send_message(
|
||||
user_id, "The conversation has been stopped due to inactivity."
|
||||
)
|
||||
await context.fsm.set_state(user_id, State.IDLE)
|
||||
|
||||
if active_user:
|
||||
await handle(active_user)
|
||||
await handle(admin_id)
|
||||
|
||||
|
||||
async def handle_reply(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not (active_user := context.bot_data.get("active_user")):
|
||||
logger.warning("No active user found, ignoring message")
|
||||
|
||||
target = (
|
||||
active_user
|
||||
if update.effective_user.id == (admin_id := context.bot_data["admin_id"])
|
||||
else admin_id
|
||||
)
|
||||
await context.bot.send_message(target, update.effective_message.text)
|
||||
logging.debug("Forwarded message to %s", target)
|
||||
await context.set_state(UserSupportMachine.WAITING_FOR_REPLY)
|
||||
logging.debug("Done setting state to WAITING_FOR_REPLY for %s", target)
|
||||
await context.fsm.set_state(target, UserSupportMachine.WRITING)
|
||||
logging.debug("Done setting state to WRITING for %s, context.fsm_key")
|
||||
|
||||
context.fsm.schedule_timeout(
|
||||
when=30,
|
||||
callback=conversation_timeout,
|
||||
cancel_keys=[active_user, admin_id],
|
||||
)
|
||||
|
||||
|
||||
async def stop_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
text = "The conversation has been stopped."
|
||||
admin_id = context.bot_data["admin_id"]
|
||||
active_user = context.bot_data.get("active_user")
|
||||
|
||||
await context.bot.send_message(admin_id, text)
|
||||
await context.fsm.set_state(admin_id, State.IDLE)
|
||||
if active_user:
|
||||
await context.bot.send_message(active_user, text)
|
||||
await context.fsm.set_state(active_user, State.IDLE)
|
||||
|
||||
|
||||
async def hold_melody(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text(
|
||||
"You have been put on hold. The admin will get back to you soon. Please hear some music "
|
||||
"while you wait: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
)
|
||||
|
||||
|
||||
async def not_your_turn(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text(
|
||||
"It's not your turn yet. Please wait for the other party to reply to your message."
|
||||
)
|
||||
|
||||
|
||||
async def unsupported_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text("This message is not supported.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
application.fsm = UserSupportMachine(admin_id=123456)
|
||||
application.fsm.set_job_queue(application.job_queue)
|
||||
application.bot_data["admin_id"] = application.fsm.admin_id
|
||||
|
||||
# Users are welcomed only if they are in the corresponding state
|
||||
application.add_handler(
|
||||
MessageHandler(~filters.User(application.fsm.admin_id) & filters.TEXT, welcome_user),
|
||||
state=UserSupportMachine.WELCOMING,
|
||||
)
|
||||
|
||||
# Conversation logic:
|
||||
# * forward messages between user and admin
|
||||
# * stop the conversation at any time (admin or user)
|
||||
# * point out that the other party is currently writing
|
||||
# Important: Order matters!
|
||||
application.add_handler(
|
||||
CommandHandler("stop", stop_conversation),
|
||||
state=UserSupportMachine.WAITING_FOR_REPLY | UserSupportMachine.WRITING,
|
||||
)
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, handle_reply), state=UserSupportMachine.WRITING
|
||||
)
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, not_your_turn), state=UserSupportMachine.WAITING_FOR_REPLY
|
||||
)
|
||||
|
||||
# If the admin is busy, put the user on hold
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, hold_melody), state=UserSupportMachine.HOLD
|
||||
)
|
||||
|
||||
# Fallback
|
||||
application.add_handler(MessageHandler(filters.ALL, unsupported_message), state=State.ANY)
|
||||
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable=unused-argument
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
"""State machine bot showcasing how concurrency can be handled with FSM.
|
||||
How to use:
|
||||
|
||||
* Use Case 1: Concurrent balance updates
|
||||
- /unsafe_update <balance_update>: Unsafe update of the wallet balance. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect
|
||||
- /safe_update <balance_update>: Safe update of the wallet balance. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect
|
||||
|
||||
* Use Case 2: Declare a winner - who is the fastest?
|
||||
- /unsafe_declare_winner: Unsafe declaration of the user as winner. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect. Needs restart
|
||||
after the winner is declared.
|
||||
- /safe_declare_winner: Safe declaration of the user as winner. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect. Needs restart
|
||||
after the winner is declared.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CommandHandler,
|
||||
ContextTypes,
|
||||
FiniteStateMachine,
|
||||
MessageHandler,
|
||||
State,
|
||||
StateInfo,
|
||||
filters,
|
||||
)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram.ext.Application").setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConcurrentMachine(FiniteStateMachine[None]):
|
||||
"""This FSM only knows a global state for the whole bot"""
|
||||
|
||||
UPDATING_BALANCE = State("UPDATING_BALANCE")
|
||||
WINNER_DECLARED = State("WINNER_DECLARED")
|
||||
|
||||
def get_state_info(self, update: object) -> StateInfo[None]:
|
||||
state, version = self.states[None]
|
||||
return StateInfo(key=None, state=state, version=version)
|
||||
|
||||
|
||||
########################################
|
||||
# Use case 1: Concurrent balance updates
|
||||
########################################
|
||||
|
||||
|
||||
async def update_balance(context: ContextTypes.DEFAULT_TYPE, update: Update) -> None:
|
||||
initial_balance = context.bot_data.get("balance", 0)
|
||||
balance_update = int(context.args[0])
|
||||
# Simulate heavy computation
|
||||
await update.effective_message.reply_text(
|
||||
f"Initiating balance update: {initial_balance}. Updating ..."
|
||||
)
|
||||
await update.effective_chat.send_action(ChatAction.TYPING)
|
||||
await asyncio.sleep(4.5)
|
||||
new_balance = context.bot_data["balance"] = initial_balance + balance_update
|
||||
await update.effective_message.reply_text(f"Balance updated. New balance: {new_balance}")
|
||||
|
||||
|
||||
async def unsafe_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Unsafe update of the wallet balance"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await context.fsm.set_state(context.fsm_state_info.key, ConcurrentMachine.UPDATING_BALANCE)
|
||||
|
||||
# At this point, the lock is released such that multiple updates can update
|
||||
# the balance concurrently. This can lead to race conditions.
|
||||
await update_balance(context, update)
|
||||
|
||||
await context.fsm.set_state(context.fsm_state_info.key, State.IDLE)
|
||||
|
||||
|
||||
async def safe_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Safe update of the wallet balance"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async with context.as_fsm_state(ConcurrentMachine.UPDATING_BALANCE):
|
||||
# At this point, the lock is acquired such that only one update can update
|
||||
# the balance at a time. This prevents race conditions.
|
||||
await update_balance(context, update)
|
||||
|
||||
|
||||
async def busy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Busy state"""
|
||||
await update.effective_message.reply_text("I'm busy, try again later.")
|
||||
|
||||
|
||||
####################################################
|
||||
# Use case 2: Declare a winner - who is the fastest?
|
||||
####################################################
|
||||
|
||||
|
||||
async def declare_winner_unsafe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Declare the user as winner"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Unsafe state update: No version check, so the state might have already changed
|
||||
await context.fsm.set_state(context.fsm_state_info.key, ConcurrentMachine.WINNER_DECLARED)
|
||||
await update.effective_message.reply_text("You are the winner!")
|
||||
|
||||
|
||||
async def declare_winner_safe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Declare the user as winner"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
try:
|
||||
await context.set_state(ConcurrentMachine.WINNER_DECLARED)
|
||||
await update.effective_message.reply_text("You are the winner!")
|
||||
except ValueError:
|
||||
await update.effective_message.reply_text(
|
||||
"Sorry, you are too late. Someone else was faster."
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
application = Application.builder().token("TOKEN").concurrent_updates(True).build()
|
||||
application.fsm = ConcurrentMachine()
|
||||
|
||||
# Note: OR-combination of states is used here to allow both use cases to be handled
|
||||
# in parallel. Not really necessary for the showcasing, just a nice touch :)
|
||||
|
||||
# Use case 2: Declare a winner - who is the fastest?
|
||||
application.add_handler(
|
||||
CommandHandler("unsafe_declare_winner", declare_winner_unsafe),
|
||||
state=State.IDLE | ConcurrentMachine.UPDATING_BALANCE,
|
||||
)
|
||||
application.add_handler(
|
||||
CommandHandler("safe_declare_winner", declare_winner_safe),
|
||||
state=State.IDLE | ConcurrentMachine.UPDATING_BALANCE,
|
||||
)
|
||||
|
||||
# Use case 1: Concurrent balance updates
|
||||
application.add_handler(
|
||||
CommandHandler("unsafe_update", unsafe_update, has_args=1),
|
||||
state=State.IDLE | ConcurrentMachine.WINNER_DECLARED,
|
||||
)
|
||||
application.add_handler(
|
||||
CommandHandler("safe_update", safe_update, has_args=1),
|
||||
state=State.IDLE | ConcurrentMachine.WINNER_DECLARED,
|
||||
)
|
||||
# Order matters, so this needs to be added last
|
||||
application.add_handler(
|
||||
MessageHandler(filters.ALL, busy), state=ConcurrentMachine.UPDATING_BALANCE
|
||||
)
|
||||
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-1
@@ -10,7 +10,7 @@ description = "We have made you a wrapper you can't refuse"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.9"
|
||||
license = "LGPL-3.0-only"
|
||||
license-files = { paths = ["LICENSE", "LICENSE.dual", "LICENSE.lesser"] }
|
||||
license-files = ["LICENSE", "LICENSE.dual", "LICENSE.lesser"]
|
||||
authors = [
|
||||
{ name = "Leandro Toledo", email = "devs@python-telegram-bot.org" }
|
||||
]
|
||||
|
||||
+177
-65
@@ -96,7 +96,15 @@ from telegram._utils.files import is_local_file, parse_file_input
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.strings import to_camel_case
|
||||
from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
|
||||
from telegram._utils.types import (
|
||||
BaseUrl,
|
||||
CorrectOptionID,
|
||||
FileInput,
|
||||
JSONDict,
|
||||
ODVInput,
|
||||
ReplyMarkup,
|
||||
TimePeriod,
|
||||
)
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram._webhookinfo import WebhookInfo
|
||||
from telegram.constants import InlineQueryLimit, ReactionEmoji
|
||||
@@ -126,6 +134,35 @@ if TYPE_CHECKING:
|
||||
BT = TypeVar("BT", bound="Bot")
|
||||
|
||||
|
||||
# Even though we document only {token} as supported insertion, we are a bit more flexible
|
||||
# internally and support additional variants. At the very least, we don't want the insertion
|
||||
# to be case sensitive.
|
||||
_SUPPORTED_INSERTIONS = {"token", "TOKEN", "bot_token", "BOT_TOKEN", "bot-token", "BOT-TOKEN"}
|
||||
_INSERTION_STRINGS = {f"{{{insertion}}}" for insertion in _SUPPORTED_INSERTIONS}
|
||||
|
||||
|
||||
class _TokenDict(dict):
|
||||
__slots__ = ("token",)
|
||||
|
||||
# small helper to make .format_map work without knowing which exact insertion name is used
|
||||
def __init__(self, token: str):
|
||||
self.token = token
|
||||
super().__init__()
|
||||
|
||||
def __missing__(self, key: str) -> str:
|
||||
if key in _SUPPORTED_INSERTIONS:
|
||||
return self.token
|
||||
raise KeyError(f"Base URL string contains unsupported insertion: {key}")
|
||||
|
||||
|
||||
def _parse_base_url(value: BaseUrl, token: str) -> str:
|
||||
if callable(value):
|
||||
return value(token)
|
||||
if any(insertion in value for insertion in _INSERTION_STRINGS):
|
||||
return value.format_map(_TokenDict(token))
|
||||
return value + token
|
||||
|
||||
|
||||
class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
"""This object represents a Telegram Bot.
|
||||
|
||||
@@ -193,8 +230,40 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
|
||||
Args:
|
||||
token (:obj:`str`): Bot's unique authentication token.
|
||||
base_url (:obj:`str`, optional): Telegram Bot API service URL.
|
||||
base_url (:obj:`str` | Callable[[:obj:`str`], :obj:`str`], optional): Telegram Bot API
|
||||
service URL. If the string contains ``{token}``, it will be replaced with the bot's
|
||||
token. If a callable is passed, it will be called with the bot's token as the only
|
||||
argument and must return the base URL. Otherwise, the token will be appended to the
|
||||
string. Defaults to ``"https://api.telegram.org/bot"``.
|
||||
|
||||
Tip:
|
||||
Customizing the base URL can be used to run a bot against
|
||||
:wiki:`Local Bot API Server <Local-Bot-API-Server>` or using Telegrams
|
||||
`test environment \
|
||||
<https://core.telegram.org/bots/features#dedicated-test-environment>`_.
|
||||
|
||||
Example:
|
||||
``"https://api.telegram.org/bot{token}/test"``
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
base_file_url (:obj:`str`, optional): Telegram Bot API file URL.
|
||||
If the string contains ``{token}``, it will be replaced with the bot's
|
||||
token. If a callable is passed, it will be called with the bot's token as the only
|
||||
argument and must return the base URL. Otherwise, the token will be appended to the
|
||||
string. Defaults to ``"https://api.telegram.org/bot"``.
|
||||
|
||||
Tip:
|
||||
Customizing the base URL can be used to run a bot against
|
||||
:wiki:`Local Bot API Server <Local-Bot-API-Server>` or using Telegrams
|
||||
`test environment \
|
||||
<https://core.telegram.org/bots/features#dedicated-test-environment>`_.
|
||||
|
||||
Example:
|
||||
``"https://api.telegram.org/file/bot{token}/test"``
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
request (:class:`telegram.request.BaseRequest`, optional): Pre initialized
|
||||
:class:`telegram.request.BaseRequest` instances. Will be used for all bot methods
|
||||
*except* for :meth:`get_updates`. If not passed, an instance of
|
||||
@@ -239,8 +308,8 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
def __init__(
|
||||
self,
|
||||
token: str,
|
||||
base_url: str = "https://api.telegram.org/bot",
|
||||
base_file_url: str = "https://api.telegram.org/file/bot",
|
||||
base_url: BaseUrl = "https://api.telegram.org/bot",
|
||||
base_file_url: BaseUrl = "https://api.telegram.org/file/bot",
|
||||
request: Optional[BaseRequest] = None,
|
||||
get_updates_request: Optional[BaseRequest] = None,
|
||||
private_key: Optional[bytes] = None,
|
||||
@@ -252,8 +321,11 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
raise InvalidToken("You must pass the token you received from https://t.me/Botfather!")
|
||||
self._token: str = token
|
||||
|
||||
self._base_url: str = base_url + self._token
|
||||
self._base_file_url: str = base_file_url + self._token
|
||||
self._base_url: str = _parse_base_url(base_url, self._token)
|
||||
self._base_file_url: str = _parse_base_url(base_file_url, self._token)
|
||||
self._LOGGER.debug("Set Bot API URL: %s", self._base_url)
|
||||
self._LOGGER.debug("Set Bot API File URL: %s", self._base_file_url)
|
||||
|
||||
self._local_mode: bool = local_mode
|
||||
self._bot_user: Optional[User] = None
|
||||
self._private_key: Optional[bytes] = None
|
||||
@@ -264,7 +336,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
HTTPXRequest() if request is None else request,
|
||||
)
|
||||
|
||||
# this section is about issuing a warning when using HTTP/2 and connect to a self hosted
|
||||
# this section is about issuing a warning when using HTTP/2 and connect to a self-hosted
|
||||
# bot api instance, which currently only supports HTTP/1.1. Checking if a custom base url
|
||||
# is set is the best way to do that.
|
||||
|
||||
@@ -273,14 +345,14 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
if (
|
||||
isinstance(self._request[0], HTTPXRequest)
|
||||
and self._request[0].http_version == "2"
|
||||
and not base_url.startswith("https://api.telegram.org/bot")
|
||||
and not self.base_url.startswith("https://api.telegram.org/bot")
|
||||
):
|
||||
warning_string = "get_updates_request"
|
||||
|
||||
if (
|
||||
isinstance(self._request[1], HTTPXRequest)
|
||||
and self._request[1].http_version == "2"
|
||||
and not base_url.startswith("https://api.telegram.org/bot")
|
||||
and not self.base_url.startswith("https://api.telegram.org/bot")
|
||||
):
|
||||
if warning_string:
|
||||
warning_string += " and request"
|
||||
@@ -901,7 +973,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
self._bot_user = User.de_json(result, self)
|
||||
return self._bot_user # type: ignore[return-value]
|
||||
return self._bot_user
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
@@ -1421,7 +1493,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
audio: Union[FileInput, "Audio"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
performer: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -1483,7 +1555,11 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|sequenceargs|
|
||||
duration (:obj:`int`, optional): Duration of sent audio in seconds.
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent audio
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
performer (:obj:`str`, optional): Performer.
|
||||
title (:obj:`str`, optional): Track name.
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
@@ -1861,7 +1937,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
video: Union[FileInput, "Video"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -1919,7 +1995,11 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
.. versionchanged:: 20.0
|
||||
File paths as input is also accepted for bots *not* running in
|
||||
:paramref:`~telegram.Bot.local_mode`.
|
||||
duration (:obj:`int`, optional): Duration of sent video in seconds.
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
width (:obj:`int`, optional): Video width.
|
||||
height (:obj:`int`, optional): Video height.
|
||||
caption (:obj:`str`, optional): Video caption (may also be used when resending videos
|
||||
@@ -2040,7 +2120,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
video_note: Union[FileInput, "VideoNote"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
length: Optional[int] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -2092,7 +2172,11 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
.. versionchanged:: 20.0
|
||||
File paths as input is also accepted for bots *not* running in
|
||||
:paramref:`~telegram.Bot.local_mode`.
|
||||
duration (:obj:`int`, optional): Duration of sent video in seconds.
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
length (:obj:`int`, optional): Video width and height, i.e. diameter of the video
|
||||
message.
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
@@ -2188,7 +2272,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
animation: Union[FileInput, "Animation"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -2240,7 +2324,11 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
|
||||
.. versionchanged:: 13.2
|
||||
Accept :obj:`bytes` as input.
|
||||
duration (:obj:`int`, optional): Duration of sent animation in seconds.
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent
|
||||
animation in seconds.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
width (:obj:`int`, optional): Animation width.
|
||||
height (:obj:`int`, optional): Animation height.
|
||||
caption (:obj:`str`, optional): Animation caption (may also be used when resending
|
||||
@@ -2359,7 +2447,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
voice: Union[FileInput, "Voice"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -2420,7 +2508,11 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|sequenceargs|
|
||||
duration (:obj:`int`, optional): Duration of the voice message in seconds.
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the voice
|
||||
message in seconds.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
protect_content (:obj:`bool`, optional): |protect_content|
|
||||
|
||||
@@ -2692,7 +2784,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
longitude: Optional[float] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
@@ -2725,12 +2817,16 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
horizontal_accuracy (:obj:`int`, optional): The radius of uncertainty for the location,
|
||||
measured in meters;
|
||||
0-:tg-const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY`.
|
||||
live_period (:obj:`int`, optional): Period in seconds for which the location will be
|
||||
live_period (:obj:`int` | :class:`datetime.timedelta`, optional): Period in seconds for
|
||||
which the location will be
|
||||
updated, should be between
|
||||
:tg-const:`telegram.constants.LocationLimit.MIN_LIVE_PERIOD` and
|
||||
:tg-const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD`, or
|
||||
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
|
||||
locations that can be edited indefinitely.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
heading (:obj:`int`, optional): For live locations, a direction in which the user is
|
||||
moving, in degrees. Must be between
|
||||
:tg-const:`telegram.constants.LocationLimit.MIN_HEADING` and
|
||||
@@ -2848,7 +2944,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
business_connection_id: Optional[str] = None,
|
||||
*,
|
||||
location: Optional[Location] = None,
|
||||
@@ -2888,7 +2984,8 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
if specified.
|
||||
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new
|
||||
inline keyboard.
|
||||
live_period (:obj:`int`, optional): New period in seconds during which the location
|
||||
live_period (:obj:`int` | :class:`datetime.timedelta`, optional): New period in seconds
|
||||
during which the location
|
||||
can be updated, starting from the message send date. If
|
||||
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` is specified,
|
||||
then the location can be updated forever. Otherwise, the new value must not exceed
|
||||
@@ -2897,6 +2994,9 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
remains unchanged
|
||||
|
||||
.. versionadded:: 21.2.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
business_connection_id (:obj:`str`, optional): |business_id_str_edit|
|
||||
|
||||
.. versionadded:: 21.4
|
||||
@@ -3552,7 +3652,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
results: Union[
|
||||
Sequence["InlineQueryResult"], Callable[[int], Optional[Sequence["InlineQueryResult"]]]
|
||||
],
|
||||
cache_time: Optional[int] = None,
|
||||
cache_time: Optional[TimePeriod] = None,
|
||||
is_personal: Optional[bool] = None,
|
||||
next_offset: Optional[str] = None,
|
||||
button: Optional[InlineQueryResultsButton] = None,
|
||||
@@ -3588,8 +3688,12 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
a callable that accepts the current page index starting from 0. It must return
|
||||
either a list of :class:`telegram.InlineQueryResult` instances or :obj:`None` if
|
||||
there are no more results.
|
||||
cache_time (:obj:`int`, optional): The maximum amount of time in seconds that the
|
||||
cache_time (:obj:`int` | :class:`datetime.timedelta`, optional): The maximum amount of
|
||||
time in seconds that the
|
||||
result of the inline query may be cached on the server. Defaults to ``300``.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
is_personal (:obj:`bool`, optional): Pass :obj:`True`, if results may be cached on
|
||||
the server side only for the user that sent the query. By default,
|
||||
results may be returned to any user who sends the same query.
|
||||
@@ -3689,7 +3793,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
"allow_group_chats": allow_group_chats,
|
||||
"allow_channel_chats": allow_channel_chats,
|
||||
}
|
||||
return PreparedInlineMessage.de_json( # type: ignore[return-value]
|
||||
return PreparedInlineMessage.de_json(
|
||||
await self._post(
|
||||
"savePreparedInlineMessage",
|
||||
data,
|
||||
@@ -3744,7 +3848,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return UserProfilePhotos.de_json(result, self) # type: ignore[return-value]
|
||||
return UserProfilePhotos.de_json(result, self)
|
||||
|
||||
async def get_file(
|
||||
self,
|
||||
@@ -3809,7 +3913,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
if file_path and not is_local_file(file_path):
|
||||
result["file_path"] = f"{self._base_file_url}/{file_path}"
|
||||
|
||||
return File.de_json(result, self) # type: ignore[return-value]
|
||||
return File.de_json(result, self)
|
||||
|
||||
async def ban_chat_member(
|
||||
self,
|
||||
@@ -4005,7 +4109,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
text: Optional[str] = None,
|
||||
show_alert: Optional[bool] = None,
|
||||
url: Optional[str] = None,
|
||||
cache_time: Optional[int] = None,
|
||||
cache_time: Optional[TimePeriod] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -4036,9 +4140,13 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
opens your game - note that this will only work if the query comes from a callback
|
||||
game button. Otherwise, you may use links like t.me/your_bot?start=XXXX that open
|
||||
your bot with a parameter.
|
||||
cache_time (:obj:`int`, optional): The maximum amount of time in seconds that the
|
||||
cache_time (:obj:`int` | :class:`datetime.timedelta`, optional): The maximum amount of
|
||||
time in seconds that the
|
||||
result of the callback query may be cached client-side. Defaults to 0.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
|
||||
Returns:
|
||||
:obj:`bool` On success, :obj:`True` is returned.
|
||||
|
||||
@@ -4386,7 +4494,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
self,
|
||||
offset: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
timeout: Optional[int] = None, # noqa: ASYNC109
|
||||
timeout: Optional[int] = None,
|
||||
allowed_updates: Optional[Sequence[str]] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -4729,7 +4837,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return ChatFullInfo.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatFullInfo.de_json(result, self)
|
||||
|
||||
async def get_chat_administrators(
|
||||
self,
|
||||
@@ -4842,7 +4950,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return ChatMember.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatMember.de_json(result, self)
|
||||
|
||||
async def set_chat_sticker_set(
|
||||
self,
|
||||
@@ -4937,7 +5045,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return WebhookInfo.de_json(result, self) # type: ignore[return-value]
|
||||
return WebhookInfo.de_json(result, self)
|
||||
|
||||
async def set_game_score(
|
||||
self,
|
||||
@@ -5444,7 +5552,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return SentWebAppMessage.de_json(api_result, self) # type: ignore[return-value]
|
||||
return SentWebAppMessage.de_json(api_result, self)
|
||||
|
||||
async def restrict_chat_member(
|
||||
self,
|
||||
@@ -5858,7 +5966,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatInviteLink.de_json(result, self)
|
||||
|
||||
async def edit_chat_invite_link(
|
||||
self,
|
||||
@@ -5937,7 +6045,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatInviteLink.de_json(result, self)
|
||||
|
||||
async def revoke_chat_invite_link(
|
||||
self,
|
||||
@@ -5984,7 +6092,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatInviteLink.de_json(result, self)
|
||||
|
||||
async def approve_chat_join_request(
|
||||
self,
|
||||
@@ -6456,7 +6564,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return StickerSet.de_json(result, self) # type: ignore[return-value]
|
||||
return StickerSet.de_json(result, self)
|
||||
|
||||
async def get_custom_emoji_stickers(
|
||||
self,
|
||||
@@ -6559,7 +6667,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return File.de_json(result, self) # type: ignore[return-value]
|
||||
return File.de_json(result, self)
|
||||
|
||||
async def add_sticker_to_set(
|
||||
self,
|
||||
@@ -7190,7 +7298,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
explanation: Optional[str] = None,
|
||||
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
open_period: Optional[int] = None,
|
||||
open_period: Optional[TimePeriod] = None,
|
||||
close_date: Optional[Union[int, dtm.datetime]] = None,
|
||||
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -7253,10 +7361,14 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|sequenceargs|
|
||||
open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active
|
||||
open_period (:obj:`int` | :class:`datetime.timedelta`, optional): Amount of time in
|
||||
seconds the poll will be active
|
||||
after creation, :tg-const:`telegram.Poll.MIN_OPEN_PERIOD`-
|
||||
:tg-const:`telegram.Poll.MAX_OPEN_PERIOD`. Can't be used together with
|
||||
:paramref:`close_date`.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
close_date (:obj:`int` | :obj:`datetime.datetime`, optional): Point in time (Unix
|
||||
timestamp) when the poll will be automatically closed. Must be at least
|
||||
:tg-const:`telegram.Poll.MIN_OPEN_PERIOD` and no more than
|
||||
@@ -7416,7 +7528,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return Poll.de_json(result, self) # type: ignore[return-value]
|
||||
return Poll.de_json(result, self)
|
||||
|
||||
async def send_dice(
|
||||
self,
|
||||
@@ -7572,7 +7684,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return ChatAdministratorRights.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatAdministratorRights.de_json(result, self)
|
||||
|
||||
async def set_my_default_administrator_rights(
|
||||
self,
|
||||
@@ -7982,7 +8094,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return MessageId.de_json(result, self) # type: ignore[return-value]
|
||||
return MessageId.de_json(result, self)
|
||||
|
||||
async def copy_messages(
|
||||
self,
|
||||
@@ -8133,7 +8245,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return MenuButton.de_json(result, bot=self) # type: ignore[return-value]
|
||||
return MenuButton.de_json(result, bot=self)
|
||||
|
||||
async def create_invoice_link(
|
||||
self,
|
||||
@@ -8157,7 +8269,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
send_phone_number_to_provider: Optional[bool] = None,
|
||||
send_email_to_provider: Optional[bool] = None,
|
||||
is_flexible: Optional[bool] = None,
|
||||
subscription_period: Optional[Union[int, dtm.timedelta]] = None,
|
||||
subscription_period: Optional[TimePeriod] = None,
|
||||
business_connection_id: Optional[str] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -8278,11 +8390,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
"is_flexible": is_flexible,
|
||||
"send_phone_number_to_provider": send_phone_number_to_provider,
|
||||
"send_email_to_provider": send_email_to_provider,
|
||||
"subscription_period": (
|
||||
subscription_period.total_seconds()
|
||||
if isinstance(subscription_period, dtm.timedelta)
|
||||
else subscription_period
|
||||
),
|
||||
"subscription_period": subscription_period,
|
||||
"business_connection_id": business_connection_id,
|
||||
}
|
||||
|
||||
@@ -8384,7 +8492,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return ForumTopic.de_json(result, self) # type: ignore[return-value]
|
||||
return ForumTopic.de_json(result, self)
|
||||
|
||||
async def edit_forum_topic(
|
||||
self,
|
||||
@@ -8972,7 +9080,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
"""
|
||||
data = {"language_code": language_code}
|
||||
return BotDescription.de_json( # type: ignore[return-value]
|
||||
return BotDescription.de_json(
|
||||
await self._post(
|
||||
"getMyDescription",
|
||||
data,
|
||||
@@ -9011,7 +9119,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
"""
|
||||
data = {"language_code": language_code}
|
||||
return BotShortDescription.de_json( # type: ignore[return-value]
|
||||
return BotShortDescription.de_json(
|
||||
await self._post(
|
||||
"getMyShortDescription",
|
||||
data,
|
||||
@@ -9097,7 +9205,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
"""
|
||||
data = {"language_code": language_code}
|
||||
return BotName.de_json( # type: ignore[return-value]
|
||||
return BotName.de_json(
|
||||
await self._post(
|
||||
"getMyName",
|
||||
data,
|
||||
@@ -9139,7 +9247,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:class:`telegram.error.TelegramError`
|
||||
"""
|
||||
data: JSONDict = {"chat_id": chat_id, "user_id": user_id}
|
||||
return UserChatBoosts.de_json( # type: ignore[return-value]
|
||||
return UserChatBoosts.de_json(
|
||||
await self._post(
|
||||
"getUserChatBoosts",
|
||||
data,
|
||||
@@ -9263,7 +9371,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:class:`telegram.error.TelegramError`
|
||||
"""
|
||||
data: JSONDict = {"business_connection_id": business_connection_id}
|
||||
return BusinessConnection.de_json( # type: ignore[return-value]
|
||||
return BusinessConnection.de_json(
|
||||
await self._post(
|
||||
"getBusinessConnection",
|
||||
data,
|
||||
@@ -9402,7 +9510,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
data: JSONDict = {"offset": offset, "limit": limit}
|
||||
|
||||
return StarTransactions.de_json( # type: ignore[return-value]
|
||||
return StarTransactions.de_json(
|
||||
await self._post(
|
||||
"getStarTransactions",
|
||||
data,
|
||||
@@ -9573,7 +9681,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
async def create_chat_subscription_invite_link(
|
||||
self,
|
||||
chat_id: Union[str, int],
|
||||
subscription_period: int,
|
||||
subscription_period: TimePeriod,
|
||||
subscription_price: int,
|
||||
name: Optional[str] = None,
|
||||
*,
|
||||
@@ -9594,9 +9702,13 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
|
||||
subscription_period (:obj:`int`): The number of seconds the subscription will be
|
||||
subscription_period (:obj:`int` | :class:`datetime.timedelta`): The number of seconds
|
||||
the subscription will be
|
||||
active for before the next payment. Currently, it must always be
|
||||
:tg-const:`telegram.constants.ChatSubscriptionLimit.SUBSCRIPTION_PERIOD` (30 days).
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
subscription_price (:obj:`int`): The number of Telegram Stars a user must pay initially
|
||||
and after each subsequent subscription period to be a member of the chat;
|
||||
:tg-const:`telegram.constants.ChatSubscriptionLimit.MIN_PRICE`-
|
||||
@@ -9628,7 +9740,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatInviteLink.de_json(result, self)
|
||||
|
||||
async def edit_chat_subscription_invite_link(
|
||||
self,
|
||||
@@ -9681,7 +9793,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatInviteLink.de_json(result, self)
|
||||
|
||||
async def get_available_gifts(
|
||||
self,
|
||||
@@ -9703,7 +9815,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
Raises:
|
||||
:class:`telegram.error.TelegramError`
|
||||
"""
|
||||
return Gifts.de_json( # type: ignore[return-value]
|
||||
return Gifts.de_json(
|
||||
await self._post(
|
||||
"getAvailableGifts",
|
||||
read_timeout=read_timeout,
|
||||
|
||||
@@ -84,9 +84,7 @@ class BotCommandScope(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["BotCommandScope"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BotCommandScope":
|
||||
"""Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes
|
||||
care of selecting the correct subclass.
|
||||
|
||||
@@ -104,9 +102,6 @@ class BotCommandScope(TelegramObject):
|
||||
"""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[BotCommandScope]] = {
|
||||
cls.DEFAULT: BotCommandScopeDefault,
|
||||
cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats,
|
||||
|
||||
+12
-37
@@ -27,7 +27,7 @@ from telegram._files.location import Location
|
||||
from telegram._files.sticker import Sticker
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -106,20 +106,15 @@ class BusinessConnection(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["BusinessConnection"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessConnection":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
data["user"] = de_json_optional(data.get("user"), User, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -177,16 +172,11 @@ class BusinessMessagesDeleted(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["BusinessMessagesDeleted"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessMessagesDeleted":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -236,16 +226,11 @@ class BusinessIntro(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["BusinessIntro"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessIntro":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["sticker"] = Sticker.de_json(data.get("sticker"), bot)
|
||||
data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -290,16 +275,11 @@ class BusinessLocation(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["BusinessLocation"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessLocation":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["location"] = Location.de_json(data.get("location"), bot)
|
||||
data["location"] = de_json_optional(data.get("location"), Location, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -439,17 +419,12 @@ class BusinessOpeningHours(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["BusinessOpeningHours"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessOpeningHours":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["opening_hours"] = BusinessOpeningHoursInterval.de_list(
|
||||
data.get("opening_hours"), bot
|
||||
data["opening_hours"] = de_list_optional(
|
||||
data.get("opening_hours"), BusinessOpeningHoursInterval, bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -26,8 +26,9 @@ from telegram._files.location import Location
|
||||
from telegram._message import MaybeInaccessibleMessage, Message
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import JSONDict, ODVInput, ReplyMarkup
|
||||
from telegram._utils.types import JSONDict, ODVInput, ReplyMarkup, TimePeriod
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import (
|
||||
@@ -149,17 +150,12 @@ class CallbackQuery(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["CallbackQuery"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "CallbackQuery":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["message"] = Message.de_json(data.get("message"), bot)
|
||||
data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
|
||||
data["message"] = de_json_optional(data.get("message"), Message, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -168,7 +164,7 @@ class CallbackQuery(TelegramObject):
|
||||
text: Optional[str] = None,
|
||||
show_alert: Optional[bool] = None,
|
||||
url: Optional[str] = None,
|
||||
cache_time: Optional[int] = None,
|
||||
cache_time: Optional[TimePeriod] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -475,7 +471,7 @@ class CallbackQuery(TelegramObject):
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
*,
|
||||
location: Optional[Location] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
|
||||
+16
-9
@@ -31,7 +31,14 @@ from telegram._reaction import ReactionType
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
|
||||
from telegram._utils.types import (
|
||||
CorrectOptionID,
|
||||
FileInput,
|
||||
JSONDict,
|
||||
ODVInput,
|
||||
ReplyMarkup,
|
||||
TimePeriod,
|
||||
)
|
||||
from telegram.helpers import escape_markdown
|
||||
from telegram.helpers import mention_html as helpers_mention_html
|
||||
from telegram.helpers import mention_markdown as helpers_mention_markdown
|
||||
@@ -1339,7 +1346,7 @@ class _ChatBase(TelegramObject):
|
||||
async def send_audio(
|
||||
self,
|
||||
audio: Union[FileInput, "Audio"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
performer: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -1668,7 +1675,7 @@ class _ChatBase(TelegramObject):
|
||||
longitude: Optional[float] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
@@ -1727,7 +1734,7 @@ class _ChatBase(TelegramObject):
|
||||
async def send_animation(
|
||||
self,
|
||||
animation: Union[FileInput, "Animation"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -1915,7 +1922,7 @@ class _ChatBase(TelegramObject):
|
||||
async def send_video(
|
||||
self,
|
||||
video: Union[FileInput, "Video"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -1987,7 +1994,7 @@ class _ChatBase(TelegramObject):
|
||||
async def send_video_note(
|
||||
self,
|
||||
video_note: Union[FileInput, "VideoNote"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
length: Optional[int] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -2045,7 +2052,7 @@ class _ChatBase(TelegramObject):
|
||||
async def send_voice(
|
||||
self,
|
||||
voice: Union[FileInput, "Voice"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -2115,7 +2122,7 @@ class _ChatBase(TelegramObject):
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
explanation: Optional[str] = None,
|
||||
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
open_period: Optional[int] = None,
|
||||
open_period: Optional[TimePeriod] = None,
|
||||
close_date: Optional[Union[int, dtm.datetime]] = None,
|
||||
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -2708,7 +2715,7 @@ class _ChatBase(TelegramObject):
|
||||
|
||||
async def create_subscription_invite_link(
|
||||
self,
|
||||
subscription_period: int,
|
||||
subscription_period: TimePeriod,
|
||||
subscription_price: int,
|
||||
name: Optional[str] = None,
|
||||
*,
|
||||
|
||||
@@ -24,7 +24,7 @@ from telegram import constants
|
||||
from telegram._files.document import Document
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -79,15 +79,10 @@ class BackgroundFill(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["BackgroundFill"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BackgroundFill":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[BackgroundFill]] = {
|
||||
cls.SOLID: BackgroundFillSolid,
|
||||
cls.GRADIENT: BackgroundFillGradient,
|
||||
@@ -270,15 +265,10 @@ class BackgroundType(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["BackgroundType"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BackgroundType":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[BackgroundType]] = {
|
||||
cls.FILL: BackgroundTypeFill,
|
||||
cls.WALLPAPER: BackgroundTypeWallpaper,
|
||||
@@ -290,10 +280,10 @@ class BackgroundType(TelegramObject):
|
||||
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
|
||||
|
||||
if "fill" in data:
|
||||
data["fill"] = BackgroundFill.de_json(data.get("fill"), bot)
|
||||
data["fill"] = de_json_optional(data.get("fill"), BackgroundFill, bot)
|
||||
|
||||
if "document" in data:
|
||||
data["document"] = Document.de_json(data.get("document"), bot)
|
||||
data["document"] = de_json_optional(data.get("document"), Document, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -533,15 +523,10 @@ class ChatBackground(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatBackground"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBackground":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["type"] = BackgroundType.de_json(data.get("type"), bot)
|
||||
data["type"] = de_json_optional(data.get("type"), BackgroundType, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+16
-41
@@ -26,7 +26,7 @@ from telegram._chat import Chat
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -110,15 +110,10 @@ class ChatBoostSource(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatBoostSource"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBoostSource":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[ChatBoostSource]] = {
|
||||
cls.PREMIUM: ChatBoostSourcePremium,
|
||||
cls.GIFT_CODE: ChatBoostSourceGiftCode,
|
||||
@@ -129,7 +124,7 @@ class ChatBoostSource(TelegramObject):
|
||||
return _class_mapping[data.pop("source")].de_json(data=data, bot=bot)
|
||||
|
||||
if "user" in data:
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
data["user"] = de_json_optional(data.get("user"), User, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -290,19 +285,14 @@ class ChatBoost(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatBoost"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBoost":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["source"] = ChatBoostSource.de_json(data.get("source"), bot)
|
||||
data["source"] = de_json_optional(data.get("source"), ChatBoostSource, bot)
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
data["add_date"] = from_timestamp(data["add_date"], tzinfo=loc_tzinfo)
|
||||
data["expiration_date"] = from_timestamp(data["expiration_date"], tzinfo=loc_tzinfo)
|
||||
data["add_date"] = from_timestamp(data.get("add_date"), tzinfo=loc_tzinfo)
|
||||
data["expiration_date"] = from_timestamp(data.get("expiration_date"), tzinfo=loc_tzinfo)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -342,17 +332,12 @@ class ChatBoostUpdated(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatBoostUpdated"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBoostUpdated":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["boost"] = ChatBoost.de_json(data.get("boost"), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["boost"] = de_json_optional(data.get("boost"), ChatBoost, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -401,19 +386,14 @@ class ChatBoostRemoved(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatBoostRemoved"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBoostRemoved":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["source"] = ChatBoostSource.de_json(data.get("source"), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["source"] = de_json_optional(data.get("source"), ChatBoostSource, bot)
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
data["remove_date"] = from_timestamp(data["remove_date"], tzinfo=loc_tzinfo)
|
||||
data["remove_date"] = from_timestamp(data.get("remove_date"), tzinfo=loc_tzinfo)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -450,15 +430,10 @@ class UserChatBoosts(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["UserChatBoosts"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UserChatBoosts":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["boosts"] = ChatBoost.de_list(data.get("boosts"), bot)
|
||||
data["boosts"] = de_list_optional(data.get("boosts"), ChatBoost, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+17
-18
@@ -28,7 +28,7 @@ from telegram._chatlocation import ChatLocation
|
||||
from telegram._chatpermissions import ChatPermissions
|
||||
from telegram._files.chatphoto import ChatPhoto
|
||||
from telegram._reaction import ReactionType
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -512,15 +512,10 @@ class ChatFullInfo(_ChatBase):
|
||||
self.can_send_paid_media: Optional[bool] = can_send_paid_media
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatFullInfo"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
@@ -528,7 +523,7 @@ class ChatFullInfo(_ChatBase):
|
||||
data.get("emoji_status_expiration_date"), tzinfo=loc_tzinfo
|
||||
)
|
||||
|
||||
data["photo"] = ChatPhoto.de_json(data.get("photo"), bot)
|
||||
data["photo"] = de_json_optional(data.get("photo"), ChatPhoto, bot)
|
||||
|
||||
from telegram import ( # pylint: disable=import-outside-toplevel
|
||||
BusinessIntro,
|
||||
@@ -537,16 +532,20 @@ class ChatFullInfo(_ChatBase):
|
||||
Message,
|
||||
)
|
||||
|
||||
data["pinned_message"] = Message.de_json(data.get("pinned_message"), bot)
|
||||
data["permissions"] = ChatPermissions.de_json(data.get("permissions"), bot)
|
||||
data["location"] = ChatLocation.de_json(data.get("location"), bot)
|
||||
data["available_reactions"] = ReactionType.de_list(data.get("available_reactions"), bot)
|
||||
data["birthdate"] = Birthdate.de_json(data.get("birthdate"), bot)
|
||||
data["personal_chat"] = Chat.de_json(data.get("personal_chat"), bot)
|
||||
data["business_intro"] = BusinessIntro.de_json(data.get("business_intro"), bot)
|
||||
data["business_location"] = BusinessLocation.de_json(data.get("business_location"), bot)
|
||||
data["business_opening_hours"] = BusinessOpeningHours.de_json(
|
||||
data.get("business_opening_hours"), bot
|
||||
data["pinned_message"] = de_json_optional(data.get("pinned_message"), Message, bot)
|
||||
data["permissions"] = de_json_optional(data.get("permissions"), ChatPermissions, bot)
|
||||
data["location"] = de_json_optional(data.get("location"), ChatLocation, bot)
|
||||
data["available_reactions"] = de_list_optional(
|
||||
data.get("available_reactions"), ReactionType, bot
|
||||
)
|
||||
data["birthdate"] = de_json_optional(data.get("birthdate"), Birthdate, bot)
|
||||
data["personal_chat"] = de_json_optional(data.get("personal_chat"), Chat, bot)
|
||||
data["business_intro"] = de_json_optional(data.get("business_intro"), BusinessIntro, bot)
|
||||
data["business_location"] = de_json_optional(
|
||||
data.get("business_location"), BusinessLocation, bot
|
||||
)
|
||||
data["business_opening_hours"] = de_json_optional(
|
||||
data.get("business_opening_hours"), BusinessOpeningHours, bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -177,19 +178,14 @@ class ChatInviteLink(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatInviteLink"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatInviteLink":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["creator"] = User.de_json(data.get("creator"), bot)
|
||||
data["creator"] = de_json_optional(data.get("creator"), User, bot)
|
||||
data["expire_date"] = from_timestamp(data.get("expire_date", None), tzinfo=loc_tzinfo)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -24,6 +24,7 @@ from telegram._chat import Chat
|
||||
from telegram._chatinvitelink import ChatInviteLink
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
@@ -129,22 +130,17 @@ class ChatJoinRequest(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatJoinRequest"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatJoinRequest":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
|
||||
data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
|
||||
data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot)
|
||||
data["invite_link"] = de_json_optional(data.get("invite_link"), ChatInviteLink, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Final, Optional
|
||||
from telegram import constants
|
||||
from telegram._files.location import Location
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -68,16 +69,11 @@ class ChatLocation(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatLocation"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatLocation":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["location"] = Location.de_json(data.get("location"), bot)
|
||||
data["location"] = de_json_optional(data.get("location"), Location, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Final, Optional
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -105,15 +106,10 @@ class ChatMember(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatMember"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatMember":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[ChatMember]] = {
|
||||
cls.OWNER: ChatMemberOwner,
|
||||
cls.ADMINISTRATOR: ChatMemberAdministrator,
|
||||
@@ -126,12 +122,12 @@ class ChatMember(TelegramObject):
|
||||
if cls is ChatMember and data.get("status") in _class_mapping:
|
||||
return _class_mapping[data.pop("status")].de_json(data=data, bot=bot)
|
||||
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
data["user"] = de_json_optional(data.get("user"), User, bot)
|
||||
if "until_date" in data:
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["until_date"] = from_timestamp(data["until_date"], tzinfo=loc_tzinfo)
|
||||
data["until_date"] = from_timestamp(data.get("until_date"), tzinfo=loc_tzinfo)
|
||||
|
||||
# This is a deprecated field that TG still returns for backwards compatibility
|
||||
# Let's filter it out to speed up the de-json process
|
||||
|
||||
@@ -25,6 +25,7 @@ from telegram._chatinvitelink import ChatInviteLink
|
||||
from telegram._chatmember import ChatMember
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -141,24 +142,19 @@ class ChatMemberUpdated(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatMemberUpdated"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatMemberUpdated":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
|
||||
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
|
||||
data["old_chat_member"] = ChatMember.de_json(data.get("old_chat_member"), bot)
|
||||
data["new_chat_member"] = ChatMember.de_json(data.get("new_chat_member"), bot)
|
||||
data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot)
|
||||
data["old_chat_member"] = de_json_optional(data.get("old_chat_member"), ChatMember, bot)
|
||||
data["new_chat_member"] = de_json_optional(data.get("new_chat_member"), ChatMember, bot)
|
||||
data["invite_link"] = de_json_optional(data.get("invite_link"), ChatInviteLink, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -231,15 +231,10 @@ class ChatPermissions(TelegramObject):
|
||||
return cls(*(14 * (False,)))
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatPermissions"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatPermissions":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
api_kwargs = {}
|
||||
# This is a deprecated field that TG still returns for backwards compatibility
|
||||
# Let's filter it out to speed up the de-json process
|
||||
|
||||
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._files.location import Location
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -92,18 +93,13 @@ class ChosenInlineResult(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChosenInlineResult"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChosenInlineResult":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Required
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
|
||||
# Optionals
|
||||
data["location"] = Location.de_json(data.get("location"), bot)
|
||||
data["location"] = de_json_optional(data.get("location"), Location, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Optional, TypeVar
|
||||
|
||||
from telegram._files._basemedium import _BaseMedium
|
||||
from telegram._files.photosize import PhotoSize
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -82,17 +83,14 @@ class _BaseThumbedMedium(_BaseMedium):
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls: type[ThumbedMT_co], data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional[ThumbedMT_co]:
|
||||
cls: type[ThumbedMT_co], data: JSONDict, bot: Optional["Bot"] = None
|
||||
) -> ThumbedMT_co:
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# In case this wasn't already done by the subclass
|
||||
if not isinstance(data.get("thumbnail"), PhotoSize):
|
||||
data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot)
|
||||
data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot)
|
||||
|
||||
api_kwargs = {}
|
||||
# This is a deprecated field that TG still returns for backwards compatibility
|
||||
|
||||
@@ -26,7 +26,7 @@ from telegram._files.file import File
|
||||
from telegram._files.photosize import PhotoSize
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -194,16 +194,13 @@ class Sticker(_BaseThumbedMedium):
|
||||
""":const:`telegram.constants.StickerType.CUSTOM_EMOJI`"""
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Sticker"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Sticker":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot)
|
||||
data["mask_position"] = MaskPosition.de_json(data.get("mask_position"), bot)
|
||||
data["premium_animation"] = File.de_json(data.get("premium_animation"), bot)
|
||||
data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot)
|
||||
data["mask_position"] = de_json_optional(data.get("mask_position"), MaskPosition, bot)
|
||||
data["premium_animation"] = de_json_optional(data.get("premium_animation"), File, bot)
|
||||
|
||||
api_kwargs = {}
|
||||
# This is a deprecated field that TG still returns for backwards compatibility
|
||||
@@ -306,15 +303,12 @@ class StickerSet(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["StickerSet"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "StickerSet":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
if not data:
|
||||
return None
|
||||
data = cls._parse_data(data)
|
||||
|
||||
data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot)
|
||||
data["stickers"] = Sticker.de_list(data.get("stickers"), bot)
|
||||
data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot)
|
||||
data["stickers"] = de_list_optional(data.get("stickers"), Sticker, bot)
|
||||
|
||||
api_kwargs = {}
|
||||
# These are deprecated fields that TG still returns for backwards compatibility
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._files.location import Location
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -103,13 +104,10 @@ class Venue(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Venue"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Venue":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["location"] = Location.de_json(data.get("location"), bot)
|
||||
data["location"] = de_json_optional(data.get("location"), Location, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -24,7 +24,7 @@ from telegram._files.animation import Animation
|
||||
from telegram._files.photosize import PhotoSize
|
||||
from telegram._messageentity import MessageEntity
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.strings import TextEncoding
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -124,16 +124,13 @@ class Game(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Game"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Game":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["photo"] = PhotoSize.de_list(data.get("photo"), bot)
|
||||
data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)
|
||||
data["animation"] = Animation.de_json(data.get("animation"), bot)
|
||||
data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
|
||||
data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot)
|
||||
data["animation"] = de_json_optional(data.get("animation"), Animation, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -61,15 +62,10 @@ class GameHighScore(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["GameHighScore"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "GameHighScore":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
data["user"] = de_json_optional(data.get("user"), User, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+5
-11
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._files.sticker import Sticker
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -99,14 +99,11 @@ class Gift(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Gift"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Gift":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["sticker"] = Sticker.de_json(data.get("sticker"), bot)
|
||||
data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -142,12 +139,9 @@ class Gifts(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Gifts"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Gifts":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["gifts"] = Gift.de_list(data.get("gifts"), bot)
|
||||
data["gifts"] = de_list_optional(data.get("gifts"), Gift, bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+8
-23
@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._chat import Chat
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -137,19 +137,14 @@ class Giveaway(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["Giveaway"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Giveaway":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["chats"] = tuple(Chat.de_list(data.get("chats"), bot))
|
||||
data["chats"] = de_list_optional(data.get("chats"), Chat, bot)
|
||||
data["winners_selection_date"] = from_timestamp(
|
||||
data.get("winners_selection_date"), tzinfo=loc_tzinfo
|
||||
)
|
||||
@@ -299,20 +294,15 @@ class GiveawayWinners(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["GiveawayWinners"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "GiveawayWinners":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["winners"] = tuple(User.de_list(data.get("winners"), bot))
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["winners"] = de_list_optional(data.get("winners"), User, bot)
|
||||
data["winners_selection_date"] = from_timestamp(
|
||||
data.get("winners_selection_date"), tzinfo=loc_tzinfo
|
||||
)
|
||||
@@ -376,18 +366,13 @@ class GiveawayCompleted(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["GiveawayCompleted"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "GiveawayCompleted":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
# Unfortunately, this needs to be here due to cyclic imports
|
||||
from telegram._message import Message # pylint: disable=import-outside-toplevel
|
||||
|
||||
data["giveaway_message"] = Message.de_json(data.get("giveaway_message"), bot)
|
||||
data["giveaway_message"] = de_json_optional(data.get("giveaway_message"), Message, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -26,6 +26,7 @@ from telegram._games.callbackgame import CallbackGame
|
||||
from telegram._loginurl import LoginUrl
|
||||
from telegram._switchinlinequerychosenchat import SwitchInlineQueryChosenChat
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._webappinfo import WebAppInfo
|
||||
|
||||
@@ -296,22 +297,17 @@ class InlineKeyboardButton(TelegramObject):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["InlineKeyboardButton"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InlineKeyboardButton":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["login_url"] = LoginUrl.de_json(data.get("login_url"), bot)
|
||||
data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot)
|
||||
data["callback_game"] = CallbackGame.de_json(data.get("callback_game"), bot)
|
||||
data["switch_inline_query_chosen_chat"] = SwitchInlineQueryChosenChat.de_json(
|
||||
data.get("switch_inline_query_chosen_chat"), bot
|
||||
data["login_url"] = de_json_optional(data.get("login_url"), LoginUrl, bot)
|
||||
data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot)
|
||||
data["callback_game"] = de_json_optional(data.get("callback_game"), CallbackGame, bot)
|
||||
data["switch_inline_query_chosen_chat"] = de_json_optional(
|
||||
data.get("switch_inline_query_chosen_chat"), SwitchInlineQueryChosenChat, bot
|
||||
)
|
||||
data["copy_text"] = CopyTextButton.de_json(data.get("copy_text"), bot)
|
||||
data["copy_text"] = de_json_optional(data.get("copy_text"), CopyTextButton, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -91,12 +91,8 @@ class InlineKeyboardMarkup(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["InlineKeyboardMarkup"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InlineKeyboardMarkup":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
keyboard = []
|
||||
for row in data["inline_keyboard"]:
|
||||
|
||||
@@ -27,8 +27,9 @@ from telegram._files.location import Location
|
||||
from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
from telegram._utils.types import JSONDict, ODVInput, TimePeriod
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot, InlineQueryResult
|
||||
@@ -126,17 +127,12 @@ class InlineQuery(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["InlineQuery"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InlineQuery":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["location"] = Location.de_json(data.get("location"), bot)
|
||||
data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
|
||||
data["location"] = de_json_optional(data.get("location"), Location, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -145,7 +141,7 @@ class InlineQuery(TelegramObject):
|
||||
results: Union[
|
||||
Sequence["InlineQueryResult"], Callable[[int], Optional[Sequence["InlineQueryResult"]]]
|
||||
],
|
||||
cache_time: Optional[int] = None,
|
||||
cache_time: Optional[TimePeriod] = None,
|
||||
is_personal: Optional[bool] = None,
|
||||
next_offset: Optional[str] = None,
|
||||
button: Optional[InlineQueryResultsButton] = None,
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Final, Optional
|
||||
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._webappinfo import WebAppInfo
|
||||
|
||||
@@ -97,14 +98,10 @@ class InlineQueryResultsButton(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["InlineQueryResultsButton"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InlineQueryResultsButton":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot)
|
||||
data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._inline.inputmessagecontent import InputMessageContent
|
||||
from telegram._payment.labeledprice import LabeledPrice
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -254,15 +254,10 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["InputInvoiceMessageContent"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InputInvoiceMessageContent":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["prices"] = LabeledPrice.de_list(data.get("prices"), bot)
|
||||
data["prices"] = de_list_optional(data.get("prices"), LabeledPrice, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -67,15 +67,10 @@ class PreparedInlineMessage(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PreparedInlineMessage"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PreparedInlineMessage":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
data["expiration_date"] = from_timestamp(data.get("expiration_date"), tzinfo=loc_tzinfo)
|
||||
|
||||
+12
-10
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._keyboardbuttonpolltype import KeyboardButtonPollType
|
||||
from telegram._keyboardbuttonrequest import KeyboardButtonRequestChat, KeyboardButtonRequestUsers
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._webappinfo import WebAppInfo
|
||||
|
||||
@@ -168,19 +169,20 @@ class KeyboardButton(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["KeyboardButton"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "KeyboardButton":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["request_poll"] = KeyboardButtonPollType.de_json(data.get("request_poll"), bot)
|
||||
data["request_users"] = KeyboardButtonRequestUsers.de_json(data.get("request_users"), bot)
|
||||
data["request_chat"] = KeyboardButtonRequestChat.de_json(data.get("request_chat"), bot)
|
||||
data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot)
|
||||
data["request_poll"] = de_json_optional(
|
||||
data.get("request_poll"), KeyboardButtonPollType, bot
|
||||
)
|
||||
data["request_users"] = de_json_optional(
|
||||
data.get("request_users"), KeyboardButtonRequestUsers, bot
|
||||
)
|
||||
data["request_chat"] = de_json_optional(
|
||||
data.get("request_chat"), KeyboardButtonRequestChat, bot
|
||||
)
|
||||
data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot)
|
||||
|
||||
api_kwargs = {}
|
||||
# This is a deprecated field that TG still returns for backwards compatibility
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._chatadministratorrights import ChatAdministratorRights
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -257,20 +258,15 @@ class KeyboardButtonRequestChat(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["KeyboardButtonRequestChat"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "KeyboardButtonRequestChat":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["user_administrator_rights"] = ChatAdministratorRights.de_json(
|
||||
data.get("user_administrator_rights"), bot
|
||||
data["user_administrator_rights"] = de_json_optional(
|
||||
data.get("user_administrator_rights"), ChatAdministratorRights, bot
|
||||
)
|
||||
data["bot_administrator_rights"] = ChatAdministratorRights.de_json(
|
||||
data.get("bot_administrator_rights"), bot
|
||||
data["bot_administrator_rights"] = de_json_optional(
|
||||
data.get("bot_administrator_rights"), ChatAdministratorRights, bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+4
-16
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Final, Optional
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._webappinfo import WebAppInfo
|
||||
|
||||
@@ -69,9 +70,7 @@ class MenuButton(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["MenuButton"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "MenuButton":
|
||||
"""Converts JSON data to the appropriate :class:`MenuButton` object, i.e. takes
|
||||
care of selecting the correct subclass.
|
||||
|
||||
@@ -89,12 +88,6 @@ class MenuButton(TelegramObject):
|
||||
"""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if not data and cls is MenuButton:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[MenuButton]] = {
|
||||
cls.COMMANDS: MenuButtonCommands,
|
||||
cls.WEB_APP: MenuButtonWebApp,
|
||||
@@ -172,16 +165,11 @@ class MenuButtonWebApp(MenuButton):
|
||||
self._id_attrs = (self.type, self.text, self.web_app)
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["MenuButtonWebApp"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "MenuButtonWebApp":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot)
|
||||
data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
|
||||
+110
-87
@@ -65,7 +65,7 @@ from telegram._shared import ChatShared, UsersShared
|
||||
from telegram._story import Story
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.entities import parse_message_entities, parse_message_entity
|
||||
@@ -77,6 +77,7 @@ from telegram._utils.types import (
|
||||
MarkdownVersion,
|
||||
ODVInput,
|
||||
ReplyMarkup,
|
||||
TimePeriod,
|
||||
)
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram._videochat import (
|
||||
@@ -191,9 +192,6 @@ class MaybeInaccessibleMessage(TelegramObject):
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
if cls is MaybeInaccessibleMessage:
|
||||
if data["date"] == 0:
|
||||
return InaccessibleMessage.de_json(data=data, bot=bot)
|
||||
@@ -206,9 +204,9 @@ class MaybeInaccessibleMessage(TelegramObject):
|
||||
if data["date"] == 0:
|
||||
data["date"] = ZERO_DATE
|
||||
else:
|
||||
data["date"] = from_timestamp(data["date"], tzinfo=loc_tzinfo)
|
||||
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs)
|
||||
|
||||
|
||||
@@ -1251,83 +1249,100 @@ class Message(MaybeInaccessibleMessage):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Message"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["sender_chat"] = Chat.de_json(data.get("sender_chat"), bot)
|
||||
data["entities"] = MessageEntity.de_list(data.get("entities"), bot)
|
||||
data["caption_entities"] = MessageEntity.de_list(data.get("caption_entities"), bot)
|
||||
data["reply_to_message"] = Message.de_json(data.get("reply_to_message"), bot)
|
||||
data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
|
||||
data["sender_chat"] = de_json_optional(data.get("sender_chat"), Chat, bot)
|
||||
data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot)
|
||||
data["caption_entities"] = de_list_optional(
|
||||
data.get("caption_entities"), MessageEntity, bot
|
||||
)
|
||||
data["reply_to_message"] = de_json_optional(data.get("reply_to_message"), Message, bot)
|
||||
data["edit_date"] = from_timestamp(data.get("edit_date"), tzinfo=loc_tzinfo)
|
||||
data["audio"] = Audio.de_json(data.get("audio"), bot)
|
||||
data["document"] = Document.de_json(data.get("document"), bot)
|
||||
data["animation"] = Animation.de_json(data.get("animation"), bot)
|
||||
data["game"] = Game.de_json(data.get("game"), bot)
|
||||
data["photo"] = PhotoSize.de_list(data.get("photo"), bot)
|
||||
data["sticker"] = Sticker.de_json(data.get("sticker"), bot)
|
||||
data["story"] = Story.de_json(data.get("story"), bot)
|
||||
data["video"] = Video.de_json(data.get("video"), bot)
|
||||
data["voice"] = Voice.de_json(data.get("voice"), bot)
|
||||
data["video_note"] = VideoNote.de_json(data.get("video_note"), bot)
|
||||
data["contact"] = Contact.de_json(data.get("contact"), bot)
|
||||
data["location"] = Location.de_json(data.get("location"), bot)
|
||||
data["venue"] = Venue.de_json(data.get("venue"), bot)
|
||||
data["new_chat_members"] = User.de_list(data.get("new_chat_members"), bot)
|
||||
data["left_chat_member"] = User.de_json(data.get("left_chat_member"), bot)
|
||||
data["new_chat_photo"] = PhotoSize.de_list(data.get("new_chat_photo"), bot)
|
||||
data["message_auto_delete_timer_changed"] = MessageAutoDeleteTimerChanged.de_json(
|
||||
data.get("message_auto_delete_timer_changed"), bot
|
||||
data["audio"] = de_json_optional(data.get("audio"), Audio, bot)
|
||||
data["document"] = de_json_optional(data.get("document"), Document, bot)
|
||||
data["animation"] = de_json_optional(data.get("animation"), Animation, bot)
|
||||
data["game"] = de_json_optional(data.get("game"), Game, bot)
|
||||
data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
|
||||
data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot)
|
||||
data["story"] = de_json_optional(data.get("story"), Story, bot)
|
||||
data["video"] = de_json_optional(data.get("video"), Video, bot)
|
||||
data["voice"] = de_json_optional(data.get("voice"), Voice, bot)
|
||||
data["video_note"] = de_json_optional(data.get("video_note"), VideoNote, bot)
|
||||
data["contact"] = de_json_optional(data.get("contact"), Contact, bot)
|
||||
data["location"] = de_json_optional(data.get("location"), Location, bot)
|
||||
data["venue"] = de_json_optional(data.get("venue"), Venue, bot)
|
||||
data["new_chat_members"] = de_list_optional(data.get("new_chat_members"), User, bot)
|
||||
data["left_chat_member"] = de_json_optional(data.get("left_chat_member"), User, bot)
|
||||
data["new_chat_photo"] = de_list_optional(data.get("new_chat_photo"), PhotoSize, bot)
|
||||
data["message_auto_delete_timer_changed"] = de_json_optional(
|
||||
data.get("message_auto_delete_timer_changed"), MessageAutoDeleteTimerChanged, bot
|
||||
)
|
||||
data["pinned_message"] = MaybeInaccessibleMessage.de_json(data.get("pinned_message"), bot)
|
||||
data["invoice"] = Invoice.de_json(data.get("invoice"), bot)
|
||||
data["successful_payment"] = SuccessfulPayment.de_json(data.get("successful_payment"), bot)
|
||||
data["passport_data"] = PassportData.de_json(data.get("passport_data"), bot)
|
||||
data["poll"] = Poll.de_json(data.get("poll"), bot)
|
||||
data["dice"] = Dice.de_json(data.get("dice"), bot)
|
||||
data["via_bot"] = User.de_json(data.get("via_bot"), bot)
|
||||
data["proximity_alert_triggered"] = ProximityAlertTriggered.de_json(
|
||||
data.get("proximity_alert_triggered"), bot
|
||||
data["pinned_message"] = de_json_optional(
|
||||
data.get("pinned_message"), MaybeInaccessibleMessage, bot
|
||||
)
|
||||
data["reply_markup"] = InlineKeyboardMarkup.de_json(data.get("reply_markup"), bot)
|
||||
data["video_chat_scheduled"] = VideoChatScheduled.de_json(
|
||||
data.get("video_chat_scheduled"), bot
|
||||
data["invoice"] = de_json_optional(data.get("invoice"), Invoice, bot)
|
||||
data["successful_payment"] = de_json_optional(
|
||||
data.get("successful_payment"), SuccessfulPayment, bot
|
||||
)
|
||||
data["video_chat_started"] = VideoChatStarted.de_json(data.get("video_chat_started"), bot)
|
||||
data["video_chat_ended"] = VideoChatEnded.de_json(data.get("video_chat_ended"), bot)
|
||||
data["video_chat_participants_invited"] = VideoChatParticipantsInvited.de_json(
|
||||
data.get("video_chat_participants_invited"), bot
|
||||
data["passport_data"] = de_json_optional(data.get("passport_data"), PassportData, bot)
|
||||
data["poll"] = de_json_optional(data.get("poll"), Poll, bot)
|
||||
data["dice"] = de_json_optional(data.get("dice"), Dice, bot)
|
||||
data["via_bot"] = de_json_optional(data.get("via_bot"), User, bot)
|
||||
data["proximity_alert_triggered"] = de_json_optional(
|
||||
data.get("proximity_alert_triggered"), ProximityAlertTriggered, bot
|
||||
)
|
||||
data["web_app_data"] = WebAppData.de_json(data.get("web_app_data"), bot)
|
||||
data["forum_topic_closed"] = ForumTopicClosed.de_json(data.get("forum_topic_closed"), bot)
|
||||
data["forum_topic_created"] = ForumTopicCreated.de_json(
|
||||
data.get("forum_topic_created"), bot
|
||||
data["reply_markup"] = de_json_optional(
|
||||
data.get("reply_markup"), InlineKeyboardMarkup, bot
|
||||
)
|
||||
data["forum_topic_reopened"] = ForumTopicReopened.de_json(
|
||||
data.get("forum_topic_reopened"), bot
|
||||
data["video_chat_scheduled"] = de_json_optional(
|
||||
data.get("video_chat_scheduled"), VideoChatScheduled, bot
|
||||
)
|
||||
data["forum_topic_edited"] = ForumTopicEdited.de_json(data.get("forum_topic_edited"), bot)
|
||||
data["general_forum_topic_hidden"] = GeneralForumTopicHidden.de_json(
|
||||
data.get("general_forum_topic_hidden"), bot
|
||||
data["video_chat_started"] = de_json_optional(
|
||||
data.get("video_chat_started"), VideoChatStarted, bot
|
||||
)
|
||||
data["general_forum_topic_unhidden"] = GeneralForumTopicUnhidden.de_json(
|
||||
data.get("general_forum_topic_unhidden"), bot
|
||||
data["video_chat_ended"] = de_json_optional(
|
||||
data.get("video_chat_ended"), VideoChatEnded, bot
|
||||
)
|
||||
data["write_access_allowed"] = WriteAccessAllowed.de_json(
|
||||
data.get("write_access_allowed"), bot
|
||||
data["video_chat_participants_invited"] = de_json_optional(
|
||||
data.get("video_chat_participants_invited"), VideoChatParticipantsInvited, bot
|
||||
)
|
||||
data["web_app_data"] = de_json_optional(data.get("web_app_data"), WebAppData, bot)
|
||||
data["forum_topic_closed"] = de_json_optional(
|
||||
data.get("forum_topic_closed"), ForumTopicClosed, bot
|
||||
)
|
||||
data["forum_topic_created"] = de_json_optional(
|
||||
data.get("forum_topic_created"), ForumTopicCreated, bot
|
||||
)
|
||||
data["forum_topic_reopened"] = de_json_optional(
|
||||
data.get("forum_topic_reopened"), ForumTopicReopened, bot
|
||||
)
|
||||
data["forum_topic_edited"] = de_json_optional(
|
||||
data.get("forum_topic_edited"), ForumTopicEdited, bot
|
||||
)
|
||||
data["general_forum_topic_hidden"] = de_json_optional(
|
||||
data.get("general_forum_topic_hidden"), GeneralForumTopicHidden, bot
|
||||
)
|
||||
data["general_forum_topic_unhidden"] = de_json_optional(
|
||||
data.get("general_forum_topic_unhidden"), GeneralForumTopicUnhidden, bot
|
||||
)
|
||||
data["write_access_allowed"] = de_json_optional(
|
||||
data.get("write_access_allowed"), WriteAccessAllowed, bot
|
||||
)
|
||||
data["users_shared"] = de_json_optional(data.get("users_shared"), UsersShared, bot)
|
||||
data["chat_shared"] = de_json_optional(data.get("chat_shared"), ChatShared, bot)
|
||||
data["chat_background_set"] = de_json_optional(
|
||||
data.get("chat_background_set"), ChatBackground, bot
|
||||
)
|
||||
data["paid_media"] = de_json_optional(data.get("paid_media"), PaidMediaInfo, bot)
|
||||
data["refunded_payment"] = de_json_optional(
|
||||
data.get("refunded_payment"), RefundedPayment, bot
|
||||
)
|
||||
data["users_shared"] = UsersShared.de_json(data.get("users_shared"), bot)
|
||||
data["chat_shared"] = ChatShared.de_json(data.get("chat_shared"), bot)
|
||||
data["chat_background_set"] = ChatBackground.de_json(data.get("chat_background_set"), bot)
|
||||
data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot)
|
||||
data["refunded_payment"] = RefundedPayment.de_json(data.get("refunded_payment"), bot)
|
||||
|
||||
# Unfortunately, this needs to be here due to cyclic imports
|
||||
from telegram._giveaway import ( # pylint: disable=import-outside-toplevel
|
||||
@@ -1344,19 +1359,27 @@ class Message(MaybeInaccessibleMessage):
|
||||
TextQuote,
|
||||
)
|
||||
|
||||
data["giveaway"] = Giveaway.de_json(data.get("giveaway"), bot)
|
||||
data["giveaway_completed"] = GiveawayCompleted.de_json(data.get("giveaway_completed"), bot)
|
||||
data["giveaway_created"] = GiveawayCreated.de_json(data.get("giveaway_created"), bot)
|
||||
data["giveaway_winners"] = GiveawayWinners.de_json(data.get("giveaway_winners"), bot)
|
||||
data["link_preview_options"] = LinkPreviewOptions.de_json(
|
||||
data.get("link_preview_options"), bot
|
||||
data["giveaway"] = de_json_optional(data.get("giveaway"), Giveaway, bot)
|
||||
data["giveaway_completed"] = de_json_optional(
|
||||
data.get("giveaway_completed"), GiveawayCompleted, bot
|
||||
)
|
||||
data["external_reply"] = ExternalReplyInfo.de_json(data.get("external_reply"), bot)
|
||||
data["quote"] = TextQuote.de_json(data.get("quote"), bot)
|
||||
data["forward_origin"] = MessageOrigin.de_json(data.get("forward_origin"), bot)
|
||||
data["reply_to_story"] = Story.de_json(data.get("reply_to_story"), bot)
|
||||
data["boost_added"] = ChatBoostAdded.de_json(data.get("boost_added"), bot)
|
||||
data["sender_business_bot"] = User.de_json(data.get("sender_business_bot"), bot)
|
||||
data["giveaway_created"] = de_json_optional(
|
||||
data.get("giveaway_created"), GiveawayCreated, bot
|
||||
)
|
||||
data["giveaway_winners"] = de_json_optional(
|
||||
data.get("giveaway_winners"), GiveawayWinners, bot
|
||||
)
|
||||
data["link_preview_options"] = de_json_optional(
|
||||
data.get("link_preview_options"), LinkPreviewOptions, bot
|
||||
)
|
||||
data["external_reply"] = de_json_optional(
|
||||
data.get("external_reply"), ExternalReplyInfo, bot
|
||||
)
|
||||
data["quote"] = de_json_optional(data.get("quote"), TextQuote, bot)
|
||||
data["forward_origin"] = de_json_optional(data.get("forward_origin"), MessageOrigin, bot)
|
||||
data["reply_to_story"] = de_json_optional(data.get("reply_to_story"), Story, bot)
|
||||
data["boost_added"] = de_json_optional(data.get("boost_added"), ChatBoostAdded, bot)
|
||||
data["sender_business_bot"] = de_json_optional(data.get("sender_business_bot"), User, bot)
|
||||
|
||||
api_kwargs = {}
|
||||
# This is a deprecated field that TG still returns for backwards compatibility
|
||||
@@ -2210,7 +2233,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
async def reply_audio(
|
||||
self,
|
||||
audio: Union[FileInput, "Audio"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
performer: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -2384,7 +2407,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
async def reply_animation(
|
||||
self,
|
||||
animation: Union[FileInput, "Animation"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -2552,7 +2575,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
async def reply_video(
|
||||
self,
|
||||
video: Union[FileInput, "Video"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -2647,7 +2670,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
async def reply_video_note(
|
||||
self,
|
||||
video_note: Union[FileInput, "VideoNote"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
length: Optional[int] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -2728,7 +2751,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
async def reply_voice(
|
||||
self,
|
||||
voice: Union[FileInput, "Voice"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -2814,7 +2837,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
longitude: Optional[float] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
@@ -3076,7 +3099,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
explanation: Optional[str] = None,
|
||||
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
open_period: Optional[int] = None,
|
||||
open_period: Optional[TimePeriod] = None,
|
||||
close_date: Optional[Union[int, dtm.datetime]] = None,
|
||||
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -3958,7 +3981,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
*,
|
||||
location: Optional[Location] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
|
||||
@@ -27,6 +27,7 @@ from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.strings import TextEncoding
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -137,16 +138,11 @@ class MessageEntity(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["MessageEntity"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "MessageEntity":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
data["user"] = de_json_optional(data.get("user"), User, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from telegram._chat import Chat
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -94,17 +95,12 @@ class MessageOrigin(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["MessageOrigin"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "MessageOrigin":
|
||||
"""Converts JSON data to the appropriate :class:`MessageOrigin` object, i.e. takes
|
||||
care of selecting the correct subclass.
|
||||
"""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[MessageOrigin]] = {
|
||||
cls.USER: MessageOriginUser,
|
||||
cls.HIDDEN_USER: MessageOriginHiddenUser,
|
||||
@@ -118,13 +114,13 @@ class MessageOrigin(TelegramObject):
|
||||
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
|
||||
|
||||
if "sender_user" in data:
|
||||
data["sender_user"] = User.de_json(data.get("sender_user"), bot)
|
||||
data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot)
|
||||
|
||||
if "sender_chat" in data:
|
||||
data["sender_chat"] = Chat.de_json(data.get("sender_chat"), bot)
|
||||
data["sender_chat"] = de_json_optional(data.get("sender_chat"), Chat, bot)
|
||||
|
||||
if "chat" in data:
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from telegram._chat import Chat
|
||||
from telegram._reaction import ReactionCount, ReactionType
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -86,21 +86,16 @@ class MessageReactionCountUpdated(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["MessageReactionCountUpdated"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "MessageReactionCountUpdated":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["reactions"] = ReactionCount.de_list(data.get("reactions"), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["reactions"] = de_list_optional(data.get("reactions"), ReactionCount, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -187,23 +182,18 @@ class MessageReactionUpdated(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["MessageReactionUpdated"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "MessageReactionUpdated":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["old_reaction"] = ReactionType.de_list(data.get("old_reaction"), bot)
|
||||
data["new_reaction"] = ReactionType.de_list(data.get("new_reaction"), bot)
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
data["actor_chat"] = Chat.de_json(data.get("actor_chat"), bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["old_reaction"] = de_list_optional(data.get("old_reaction"), ReactionType, bot)
|
||||
data["new_reaction"] = de_list_optional(data.get("new_reaction"), ReactionType, bot)
|
||||
data["user"] = de_json_optional(data.get("user"), User, bot)
|
||||
data["actor_chat"] = de_json_optional(data.get("actor_chat"), Chat, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+9
-37
@@ -27,7 +27,7 @@ from telegram._files.video import Video
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -75,9 +75,7 @@ class PaidMedia(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PaidMedia"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PaidMedia":
|
||||
"""Converts JSON data to the appropriate :class:`PaidMedia` object, i.e. takes
|
||||
care of selecting the correct subclass.
|
||||
|
||||
@@ -91,12 +89,6 @@ class PaidMedia(TelegramObject):
|
||||
"""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if not data and cls is PaidMedia:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[PaidMedia]] = {
|
||||
cls.PREVIEW: PaidMediaPreview,
|
||||
cls.PHOTO: PaidMediaPhoto,
|
||||
@@ -185,15 +177,10 @@ class PaidMediaPhoto(PaidMedia):
|
||||
self._id_attrs = (self.type, self.photo)
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PaidMediaPhoto"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PaidMediaPhoto":
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["photo"] = PhotoSize.de_list(data.get("photo"), bot=bot)
|
||||
data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
|
||||
@@ -231,15 +218,10 @@ class PaidMediaVideo(PaidMedia):
|
||||
self._id_attrs = (self.type, self.video)
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PaidMediaVideo"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PaidMediaVideo":
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["video"] = Video.de_json(data.get("video"), bot=bot)
|
||||
data["video"] = de_json_optional(data.get("video"), Video, bot)
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
|
||||
@@ -280,15 +262,10 @@ class PaidMediaInfo(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PaidMediaInfo"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PaidMediaInfo":
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot)
|
||||
data["paid_media"] = de_list_optional(data.get("paid_media"), PaidMedia, bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -329,13 +306,8 @@ class PaidMediaPurchased(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PaidMediaPurchased"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PaidMediaPurchased":
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["from_user"] = User.de_json(data=data.pop("from"), bot=bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -39,7 +39,7 @@ except ImportError:
|
||||
CRYPTO_INSTALLED = False
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.strings import TextEncoding
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.error import PassportDecryptionError
|
||||
@@ -207,7 +207,7 @@ class EncryptedCredentials(TelegramObject):
|
||||
decrypt_json(self.decrypted_secret, b64decode(self.hash), b64decode(self.data)),
|
||||
self.get_bot(),
|
||||
)
|
||||
return self._decrypted_data # type: ignore[return-value]
|
||||
return self._decrypted_data
|
||||
|
||||
|
||||
class Credentials(TelegramObject):
|
||||
@@ -234,16 +234,11 @@ class Credentials(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["Credentials"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Credentials":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["secure_data"] = SecureData.de_json(data.get("secure_data"), bot=bot)
|
||||
data["secure_data"] = de_json_optional(data.get("secure_data"), SecureData, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -346,30 +341,27 @@ class SecureData(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["SecureData"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SecureData":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["temporary_registration"] = SecureValue.de_json(
|
||||
data.get("temporary_registration"), bot=bot
|
||||
data["temporary_registration"] = de_json_optional(
|
||||
data.get("temporary_registration"), SecureValue, bot
|
||||
)
|
||||
data["passport_registration"] = SecureValue.de_json(
|
||||
data.get("passport_registration"), bot=bot
|
||||
data["passport_registration"] = de_json_optional(
|
||||
data.get("passport_registration"), SecureValue, bot
|
||||
)
|
||||
data["rental_agreement"] = SecureValue.de_json(data.get("rental_agreement"), bot=bot)
|
||||
data["bank_statement"] = SecureValue.de_json(data.get("bank_statement"), bot=bot)
|
||||
data["utility_bill"] = SecureValue.de_json(data.get("utility_bill"), bot=bot)
|
||||
data["address"] = SecureValue.de_json(data.get("address"), bot=bot)
|
||||
data["identity_card"] = SecureValue.de_json(data.get("identity_card"), bot=bot)
|
||||
data["driver_license"] = SecureValue.de_json(data.get("driver_license"), bot=bot)
|
||||
data["internal_passport"] = SecureValue.de_json(data.get("internal_passport"), bot=bot)
|
||||
data["passport"] = SecureValue.de_json(data.get("passport"), bot=bot)
|
||||
data["personal_details"] = SecureValue.de_json(data.get("personal_details"), bot=bot)
|
||||
data["rental_agreement"] = de_json_optional(data.get("rental_agreement"), SecureValue, bot)
|
||||
data["bank_statement"] = de_json_optional(data.get("bank_statement"), SecureValue, bot)
|
||||
data["utility_bill"] = de_json_optional(data.get("utility_bill"), SecureValue, bot)
|
||||
data["address"] = de_json_optional(data.get("address"), SecureValue, bot)
|
||||
data["identity_card"] = de_json_optional(data.get("identity_card"), SecureValue, bot)
|
||||
data["driver_license"] = de_json_optional(data.get("driver_license"), SecureValue, bot)
|
||||
data["internal_passport"] = de_json_optional(
|
||||
data.get("internal_passport"), SecureValue, bot
|
||||
)
|
||||
data["passport"] = de_json_optional(data.get("passport"), SecureValue, bot)
|
||||
data["personal_details"] = de_json_optional(data.get("personal_details"), SecureValue, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -454,21 +446,16 @@ class SecureValue(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["SecureValue"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SecureValue":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["data"] = DataCredentials.de_json(data.get("data"), bot=bot)
|
||||
data["front_side"] = FileCredentials.de_json(data.get("front_side"), bot=bot)
|
||||
data["reverse_side"] = FileCredentials.de_json(data.get("reverse_side"), bot=bot)
|
||||
data["selfie"] = FileCredentials.de_json(data.get("selfie"), bot=bot)
|
||||
data["files"] = FileCredentials.de_list(data.get("files"), bot=bot)
|
||||
data["translation"] = FileCredentials.de_list(data.get("translation"), bot=bot)
|
||||
data["data"] = de_json_optional(data.get("data"), DataCredentials, bot)
|
||||
data["front_side"] = de_json_optional(data.get("front_side"), FileCredentials, bot)
|
||||
data["reverse_side"] = de_json_optional(data.get("reverse_side"), FileCredentials, bot)
|
||||
data["selfie"] = de_json_optional(data.get("selfie"), FileCredentials, bot)
|
||||
data["files"] = de_list_optional(data.get("files"), FileCredentials, bot)
|
||||
data["translation"] = de_list_optional(data.get("translation"), FileCredentials, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -25,7 +25,13 @@ from telegram._passport.credentials import decrypt_json
|
||||
from telegram._passport.data import IdDocumentData, PersonalDetails, ResidentialAddress
|
||||
from telegram._passport.passportfile import PassportFile
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import (
|
||||
de_json_decrypted_optional,
|
||||
de_json_optional,
|
||||
de_list_decrypted_optional,
|
||||
de_list_optional,
|
||||
parse_sequence_arg,
|
||||
)
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -194,27 +200,22 @@ class EncryptedPassportElement(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["EncryptedPassportElement"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "EncryptedPassportElement":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["files"] = PassportFile.de_list(data.get("files"), bot) or None
|
||||
data["front_side"] = PassportFile.de_json(data.get("front_side"), bot)
|
||||
data["reverse_side"] = PassportFile.de_json(data.get("reverse_side"), bot)
|
||||
data["selfie"] = PassportFile.de_json(data.get("selfie"), bot)
|
||||
data["translation"] = PassportFile.de_list(data.get("translation"), bot) or None
|
||||
data["files"] = de_list_optional(data.get("files"), PassportFile, bot) or None
|
||||
data["front_side"] = de_json_optional(data.get("front_side"), PassportFile, bot)
|
||||
data["reverse_side"] = de_json_optional(data.get("reverse_side"), PassportFile, bot)
|
||||
data["selfie"] = de_json_optional(data.get("selfie"), PassportFile, bot)
|
||||
data["translation"] = de_list_optional(data.get("translation"), PassportFile, bot) or None
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@classmethod
|
||||
def de_json_decrypted(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"], credentials: "Credentials"
|
||||
) -> Optional["EncryptedPassportElement"]:
|
||||
cls, data: JSONDict, bot: Optional["Bot"], credentials: "Credentials"
|
||||
) -> "EncryptedPassportElement":
|
||||
"""Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account
|
||||
passport credentials.
|
||||
|
||||
@@ -234,8 +235,6 @@ class EncryptedPassportElement(TelegramObject):
|
||||
:class:`telegram.EncryptedPassportElement`:
|
||||
|
||||
"""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
if data["type"] not in ("phone_number", "email"):
|
||||
secure_data = getattr(credentials.secure_data, data["type"])
|
||||
@@ -261,20 +260,21 @@ class EncryptedPassportElement(TelegramObject):
|
||||
data["data"] = ResidentialAddress.de_json(data["data"], bot=bot)
|
||||
|
||||
data["files"] = (
|
||||
PassportFile.de_list_decrypted(data.get("files"), bot, secure_data.files) or None
|
||||
de_list_decrypted_optional(data.get("files"), PassportFile, bot, secure_data.files)
|
||||
or None
|
||||
)
|
||||
data["front_side"] = PassportFile.de_json_decrypted(
|
||||
data.get("front_side"), bot, secure_data.front_side
|
||||
data["front_side"] = de_json_decrypted_optional(
|
||||
data.get("front_side"), PassportFile, bot, secure_data.front_side
|
||||
)
|
||||
data["reverse_side"] = PassportFile.de_json_decrypted(
|
||||
data.get("reverse_side"), bot, secure_data.reverse_side
|
||||
data["reverse_side"] = de_json_decrypted_optional(
|
||||
data.get("reverse_side"), PassportFile, bot, secure_data.reverse_side
|
||||
)
|
||||
data["selfie"] = PassportFile.de_json_decrypted(
|
||||
data.get("selfie"), bot, secure_data.selfie
|
||||
data["selfie"] = de_json_decrypted_optional(
|
||||
data.get("selfie"), PassportFile, bot, secure_data.selfie
|
||||
)
|
||||
data["translation"] = (
|
||||
PassportFile.de_list_decrypted(
|
||||
data.get("translation"), bot, secure_data.translation
|
||||
de_list_decrypted_optional(
|
||||
data.get("translation"), PassportFile, bot, secure_data.translation
|
||||
)
|
||||
or None
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._passport.credentials import EncryptedCredentials
|
||||
from telegram._passport.encryptedpassportelement import EncryptedPassportElement
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -82,17 +82,12 @@ class PassportData(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PassportData"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PassportData":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["data"] = EncryptedPassportElement.de_list(data.get("data"), bot)
|
||||
data["credentials"] = EncryptedCredentials.de_json(data.get("credentials"), bot)
|
||||
data["data"] = de_list_optional(data.get("data"), EncryptedPassportElement, bot)
|
||||
data["credentials"] = de_json_optional(data.get("credentials"), EncryptedCredentials, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -118,8 +118,8 @@ class PassportFile(TelegramObject):
|
||||
|
||||
@classmethod
|
||||
def de_json_decrypted(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"], credentials: "FileCredentials"
|
||||
) -> Optional["PassportFile"]:
|
||||
cls, data: JSONDict, bot: Optional["Bot"], credentials: "FileCredentials"
|
||||
) -> "PassportFile":
|
||||
"""Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account
|
||||
passport credentials.
|
||||
|
||||
@@ -141,9 +141,6 @@ class PassportFile(TelegramObject):
|
||||
"""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["credentials"] = credentials
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
@@ -151,10 +148,10 @@ class PassportFile(TelegramObject):
|
||||
@classmethod
|
||||
def de_list_decrypted(
|
||||
cls,
|
||||
data: Optional[list[JSONDict]],
|
||||
data: list[JSONDict],
|
||||
bot: Optional["Bot"],
|
||||
credentials: list["FileCredentials"],
|
||||
) -> tuple[Optional["PassportFile"], ...]:
|
||||
) -> tuple["PassportFile", ...]:
|
||||
"""Variant of :meth:`telegram.TelegramObject.de_list` that also takes into account
|
||||
passport credentials.
|
||||
|
||||
@@ -179,16 +176,12 @@ class PassportFile(TelegramObject):
|
||||
tuple[:class:`telegram.PassportFile`]:
|
||||
|
||||
"""
|
||||
if not data:
|
||||
return ()
|
||||
|
||||
return tuple(
|
||||
obj
|
||||
for obj in (
|
||||
cls.de_json_decrypted(passport_file, bot, credentials[i])
|
||||
for i, passport_file in enumerate(data)
|
||||
)
|
||||
if obj is not None
|
||||
)
|
||||
|
||||
async def get_file(
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._payment.shippingaddress import ShippingAddress
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -71,15 +72,12 @@ class OrderInfo(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["OrderInfo"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OrderInfo":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return cls()
|
||||
|
||||
data["shipping_address"] = ShippingAddress.de_json(data.get("shipping_address"), bot)
|
||||
data["shipping_address"] = de_json_optional(
|
||||
data.get("shipping_address"), ShippingAddress, bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._payment.orderinfo import OrderInfo
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
|
||||
@@ -110,17 +111,12 @@ class PreCheckoutQuery(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PreCheckoutQuery"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PreCheckoutQuery":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["order_info"] = OrderInfo.de_json(data.get("order_info"), bot)
|
||||
data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
|
||||
data["order_info"] = de_json_optional(data.get("order_info"), OrderInfo, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._payment.shippingaddress import ShippingAddress
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
|
||||
@@ -78,17 +79,14 @@ class ShippingQuery(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ShippingQuery"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ShippingQuery":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["shipping_address"] = ShippingAddress.de_json(data.get("shipping_address"), bot)
|
||||
data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
|
||||
data["shipping_address"] = de_json_optional(
|
||||
data.get("shipping_address"), ShippingAddress, bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._chat import Chat
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -105,16 +106,11 @@ class AffiliateInfo(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["AffiliateInfo"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "AffiliateInfo":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["affiliate_user"] = User.de_json(data.get("affiliate_user"), bot)
|
||||
data["affiliate_chat"] = Chat.de_json(data.get("affiliate_chat"), bot)
|
||||
data["affiliate_user"] = de_json_optional(data.get("affiliate_user"), User, bot)
|
||||
data["affiliate_chat"] = de_json_optional(data.get("affiliate_chat"), Chat, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -68,9 +68,7 @@ class RevenueWithdrawalState(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["RevenueWithdrawalState"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "RevenueWithdrawalState":
|
||||
"""Converts JSON data to the appropriate :class:`RevenueWithdrawalState` object, i.e. takes
|
||||
care of selecting the correct subclass.
|
||||
|
||||
@@ -84,9 +82,6 @@ class RevenueWithdrawalState(TelegramObject):
|
||||
"""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if (cls is RevenueWithdrawalState and not data) or data is None:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[RevenueWithdrawalState]] = {
|
||||
cls.PENDING: RevenueWithdrawalStatePending,
|
||||
cls.SUCCEEDED: RevenueWithdrawalStateSucceeded,
|
||||
@@ -156,14 +151,11 @@ class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState):
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["RevenueWithdrawalStateSucceeded"]:
|
||||
cls, data: JSONDict, bot: Optional["Bot"] = None
|
||||
) -> "RevenueWithdrawalStateSucceeded":
|
||||
"""See :meth:`telegram.RevenueWithdrawalState.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
|
||||
|
||||
@@ -24,7 +24,7 @@ from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -112,21 +112,16 @@ class StarTransaction(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["StarTransaction"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "StarTransaction":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
|
||||
|
||||
data["source"] = TransactionPartner.de_json(data.get("source"), bot)
|
||||
data["receiver"] = TransactionPartner.de_json(data.get("receiver"), bot)
|
||||
data["source"] = de_json_optional(data.get("source"), TransactionPartner, bot)
|
||||
data["receiver"] = de_json_optional(data.get("receiver"), TransactionPartner, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -159,14 +154,9 @@ class StarTransactions(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["StarTransactions"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "StarTransactions":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
data["transactions"] = StarTransaction.de_list(data.get("transactions"), bot)
|
||||
data["transactions"] = de_list_optional(data.get("transactions"), StarTransaction, bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -28,7 +28,7 @@ from telegram._paidmedia import PaidMedia
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
from .affiliateinfo import AffiliateInfo
|
||||
@@ -87,9 +87,7 @@ class TransactionPartner(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["TransactionPartner"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPartner":
|
||||
"""Converts JSON data to the appropriate :class:`TransactionPartner` object, i.e. takes
|
||||
care of selecting the correct subclass.
|
||||
|
||||
@@ -103,9 +101,6 @@ class TransactionPartner(TelegramObject):
|
||||
"""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if (cls is TransactionPartner and not data) or data is None:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[TransactionPartner]] = {
|
||||
cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram,
|
||||
cls.FRAGMENT: TransactionPartnerFragment,
|
||||
@@ -166,15 +161,12 @@ class TransactionPartnerAffiliateProgram(TransactionPartner):
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["TransactionPartnerAffiliateProgram"]:
|
||||
cls, data: JSONDict, bot: Optional["Bot"] = None
|
||||
) -> "TransactionPartnerAffiliateProgram":
|
||||
"""See :meth:`telegram.TransactionPartner.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["sponsor_user"] = User.de_json(data.get("sponsor_user"), bot)
|
||||
data["sponsor_user"] = de_json_optional(data.get("sponsor_user"), User, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
@@ -209,17 +201,12 @@ class TransactionPartnerFragment(TransactionPartner):
|
||||
self.withdrawal_state: Optional[RevenueWithdrawalState] = withdrawal_state
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["TransactionPartnerFragment"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPartnerFragment":
|
||||
"""See :meth:`telegram.TransactionPartner.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
data["withdrawal_state"] = RevenueWithdrawalState.de_json(
|
||||
data.get("withdrawal_state"), bot
|
||||
data["withdrawal_state"] = de_json_optional(
|
||||
data.get("withdrawal_state"), RevenueWithdrawalState, bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
@@ -320,24 +307,19 @@ class TransactionPartnerUser(TransactionPartner):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["TransactionPartnerUser"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPartnerUser":
|
||||
"""See :meth:`telegram.TransactionPartner.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
data["affiliate"] = AffiliateInfo.de_json(data.get("affiliate"), bot)
|
||||
data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot)
|
||||
data["user"] = de_json_optional(data.get("user"), User, bot)
|
||||
data["affiliate"] = de_json_optional(data.get("affiliate"), AffiliateInfo, bot)
|
||||
data["paid_media"] = de_list_optional(data.get("paid_media"), PaidMedia, bot)
|
||||
data["subscription_period"] = (
|
||||
dtm.timedelta(seconds=sp)
|
||||
if (sp := data.get("subscription_period")) is not None
|
||||
else None
|
||||
)
|
||||
data["gift"] = Gift.de_json(data.get("gift"), bot)
|
||||
data["gift"] = de_json_optional(data.get("gift"), Gift, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._payment.orderinfo import OrderInfo
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
@@ -138,16 +139,11 @@ class SuccessfulPayment(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["SuccessfulPayment"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuccessfulPayment":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["order_info"] = OrderInfo.de_json(data.get("order_info"), bot)
|
||||
data["order_info"] = de_json_optional(data.get("order_info"), OrderInfo, bot)
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
+16
-30
@@ -27,7 +27,7 @@ from telegram._messageentity import MessageEntity
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.entities import parse_message_entities, parse_message_entity
|
||||
@@ -91,16 +91,11 @@ class InputPollOption(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["InputPollOption"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InputPollOption":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)
|
||||
data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -157,16 +152,11 @@ class PollOption(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PollOption"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PollOption":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)
|
||||
data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -306,17 +296,12 @@ class PollAnswer(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["PollAnswer"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PollAnswer":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
data["voter_chat"] = Chat.de_json(data.get("voter_chat"), bot)
|
||||
data["user"] = de_json_optional(data.get("user"), User, bot)
|
||||
data["voter_chat"] = de_json_optional(data.get("voter_chat"), Chat, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -474,20 +459,21 @@ class Poll(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Poll"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Poll":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["options"] = [PollOption.de_json(option, bot) for option in data["options"]]
|
||||
data["explanation_entities"] = MessageEntity.de_list(data.get("explanation_entities"), bot)
|
||||
data["options"] = de_list_optional(data.get("options"), PollOption, bot)
|
||||
data["explanation_entities"] = de_list_optional(
|
||||
data.get("explanation_entities"), MessageEntity, bot
|
||||
)
|
||||
data["close_date"] = from_timestamp(data.get("close_date"), tzinfo=loc_tzinfo)
|
||||
data["question_entities"] = MessageEntity.de_list(data.get("question_entities"), bot)
|
||||
data["question_entities"] = de_list_optional(
|
||||
data.get("question_entities"), MessageEntity, bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -67,16 +68,11 @@ class ProximityAlertTriggered(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ProximityAlertTriggered"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ProximityAlertTriggered":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["traveler"] = User.de_json(data.get("traveler"), bot)
|
||||
data["watcher"] = User.de_json(data.get("watcher"), bot)
|
||||
data["traveler"] = de_json_optional(data.get("traveler"), User, bot)
|
||||
data["watcher"] = de_json_optional(data.get("watcher"), User, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+4
-16
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Final, Literal, Optional, Union
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -77,18 +78,10 @@ class ReactionType(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ReactionType"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ReactionType":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if not data and cls is ReactionType:
|
||||
return None
|
||||
|
||||
_class_mapping: dict[str, type[ReactionType]] = {
|
||||
cls.EMOJI: ReactionTypeEmoji,
|
||||
cls.CUSTOM_EMOJI: ReactionTypeCustomEmoji,
|
||||
@@ -230,15 +223,10 @@ class ReactionCount(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ReactionCount"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ReactionCount":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["type"] = ReactionType.de_json(data.get("type"), bot)
|
||||
data["type"] = de_json_optional(data.get("type"), ReactionType, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+33
-44
@@ -43,7 +43,7 @@ from telegram._payment.invoice import Invoice
|
||||
from telegram._poll import Poll
|
||||
from telegram._story import Story
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
|
||||
@@ -248,39 +248,36 @@ class ExternalReplyInfo(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ExternalReplyInfo"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ExternalReplyInfo":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
data["origin"] = MessageOrigin.de_json(data.get("origin"), bot)
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["link_preview_options"] = LinkPreviewOptions.de_json(
|
||||
data.get("link_preview_options"), bot
|
||||
data["origin"] = de_json_optional(data.get("origin"), MessageOrigin, bot)
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["link_preview_options"] = de_json_optional(
|
||||
data.get("link_preview_options"), LinkPreviewOptions, bot
|
||||
)
|
||||
data["animation"] = Animation.de_json(data.get("animation"), bot)
|
||||
data["audio"] = Audio.de_json(data.get("audio"), bot)
|
||||
data["document"] = Document.de_json(data.get("document"), bot)
|
||||
data["photo"] = tuple(PhotoSize.de_list(data.get("photo"), bot))
|
||||
data["sticker"] = Sticker.de_json(data.get("sticker"), bot)
|
||||
data["story"] = Story.de_json(data.get("story"), bot)
|
||||
data["video"] = Video.de_json(data.get("video"), bot)
|
||||
data["video_note"] = VideoNote.de_json(data.get("video_note"), bot)
|
||||
data["voice"] = Voice.de_json(data.get("voice"), bot)
|
||||
data["contact"] = Contact.de_json(data.get("contact"), bot)
|
||||
data["dice"] = Dice.de_json(data.get("dice"), bot)
|
||||
data["game"] = Game.de_json(data.get("game"), bot)
|
||||
data["giveaway"] = Giveaway.de_json(data.get("giveaway"), bot)
|
||||
data["giveaway_winners"] = GiveawayWinners.de_json(data.get("giveaway_winners"), bot)
|
||||
data["invoice"] = Invoice.de_json(data.get("invoice"), bot)
|
||||
data["location"] = Location.de_json(data.get("location"), bot)
|
||||
data["poll"] = Poll.de_json(data.get("poll"), bot)
|
||||
data["venue"] = Venue.de_json(data.get("venue"), bot)
|
||||
data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot)
|
||||
data["animation"] = de_json_optional(data.get("animation"), Animation, bot)
|
||||
data["audio"] = de_json_optional(data.get("audio"), Audio, bot)
|
||||
data["document"] = de_json_optional(data.get("document"), Document, bot)
|
||||
data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
|
||||
data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot)
|
||||
data["story"] = de_json_optional(data.get("story"), Story, bot)
|
||||
data["video"] = de_json_optional(data.get("video"), Video, bot)
|
||||
data["video_note"] = de_json_optional(data.get("video_note"), VideoNote, bot)
|
||||
data["voice"] = de_json_optional(data.get("voice"), Voice, bot)
|
||||
data["contact"] = de_json_optional(data.get("contact"), Contact, bot)
|
||||
data["dice"] = de_json_optional(data.get("dice"), Dice, bot)
|
||||
data["game"] = de_json_optional(data.get("game"), Game, bot)
|
||||
data["giveaway"] = de_json_optional(data.get("giveaway"), Giveaway, bot)
|
||||
data["giveaway_winners"] = de_json_optional(
|
||||
data.get("giveaway_winners"), GiveawayWinners, bot
|
||||
)
|
||||
data["invoice"] = de_json_optional(data.get("invoice"), Invoice, bot)
|
||||
data["location"] = de_json_optional(data.get("location"), Location, bot)
|
||||
data["poll"] = de_json_optional(data.get("poll"), Poll, bot)
|
||||
data["venue"] = de_json_optional(data.get("venue"), Venue, bot)
|
||||
data["paid_media"] = de_json_optional(data.get("paid_media"), PaidMediaInfo, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -350,16 +347,11 @@ class TextQuote(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["TextQuote"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TextQuote":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
data["entities"] = tuple(MessageEntity.de_list(data.get("entities"), bot))
|
||||
data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -458,15 +450,12 @@ class ReplyParameters(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ReplyParameters"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ReplyParameters":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
data["quote_entities"] = tuple(MessageEntity.de_list(data.get("quote_entities"), bot))
|
||||
data["quote_entities"] = tuple(
|
||||
de_list_optional(data.get("quote_entities"), MessageEntity, bot)
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+7
-22
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._files.photosize import PhotoSize
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -84,16 +84,11 @@ class UsersShared(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["UsersShared"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UsersShared":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["users"] = SharedUser.de_list(data.get("users"), bot)
|
||||
data["users"] = de_list_optional(data.get("users"), SharedUser, bot)
|
||||
|
||||
api_kwargs = {}
|
||||
# This is a deprecated field that TG still returns for backwards compatibility
|
||||
@@ -175,16 +170,11 @@ class ChatShared(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["ChatShared"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatShared":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["photo"] = PhotoSize.de_list(data.get("photo"), bot)
|
||||
data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -255,14 +245,9 @@ class SharedUser(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["SharedUser"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SharedUser":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["photo"] = PhotoSize.de_list(data.get("photo"), bot)
|
||||
data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+1
-4
@@ -71,12 +71,9 @@ class Story(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Story"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Story":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat", {}), bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -377,23 +377,20 @@ class TelegramObject:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]:
|
||||
def _parse_data(data: JSONDict) -> JSONDict:
|
||||
"""Should be called by subclasses that override de_json to ensure that the input
|
||||
is not altered. Whoever calls de_json might still want to use the original input
|
||||
for something else.
|
||||
"""
|
||||
return None if data is None else data.copy()
|
||||
return data.copy()
|
||||
|
||||
@classmethod
|
||||
def _de_json(
|
||||
cls: type[Tele_co],
|
||||
data: Optional[JSONDict],
|
||||
data: JSONDict,
|
||||
bot: Optional["Bot"],
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> Optional[Tele_co]:
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
) -> Tele_co:
|
||||
# try-except is significantly faster in case we already have a correct argument set
|
||||
try:
|
||||
obj = cls(**data, api_kwargs=api_kwargs)
|
||||
@@ -417,9 +414,7 @@ class TelegramObject:
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls: type[Tele_co], data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional[Tele_co]:
|
||||
def de_json(cls: type[Tele_co], data: JSONDict, bot: Optional["Bot"] = None) -> Tele_co:
|
||||
"""Converts JSON data to a Telegram object.
|
||||
|
||||
Args:
|
||||
@@ -438,7 +433,7 @@ class TelegramObject:
|
||||
|
||||
@classmethod
|
||||
def de_list(
|
||||
cls: type[Tele_co], data: Optional[list[JSONDict]], bot: Optional["Bot"] = None
|
||||
cls: type[Tele_co], data: list[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> tuple[Tele_co, ...]:
|
||||
"""Converts a list of JSON objects to a tuple of Telegram objects.
|
||||
|
||||
@@ -459,10 +454,7 @@ class TelegramObject:
|
||||
A tuple of Telegram objects.
|
||||
|
||||
"""
|
||||
if not data:
|
||||
return ()
|
||||
|
||||
return tuple(obj for obj in (cls.de_json(d, bot) for d in data) if obj is not None)
|
||||
return tuple(cls.de_json(d, bot) for d in data)
|
||||
|
||||
@contextmanager
|
||||
def _unfrozen(self: Tele_co) -> Iterator[Tele_co]:
|
||||
|
||||
+43
-33
@@ -35,6 +35,7 @@ from telegram._payment.precheckoutquery import PreCheckoutQuery
|
||||
from telegram._payment.shippingquery import ShippingQuery
|
||||
from telegram._poll import Poll, PollAnswer
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._utils.warnings import warn
|
||||
|
||||
@@ -757,47 +758,56 @@ class Update(TelegramObject):
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Update"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Update":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["message"] = Message.de_json(data.get("message"), bot)
|
||||
data["edited_message"] = Message.de_json(data.get("edited_message"), bot)
|
||||
data["inline_query"] = InlineQuery.de_json(data.get("inline_query"), bot)
|
||||
data["chosen_inline_result"] = ChosenInlineResult.de_json(
|
||||
data.get("chosen_inline_result"), bot
|
||||
data["message"] = de_json_optional(data.get("message"), Message, bot)
|
||||
data["edited_message"] = de_json_optional(data.get("edited_message"), Message, bot)
|
||||
data["inline_query"] = de_json_optional(data.get("inline_query"), InlineQuery, bot)
|
||||
data["chosen_inline_result"] = de_json_optional(
|
||||
data.get("chosen_inline_result"), ChosenInlineResult, bot
|
||||
)
|
||||
data["callback_query"] = CallbackQuery.de_json(data.get("callback_query"), bot)
|
||||
data["shipping_query"] = ShippingQuery.de_json(data.get("shipping_query"), bot)
|
||||
data["pre_checkout_query"] = PreCheckoutQuery.de_json(data.get("pre_checkout_query"), bot)
|
||||
data["channel_post"] = Message.de_json(data.get("channel_post"), bot)
|
||||
data["edited_channel_post"] = Message.de_json(data.get("edited_channel_post"), bot)
|
||||
data["poll"] = Poll.de_json(data.get("poll"), bot)
|
||||
data["poll_answer"] = PollAnswer.de_json(data.get("poll_answer"), bot)
|
||||
data["my_chat_member"] = ChatMemberUpdated.de_json(data.get("my_chat_member"), bot)
|
||||
data["chat_member"] = ChatMemberUpdated.de_json(data.get("chat_member"), bot)
|
||||
data["chat_join_request"] = ChatJoinRequest.de_json(data.get("chat_join_request"), bot)
|
||||
data["chat_boost"] = ChatBoostUpdated.de_json(data.get("chat_boost"), bot)
|
||||
data["removed_chat_boost"] = ChatBoostRemoved.de_json(data.get("removed_chat_boost"), bot)
|
||||
data["message_reaction"] = MessageReactionUpdated.de_json(
|
||||
data.get("message_reaction"), bot
|
||||
data["callback_query"] = de_json_optional(data.get("callback_query"), CallbackQuery, bot)
|
||||
data["shipping_query"] = de_json_optional(data.get("shipping_query"), ShippingQuery, bot)
|
||||
data["pre_checkout_query"] = de_json_optional(
|
||||
data.get("pre_checkout_query"), PreCheckoutQuery, bot
|
||||
)
|
||||
data["message_reaction_count"] = MessageReactionCountUpdated.de_json(
|
||||
data.get("message_reaction_count"), bot
|
||||
data["channel_post"] = de_json_optional(data.get("channel_post"), Message, bot)
|
||||
data["edited_channel_post"] = de_json_optional(
|
||||
data.get("edited_channel_post"), Message, bot
|
||||
)
|
||||
data["business_connection"] = BusinessConnection.de_json(
|
||||
data.get("business_connection"), bot
|
||||
data["poll"] = de_json_optional(data.get("poll"), Poll, bot)
|
||||
data["poll_answer"] = de_json_optional(data.get("poll_answer"), PollAnswer, bot)
|
||||
data["my_chat_member"] = de_json_optional(
|
||||
data.get("my_chat_member"), ChatMemberUpdated, bot
|
||||
)
|
||||
data["business_message"] = Message.de_json(data.get("business_message"), bot)
|
||||
data["edited_business_message"] = Message.de_json(data.get("edited_business_message"), bot)
|
||||
data["deleted_business_messages"] = BusinessMessagesDeleted.de_json(
|
||||
data.get("deleted_business_messages"), bot
|
||||
data["chat_member"] = de_json_optional(data.get("chat_member"), ChatMemberUpdated, bot)
|
||||
data["chat_join_request"] = de_json_optional(
|
||||
data.get("chat_join_request"), ChatJoinRequest, bot
|
||||
)
|
||||
data["purchased_paid_media"] = PaidMediaPurchased.de_json(
|
||||
data.get("purchased_paid_media"), bot
|
||||
data["chat_boost"] = de_json_optional(data.get("chat_boost"), ChatBoostUpdated, bot)
|
||||
data["removed_chat_boost"] = de_json_optional(
|
||||
data.get("removed_chat_boost"), ChatBoostRemoved, bot
|
||||
)
|
||||
data["message_reaction"] = de_json_optional(
|
||||
data.get("message_reaction"), MessageReactionUpdated, bot
|
||||
)
|
||||
data["message_reaction_count"] = de_json_optional(
|
||||
data.get("message_reaction_count"), MessageReactionCountUpdated, bot
|
||||
)
|
||||
data["business_connection"] = de_json_optional(
|
||||
data.get("business_connection"), BusinessConnection, bot
|
||||
)
|
||||
data["business_message"] = de_json_optional(data.get("business_message"), Message, bot)
|
||||
data["edited_business_message"] = de_json_optional(
|
||||
data.get("edited_business_message"), Message, bot
|
||||
)
|
||||
data["deleted_business_messages"] = de_json_optional(
|
||||
data.get("deleted_business_messages"), BusinessMessagesDeleted, bot
|
||||
)
|
||||
data["purchased_paid_media"] = de_json_optional(
|
||||
data.get("purchased_paid_media"), PaidMediaPurchased, bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+15
-8
@@ -26,7 +26,14 @@ from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton
|
||||
from telegram._menubutton import MenuButton
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
|
||||
from telegram._utils.types import (
|
||||
CorrectOptionID,
|
||||
FileInput,
|
||||
JSONDict,
|
||||
ODVInput,
|
||||
ReplyMarkup,
|
||||
TimePeriod,
|
||||
)
|
||||
from telegram.helpers import mention_html as helpers_mention_html
|
||||
from telegram.helpers import mention_markdown as helpers_mention_markdown
|
||||
|
||||
@@ -668,7 +675,7 @@ class User(TelegramObject):
|
||||
async def send_audio(
|
||||
self,
|
||||
audio: Union[FileInput, "Audio"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
performer: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -1113,7 +1120,7 @@ class User(TelegramObject):
|
||||
longitude: Optional[float] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
@@ -1175,7 +1182,7 @@ class User(TelegramObject):
|
||||
async def send_animation(
|
||||
self,
|
||||
animation: Union[FileInput, "Animation"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -1303,7 +1310,7 @@ class User(TelegramObject):
|
||||
async def send_video(
|
||||
self,
|
||||
video: Union[FileInput, "Video"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -1447,7 +1454,7 @@ class User(TelegramObject):
|
||||
async def send_video_note(
|
||||
self,
|
||||
video_note: Union[FileInput, "VideoNote"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
length: Optional[int] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -1508,7 +1515,7 @@ class User(TelegramObject):
|
||||
async def send_voice(
|
||||
self,
|
||||
voice: Union[FileInput, "Voice"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -1581,7 +1588,7 @@ class User(TelegramObject):
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
explanation: Optional[str] = None,
|
||||
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
open_period: Optional[int] = None,
|
||||
open_period: Optional[TimePeriod] = None,
|
||||
close_date: Optional[Union[int, dtm.datetime]] = None,
|
||||
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
|
||||
@@ -71,15 +71,10 @@ class UserProfilePhotos(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["UserProfilePhotos"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UserProfilePhotos":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["photos"] = [PhotoSize.de_list(photo, bot) for photo in data["photos"]]
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -24,10 +24,16 @@ Warning:
|
||||
the changelog.
|
||||
"""
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional, TypeVar
|
||||
from typing import TYPE_CHECKING, Optional, Protocol, TypeVar
|
||||
|
||||
from telegram._linkpreviewoptions import LinkPreviewOptions
|
||||
from telegram._utils.types import ODVInput
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import type_check_only
|
||||
|
||||
from telegram import Bot, FileCredentials
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -60,3 +66,75 @@ def parse_lpo_and_dwpp(
|
||||
link_preview_options = LinkPreviewOptions(is_disabled=disable_web_page_preview)
|
||||
|
||||
return link_preview_options
|
||||
|
||||
|
||||
Tele_co = TypeVar("Tele_co", bound=TelegramObject, covariant=True)
|
||||
TeleCrypto_co = TypeVar("TeleCrypto_co", bound="HasDecryptMethod", covariant=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@type_check_only
|
||||
class HasDecryptMethod(Protocol):
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def de_json_decrypted(
|
||||
cls: type[TeleCrypto_co],
|
||||
data: JSONDict,
|
||||
bot: Optional["Bot"],
|
||||
credentials: list["FileCredentials"],
|
||||
) -> TeleCrypto_co: ...
|
||||
|
||||
@classmethod
|
||||
def de_list_decrypted(
|
||||
cls: type[TeleCrypto_co],
|
||||
data: list[JSONDict],
|
||||
bot: Optional["Bot"],
|
||||
credentials: list["FileCredentials"],
|
||||
) -> tuple[TeleCrypto_co, ...]: ...
|
||||
|
||||
|
||||
def de_json_optional(
|
||||
data: Optional[JSONDict], cls: type[Tele_co], bot: Optional["Bot"]
|
||||
) -> Optional[Tele_co]:
|
||||
"""Wrapper around TO.de_json that returns None if data is None."""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
return cls.de_json(data, bot)
|
||||
|
||||
|
||||
def de_json_decrypted_optional(
|
||||
data: Optional[JSONDict],
|
||||
cls: type[TeleCrypto_co],
|
||||
bot: Optional["Bot"],
|
||||
credentials: list["FileCredentials"],
|
||||
) -> Optional[TeleCrypto_co]:
|
||||
"""Wrapper around TO.de_json_decrypted that returns None if data is None."""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
return cls.de_json_decrypted(data, bot, credentials)
|
||||
|
||||
|
||||
def de_list_optional(
|
||||
data: Optional[list[JSONDict]], cls: type[Tele_co], bot: Optional["Bot"]
|
||||
) -> tuple[Tele_co, ...]:
|
||||
"""Wrapper around TO.de_list that returns an empty list if data is None."""
|
||||
if data is None:
|
||||
return ()
|
||||
|
||||
return cls.de_list(data, bot)
|
||||
|
||||
|
||||
def de_list_decrypted_optional(
|
||||
data: Optional[list[JSONDict]],
|
||||
cls: type[TeleCrypto_co],
|
||||
bot: Optional["Bot"],
|
||||
credentials: list["FileCredentials"],
|
||||
) -> tuple[TeleCrypto_co, ...]:
|
||||
"""Wrapper around TO.de_list_decrypted that returns an empty list if data is None."""
|
||||
if data is None:
|
||||
return ()
|
||||
|
||||
return cls.de_list_decrypted(data, bot, credentials)
|
||||
|
||||
@@ -23,9 +23,10 @@ Warning:
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
import datetime as dtm
|
||||
from collections.abc import Collection
|
||||
from pathlib import Path
|
||||
from typing import IO, TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
|
||||
from typing import IO, TYPE_CHECKING, Any, Callable, Literal, Optional, TypeVar, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import (
|
||||
@@ -91,3 +92,7 @@ SocketOpt = Union[
|
||||
tuple[int, int, Union[bytes, bytearray]],
|
||||
tuple[int, int, None, int],
|
||||
]
|
||||
|
||||
BaseUrl = Union[str, Callable[[str], str]]
|
||||
|
||||
TimePeriod = Union[int, dtm.timedelta]
|
||||
|
||||
+4
-12
@@ -126,14 +126,11 @@ class VideoChatParticipantsInvited(TelegramObject):
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["VideoChatParticipantsInvited"]:
|
||||
cls, data: JSONDict, bot: Optional["Bot"] = None
|
||||
) -> "VideoChatParticipantsInvited":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["users"] = User.de_list(data.get("users", []), bot)
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -178,18 +175,13 @@ class VideoChatScheduled(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["VideoChatScheduled"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "VideoChatScheduled":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["start_date"] = from_timestamp(data["start_date"], tzinfo=loc_tzinfo)
|
||||
data["start_date"] = from_timestamp(data.get("start_date"), tzinfo=loc_tzinfo)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -166,15 +166,10 @@ class WebhookInfo(TelegramObject):
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(
|
||||
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
|
||||
) -> Optional["WebhookInfo"]:
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "WebhookInfo":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ __all__ = (
|
||||
"Defaults",
|
||||
"DictPersistence",
|
||||
"ExtBot",
|
||||
"FiniteStateMachine",
|
||||
"InlineQueryHandler",
|
||||
"InvalidCallbackData",
|
||||
"Job",
|
||||
@@ -57,6 +58,9 @@ __all__ = (
|
||||
"PrefixHandler",
|
||||
"ShippingQueryHandler",
|
||||
"SimpleUpdateProcessor",
|
||||
"SingleStateMachine",
|
||||
"State",
|
||||
"StateInfo",
|
||||
"StringCommandHandler",
|
||||
"StringRegexHandler",
|
||||
"TypeHandler",
|
||||
@@ -77,6 +81,7 @@ from ._contexttypes import ContextTypes
|
||||
from ._defaults import Defaults
|
||||
from ._dictpersistence import DictPersistence
|
||||
from ._extbot import ExtBot
|
||||
from ._fsm import FiniteStateMachine, SingleStateMachine, State, StateInfo
|
||||
from ._handlers.basehandler import BaseHandler
|
||||
from ._handlers.businessconnectionhandler import BusinessConnectionHandler
|
||||
from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler
|
||||
|
||||
@@ -32,6 +32,7 @@ try:
|
||||
except ImportError:
|
||||
AIO_LIMITER_AVAILABLE = False
|
||||
|
||||
from telegram import constants
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.error import RetryAfter
|
||||
@@ -86,7 +87,8 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
* 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.
|
||||
be allowed to send messages in another group or with
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` set to :obj:`True`.
|
||||
|
||||
Tip:
|
||||
With `Bot API 7.1 <https://core.telegram.org/bots/api-changelog#october-31-2024>`_
|
||||
@@ -96,10 +98,10 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
:tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second by
|
||||
paying a fee in Telegram Stars.
|
||||
|
||||
.. caution::
|
||||
This class currently doesn't take the
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` parameter into account.
|
||||
This means that the rate limiting is applied just like for any other message.
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
This class automatically takes the
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` parameter into account and
|
||||
throttles the requests accordingly.
|
||||
|
||||
Note:
|
||||
This class is to be understood as minimal effort reference implementation.
|
||||
@@ -114,16 +116,17 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
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``.
|
||||
Defaults to :tg-const:`telegram.constants.FloodLimit.MESSAGES_PER_SECOND`.
|
||||
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.
|
||||
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.
|
||||
limiting will be applied. Defaults to
|
||||
:tg-const:`telegram.constants.FloodLimit.MESSAGES_PER_MINUTE_PER_GROUP`.
|
||||
group_time_period (:obj:`float`): The time period (in seconds) during which the
|
||||
:paramref:`group_max_rate` is enforced. When set to 0, no rate limiting will be
|
||||
applied. Defaults to 60.
|
||||
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``.
|
||||
@@ -131,6 +134,7 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_apb_limiter",
|
||||
"_base_limiter",
|
||||
"_group_limiters",
|
||||
"_group_max_rate",
|
||||
@@ -141,9 +145,9 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
overall_max_rate: float = 30,
|
||||
overall_max_rate: float = constants.FloodLimit.MESSAGES_PER_SECOND,
|
||||
overall_time_period: float = 1,
|
||||
group_max_rate: float = 20,
|
||||
group_max_rate: float = constants.FloodLimit.MESSAGES_PER_MINUTE_PER_GROUP,
|
||||
group_time_period: float = 60,
|
||||
max_retries: int = 0,
|
||||
) -> None:
|
||||
@@ -167,6 +171,9 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
self._group_time_period = 0
|
||||
|
||||
self._group_limiters: dict[Union[str, int], AsyncLimiter] = {}
|
||||
self._apb_limiter: AsyncLimiter = AsyncLimiter(
|
||||
max_rate=constants.FloodLimit.PAID_MESSAGES_PER_SECOND, time_period=1
|
||||
)
|
||||
self._max_retries: int = max_retries
|
||||
self._retry_after_event = asyncio.Event()
|
||||
self._retry_after_event.set()
|
||||
@@ -201,21 +208,30 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
self,
|
||||
chat: bool,
|
||||
group: Union[str, int, bool],
|
||||
allow_paid_broadcast: bool,
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, list[JSONDict]]]],
|
||||
args: Any,
|
||||
kwargs: dict[str, Any],
|
||||
) -> Union[bool, JSONDict, list[JSONDict]]:
|
||||
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, base_context:
|
||||
async def inner() -> Union[bool, JSONDict, list[JSONDict]]:
|
||||
# In case a retry_after was hit, we wait with processing the request
|
||||
await self._retry_after_event.wait()
|
||||
|
||||
return await callback(*args, **kwargs)
|
||||
|
||||
if allow_paid_broadcast:
|
||||
async with self._apb_limiter:
|
||||
return await inner()
|
||||
else:
|
||||
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, base_context:
|
||||
return await inner()
|
||||
|
||||
# mypy doesn't understand that the last run of the for loop raises an exception
|
||||
async def process_request(
|
||||
self,
|
||||
@@ -242,12 +258,13 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
group: Union[int, str, bool] = False
|
||||
chat: bool = False
|
||||
chat_id = data.get("chat_id")
|
||||
allow_paid_broadcast = data.get("allow_paid_broadcast", False)
|
||||
if chat_id is not None:
|
||||
chat = True
|
||||
|
||||
# In case user passes integer chat id as string
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
chat_id = int(chat_id)
|
||||
chat_id = int(chat_id) # type: ignore[arg-type]
|
||||
|
||||
if (isinstance(chat_id, int) and chat_id < 0) or isinstance(chat_id, str):
|
||||
# string chat_id only works for channels and supergroups
|
||||
@@ -257,7 +274,12 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
for i in range(max_retries + 1):
|
||||
try:
|
||||
return await self._run_request(
|
||||
chat=chat, group=group, callback=callback, args=args, kwargs=kwargs
|
||||
chat=chat,
|
||||
group=group,
|
||||
allow_paid_broadcast=allow_paid_broadcast,
|
||||
callback=callback,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
except RetryAfter as exc:
|
||||
if i == max_retries:
|
||||
|
||||
@@ -48,6 +48,7 @@ from telegram.error import TelegramError
|
||||
from telegram.ext._basepersistence import BasePersistence
|
||||
from telegram.ext._contexttypes import ContextTypes
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._fsm import SingleStateMachine, State, StateInfo
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._updater import Updater
|
||||
from telegram.ext._utils.stack import was_called_by
|
||||
@@ -59,7 +60,7 @@ if TYPE_CHECKING:
|
||||
from socket import socket
|
||||
|
||||
from telegram import Message
|
||||
from telegram.ext import ConversationHandler, JobQueue
|
||||
from telegram.ext import ConversationHandler, FiniteStateMachine, JobQueue
|
||||
from telegram.ext._applicationbuilder import InitApplicationBuilder
|
||||
from telegram.ext._baseupdateprocessor import BaseUpdateProcessor
|
||||
from telegram.ext._jobqueue import Job
|
||||
@@ -235,7 +236,7 @@ class Application(
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
( # noqa: RUF005
|
||||
(
|
||||
"__create_task_tasks",
|
||||
"__update_fetcher_task",
|
||||
"__update_persistence_event",
|
||||
@@ -266,13 +267,12 @@ class Application(
|
||||
"update_queue",
|
||||
"updater",
|
||||
"user_data",
|
||||
"fsm",
|
||||
)
|
||||
# Allowing '__weakref__' creation here since we need it for the JobQueue
|
||||
# Currently the __weakref__ slot is already created
|
||||
# in the AsyncContextManager base class for pythons < 3.13
|
||||
+ ("__weakref__",)
|
||||
if sys.version_info >= (3, 13)
|
||||
else ()
|
||||
+ (("__weakref__",) if sys.version_info >= (3, 13) else ())
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -303,11 +303,12 @@ class Application(
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self.fsm: FiniteStateMachine = SingleStateMachine()
|
||||
self.bot: BT = bot
|
||||
self.update_queue: asyncio.Queue[object] = update_queue
|
||||
self.context_types: ContextTypes[CCT, UD, CD, BD] = context_types
|
||||
self.updater: Optional[Updater] = updater
|
||||
self.handlers: dict[int, list[BaseHandler[Any, CCT, Any]]] = {}
|
||||
self.handlers: dict[State, dict[int, list[BaseHandler[Any, CCT, Any]]]] = {}
|
||||
self.error_handlers: dict[
|
||||
HandlerCallback[object, CCT, None], Union[bool, DefaultValue[bool]]
|
||||
] = {}
|
||||
@@ -1280,19 +1281,46 @@ class Application(
|
||||
# Processing updates before initialize() is a problem e.g. if persistence is used
|
||||
self._check_initialized()
|
||||
|
||||
fsm_state_info = self.fsm.get_state_info(update)
|
||||
|
||||
for state, state_handlers in self.handlers.items():
|
||||
if state.matches(fsm_state_info.state):
|
||||
_LOGGER.debug("Processing in state %s", state)
|
||||
was_handled = await self.__process_update_groups(
|
||||
update, state_handlers, fsm_state_info
|
||||
)
|
||||
if was_handled:
|
||||
_LOGGER.debug(
|
||||
"Update was handled in state %s. Stopping further processing", state
|
||||
)
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"No handlers found for key %s in state %s", fsm_state_info.key, fsm_state_info.state
|
||||
)
|
||||
return
|
||||
|
||||
async def __process_update_groups(
|
||||
self,
|
||||
update: object,
|
||||
state_handlers: dict[int, list[BaseHandler]],
|
||||
fsm_state_info: StateInfo,
|
||||
) -> bool:
|
||||
context = None
|
||||
was_handled = False
|
||||
any_blocking = False # Flag which is set to True if any handler specifies block=True
|
||||
|
||||
for handlers in self.handlers.values():
|
||||
for handlers in state_handlers.values():
|
||||
try:
|
||||
for handler in handlers:
|
||||
check = handler.check_update(update) # Should the handler handle this update?
|
||||
if check is None or check is False:
|
||||
continue
|
||||
was_handled = True
|
||||
|
||||
if not context: # build a context if not already built
|
||||
try:
|
||||
context = self.context_types.context.from_update(update, self)
|
||||
context.fsm_state_info = fsm_state_info
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
(
|
||||
@@ -1302,7 +1330,7 @@ class Application(
|
||||
update,
|
||||
exc_info=exc,
|
||||
)
|
||||
return
|
||||
return True
|
||||
await context.refresh_data()
|
||||
coroutine: Coroutine = handler.handle_update(update, self, check, context)
|
||||
|
||||
@@ -1342,7 +1370,14 @@ class Application(
|
||||
# (in __create_task_callback)
|
||||
self._mark_for_persistence_update(update=update)
|
||||
|
||||
def add_handler(self, handler: BaseHandler[Any, CCT, Any], group: int = DEFAULT_GROUP) -> None:
|
||||
return was_handled
|
||||
|
||||
def add_handler(
|
||||
self,
|
||||
handler: BaseHandler[Any, CCT, Any],
|
||||
group: int = DEFAULT_GROUP,
|
||||
state: State = State.IDLE,
|
||||
) -> None:
|
||||
"""Register a handler.
|
||||
|
||||
TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of
|
||||
@@ -1401,11 +1436,11 @@ class Application(
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if group not in self.handlers:
|
||||
self.handlers[group] = []
|
||||
self.handlers = dict(sorted(self.handlers.items())) # lower -> higher groups
|
||||
state_handlers = self.handlers.setdefault(state, {})
|
||||
if group not in state_handlers:
|
||||
state_handlers[group] = []
|
||||
|
||||
self.handlers[group].append(handler)
|
||||
state_handlers[group].append(handler)
|
||||
|
||||
def add_handlers(
|
||||
self,
|
||||
@@ -1477,10 +1512,11 @@ class Application(
|
||||
group (:obj:`object`, optional): The group identifier. Default is ``0``.
|
||||
|
||||
"""
|
||||
if handler in self.handlers[group]:
|
||||
self.handlers[group].remove(handler)
|
||||
if not self.handlers[group]:
|
||||
del self.handlers[group]
|
||||
for state_handlers in self.handlers.values():
|
||||
if handler in state_handlers[group]:
|
||||
state_handlers[group].remove(handler)
|
||||
if not state_handlers[group]:
|
||||
del state_handlers[group]
|
||||
|
||||
def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Drops the corresponding entry from the :attr:`chat_data`. Will also be deleted from
|
||||
|
||||
@@ -26,7 +26,15 @@ import httpx
|
||||
|
||||
from telegram._bot import Bot
|
||||
from telegram._utils.defaultvalue import DEFAULT_FALSE, DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.types import DVInput, DVType, FilePathInput, HTTPVersion, ODVInput, SocketOpt
|
||||
from telegram._utils.types import (
|
||||
BaseUrl,
|
||||
DVInput,
|
||||
DVType,
|
||||
FilePathInput,
|
||||
HTTPVersion,
|
||||
ODVInput,
|
||||
SocketOpt,
|
||||
)
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._application import Application
|
||||
from telegram.ext._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor
|
||||
@@ -164,8 +172,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
|
||||
def __init__(self: "InitApplicationBuilder"):
|
||||
self._token: DVType[str] = DefaultValue("")
|
||||
self._base_url: DVType[str] = DefaultValue("https://api.telegram.org/bot")
|
||||
self._base_file_url: DVType[str] = DefaultValue("https://api.telegram.org/file/bot")
|
||||
self._base_url: DVType[BaseUrl] = DefaultValue("https://api.telegram.org/bot")
|
||||
self._base_file_url: DVType[BaseUrl] = DefaultValue("https://api.telegram.org/file/bot")
|
||||
self._connection_pool_size: DVInput[int] = DEFAULT_NONE
|
||||
self._proxy: DVInput[Union[str, httpx.Proxy, httpx.URL]] = DEFAULT_NONE
|
||||
self._socket_options: DVInput[Collection[SocketOpt]] = DEFAULT_NONE
|
||||
@@ -378,15 +386,19 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
self._token = token
|
||||
return self
|
||||
|
||||
def base_url(self: BuilderType, base_url: str) -> BuilderType:
|
||||
def base_url(self: BuilderType, base_url: BaseUrl) -> BuilderType:
|
||||
"""Sets the base URL for :attr:`telegram.ext.Application.bot`. If not called,
|
||||
will default to ``'https://api.telegram.org/bot'``.
|
||||
|
||||
.. seealso:: :paramref:`telegram.Bot.base_url`,
|
||||
:wiki:`Local Bot API Server <Local-Bot-API-Server>`, :meth:`base_file_url`
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
|
||||
Args:
|
||||
base_url (:obj:`str`): The URL.
|
||||
base_url (:obj:`str` | Callable[[:obj:`str`], :obj:`str`]): The URL or
|
||||
input for the URL as accepted by :paramref:`telegram.Bot.base_url`.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
@@ -396,15 +408,19 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
self._base_url = base_url
|
||||
return self
|
||||
|
||||
def base_file_url(self: BuilderType, base_file_url: str) -> BuilderType:
|
||||
def base_file_url(self: BuilderType, base_file_url: BaseUrl) -> BuilderType:
|
||||
"""Sets the base file URL for :attr:`telegram.ext.Application.bot`. If not
|
||||
called, will default to ``'https://api.telegram.org/file/bot'``.
|
||||
|
||||
.. seealso:: :paramref:`telegram.Bot.base_file_url`,
|
||||
:wiki:`Local Bot API Server <Local-Bot-API-Server>`, :meth:`base_url`
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
|
||||
Args:
|
||||
base_file_url (:obj:`str`): The URL.
|
||||
base_file_url (:obj:`str` | Callable[[:obj:`str`], :obj:`str`]): The URL or
|
||||
input for the URL as accepted by :paramref:`telegram.Bot.base_file_url`.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
|
||||
@@ -18,11 +18,12 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the BaseProcessor class."""
|
||||
from abc import ABC, abstractmethod
|
||||
from asyncio import BoundedSemaphore
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, final
|
||||
|
||||
from telegram.ext._utils.asyncio import TrackedBoundedSemaphore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Awaitable
|
||||
|
||||
@@ -71,7 +72,7 @@ class BaseUpdateProcessor(AbstractAsyncContextManager["BaseUpdateProcessor"], AB
|
||||
self._max_concurrent_updates = max_concurrent_updates
|
||||
if self.max_concurrent_updates < 1:
|
||||
raise ValueError("`max_concurrent_updates` must be a positive integer!")
|
||||
self._semaphore = BoundedSemaphore(self.max_concurrent_updates)
|
||||
self._semaphore = TrackedBoundedSemaphore(self.max_concurrent_updates)
|
||||
|
||||
async def __aenter__(self: _BUPT) -> _BUPT: # noqa: PYI019
|
||||
"""|async_context_manager| :meth:`initializes <initialize>` the Processor.
|
||||
@@ -104,6 +105,18 @@ class BaseUpdateProcessor(AbstractAsyncContextManager["BaseUpdateProcessor"], AB
|
||||
""":obj:`int`: The maximum number of updates that can be processed concurrently."""
|
||||
return self._max_concurrent_updates
|
||||
|
||||
@property
|
||||
def current_concurrent_updates(self) -> int:
|
||||
""":obj:`int`: The number of updates currently being processed.
|
||||
|
||||
Caution:
|
||||
This value is a snapshot of the current number of updates being processed. It may
|
||||
change immediately after being read.
|
||||
|
||||
.. versionadded:: NEXT.VERSION
|
||||
"""
|
||||
return self.max_concurrent_updates - self._semaphore.current_value
|
||||
|
||||
@abstractmethod
|
||||
async def do_process_update(
|
||||
self,
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the CallbackContext class."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Generator
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from re import Match
|
||||
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, TypeVar, Union
|
||||
|
||||
@@ -26,12 +27,13 @@ from telegram._callbackquery import CallbackQuery
|
||||
from telegram._update import Update
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._fsm import FiniteStateMachine, State
|
||||
from telegram.ext._utils.types import BD, BT, CD, UD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asyncio import Future, Queue
|
||||
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
from telegram.ext import Application, Job, JobQueue, StateInfo
|
||||
from telegram.ext._utils.types import CCT
|
||||
|
||||
_STORING_DATA_WIKI = (
|
||||
@@ -121,6 +123,7 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
"args",
|
||||
"coroutine",
|
||||
"error",
|
||||
"fsm_state_info",
|
||||
"job",
|
||||
"matches",
|
||||
)
|
||||
@@ -141,11 +144,12 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
self.coroutine: Optional[
|
||||
Union[Generator[Optional[Future[object]], None, Any], Awaitable[Any]]
|
||||
] = None
|
||||
self.fsm_state_info: StateInfo = None # type: ignore[assignment]
|
||||
|
||||
@property
|
||||
def application(self) -> "Application[BT, ST, UD, CD, BD, Any]":
|
||||
""":class:`telegram.ext.Application`: The application associated with this context."""
|
||||
return self._application
|
||||
return self._application # type: ignore[return-value]
|
||||
|
||||
@property
|
||||
def bot_data(self) -> BD:
|
||||
@@ -269,6 +273,22 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
"telegram.Bot does not allow for arbitrary callback data."
|
||||
)
|
||||
|
||||
@property
|
||||
def fsm(self) -> FiniteStateMachine:
|
||||
return self.application.fsm
|
||||
|
||||
def fsm_semaphore(self) -> asyncio.Lock:
|
||||
return self.fsm.get_lock(self.fsm_state_info.key)
|
||||
|
||||
async def set_state(self, state: State) -> None:
|
||||
await self.fsm.set_state(self.fsm_state_info.key, state, self.fsm_state_info.version)
|
||||
|
||||
def set_state_nowait(self, state: State) -> None:
|
||||
self.fsm.set_state_nowait(self.fsm_state_info.key, state, self.fsm_state_info.version)
|
||||
|
||||
def as_fsm_state(self, state: State) -> AbstractAsyncContextManager[None]:
|
||||
return self.fsm.as_state(self.fsm_state_info.key, state)
|
||||
|
||||
@classmethod
|
||||
def from_error(
|
||||
cls: type["CCT"],
|
||||
|
||||
+28
-20
@@ -93,7 +93,15 @@ from telegram._utils.datetime import to_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
|
||||
from telegram._utils.types import (
|
||||
BaseUrl,
|
||||
CorrectOptionID,
|
||||
FileInput,
|
||||
JSONDict,
|
||||
ODVInput,
|
||||
ReplyMarkup,
|
||||
TimePeriod,
|
||||
)
|
||||
from telegram.ext._callbackdatacache import CallbackDataCache
|
||||
from telegram.ext._utils.types import RLARGS
|
||||
from telegram.request import BaseRequest
|
||||
@@ -184,8 +192,8 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
def __init__(
|
||||
self: "ExtBot[None]",
|
||||
token: str,
|
||||
base_url: str = "https://api.telegram.org/bot",
|
||||
base_file_url: str = "https://api.telegram.org/file/bot",
|
||||
base_url: BaseUrl = "https://api.telegram.org/bot",
|
||||
base_file_url: BaseUrl = "https://api.telegram.org/file/bot",
|
||||
request: Optional[BaseRequest] = None,
|
||||
get_updates_request: Optional[BaseRequest] = None,
|
||||
private_key: Optional[bytes] = None,
|
||||
@@ -199,8 +207,8 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
def __init__(
|
||||
self: "ExtBot[RLARGS]",
|
||||
token: str,
|
||||
base_url: str = "https://api.telegram.org/bot",
|
||||
base_file_url: str = "https://api.telegram.org/file/bot",
|
||||
base_url: BaseUrl = "https://api.telegram.org/bot",
|
||||
base_file_url: BaseUrl = "https://api.telegram.org/file/bot",
|
||||
request: Optional[BaseRequest] = None,
|
||||
get_updates_request: Optional[BaseRequest] = None,
|
||||
private_key: Optional[bytes] = None,
|
||||
@@ -214,8 +222,8 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
def __init__(
|
||||
self,
|
||||
token: str,
|
||||
base_url: str = "https://api.telegram.org/bot",
|
||||
base_file_url: str = "https://api.telegram.org/file/bot",
|
||||
base_url: BaseUrl = "https://api.telegram.org/bot",
|
||||
base_file_url: BaseUrl = "https://api.telegram.org/file/bot",
|
||||
request: Optional[BaseRequest] = None,
|
||||
get_updates_request: Optional[BaseRequest] = None,
|
||||
private_key: Optional[bytes] = None,
|
||||
@@ -638,7 +646,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
self,
|
||||
offset: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
timeout: Optional[int] = None, # noqa: ASYNC109
|
||||
timeout: Optional[int] = None,
|
||||
allowed_updates: Optional[Sequence[str]] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -926,7 +934,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
text: Optional[str] = None,
|
||||
show_alert: Optional[bool] = None,
|
||||
url: Optional[str] = None,
|
||||
cache_time: Optional[int] = None,
|
||||
cache_time: Optional[TimePeriod] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -954,7 +962,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
results: Union[
|
||||
Sequence["InlineQueryResult"], Callable[[int], Optional[Sequence["InlineQueryResult"]]]
|
||||
],
|
||||
cache_time: Optional[int] = None,
|
||||
cache_time: Optional[TimePeriod] = None,
|
||||
is_personal: Optional[bool] = None,
|
||||
next_offset: Optional[str] = None,
|
||||
button: Optional[InlineQueryResultsButton] = None,
|
||||
@@ -1204,7 +1212,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
send_phone_number_to_provider: Optional[bool] = None,
|
||||
send_email_to_provider: Optional[bool] = None,
|
||||
is_flexible: Optional[bool] = None,
|
||||
subscription_period: Optional[Union[int, dtm.timedelta]] = None,
|
||||
subscription_period: Optional[TimePeriod] = None,
|
||||
business_connection_id: Optional[str] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1589,7 +1597,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
business_connection_id: Optional[str] = None,
|
||||
*,
|
||||
location: Optional[Location] = None,
|
||||
@@ -2424,7 +2432,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
animation: Union[FileInput, "Animation"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -2486,7 +2494,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
audio: Union[FileInput, "Audio"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
performer: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
caption: Optional[str] = None,
|
||||
@@ -2841,7 +2849,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
longitude: Optional[float] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
live_period: Optional[int] = None,
|
||||
live_period: Optional[TimePeriod] = None,
|
||||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
@@ -3054,7 +3062,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
explanation: Optional[str] = None,
|
||||
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
open_period: Optional[int] = None,
|
||||
open_period: Optional[TimePeriod] = None,
|
||||
close_date: Optional[Union[int, dtm.datetime]] = None,
|
||||
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -3214,7 +3222,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
video: Union[FileInput, "Video"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -3278,7 +3286,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
video_note: Union[FileInput, "VideoNote"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
length: Optional[int] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -3328,7 +3336,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
voice: Union[FileInput, "Voice"],
|
||||
duration: Optional[int] = None,
|
||||
duration: Optional[TimePeriod] = None,
|
||||
caption: Optional[str] = None,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: Optional[ReplyMarkup] = None,
|
||||
@@ -4393,7 +4401,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
async def create_chat_subscription_invite_link(
|
||||
self,
|
||||
chat_id: Union[str, int],
|
||||
subscription_period: int,
|
||||
subscription_period: TimePeriod,
|
||||
subscription_price: int,
|
||||
name: Optional[str] = None,
|
||||
*,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Private Submbodule for finite state machine implementation."""
|
||||
|
||||
__all__ = ["FiniteStateMachine", "SingleStateMachine", "State", "StateInfo"]
|
||||
|
||||
from .machine import FiniteStateMachine, SingleStateMachine, StateInfo
|
||||
from .states import State
|
||||
@@ -0,0 +1,200 @@
|
||||
"""This Module contains the FiniteStateMachine class and the built-in subclass SingleStateMachine.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime as dtm
|
||||
import logging
|
||||
import time
|
||||
import weakref
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncIterator, Hashable, Mapping, MutableSequence, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Generic, Literal, Optional, TypeVar, Union, overload
|
||||
|
||||
from telegram.ext._fsm.states import State
|
||||
from telegram.ext._utils.types import JobCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
from telegram.ext import JobQueue
|
||||
|
||||
_KT = TypeVar("_KT", bound=Hashable)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class StateInfo(Generic[_KT]):
|
||||
def __init__(self: "StateInfo[_KT]", key: _KT, state: State, version: int) -> None:
|
||||
self.key: _KT = key
|
||||
self.state: State = state
|
||||
self.version: int = version
|
||||
|
||||
|
||||
class FiniteStateMachine(abc.ABC, Generic[_KT]):
|
||||
def __init__(self) -> None:
|
||||
self._locks: MutableMapping[_KT, asyncio.Lock] = weakref.WeakValueDictionary()
|
||||
|
||||
# There is likely litte benefit for a user to customize how exactly the states are stored
|
||||
# and accessed. So we make this private and only provide a read-only view.
|
||||
self.__states: dict[_KT, tuple[State, int]] = defaultdict(
|
||||
lambda: (State.IDLE, time.perf_counter_ns())
|
||||
)
|
||||
self._states = MappingProxyType(self.__states)
|
||||
|
||||
self.__job_queue: Optional[weakref.ReferenceType[JobQueue]] = None
|
||||
self.__history: Mapping[_KT, MutableSequence[State]] = defaultdict(
|
||||
lambda: deque(maxlen=10)
|
||||
)
|
||||
|
||||
@property
|
||||
def states(self) -> Mapping[_KT, tuple[State, int]]:
|
||||
return self._states
|
||||
|
||||
def store_state_history(self, key: _KT, state: State) -> None:
|
||||
# Making this public so that users can override if they want to customize the history
|
||||
# E.g., they could want to store more/fewer states, also depending on the key
|
||||
self.__history[key].append(state)
|
||||
|
||||
def get_state_history(self, key: _KT) -> Sequence[State]:
|
||||
return list(self.__history[key])
|
||||
|
||||
def get_lock(self, key: _KT) -> asyncio.Lock:
|
||||
"""Returns a lock that is unique for this key at runtime.
|
||||
It can be used to prevent concurrent access to resources associated to this key.
|
||||
"""
|
||||
return self._locks.setdefault(key, asyncio.Lock())
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_state_info(self, update: object) -> StateInfo[_KT]:
|
||||
"""Returns exactly one active state for the update.
|
||||
If more than one stored key applies to the update, one must be chosen.
|
||||
It's recommended to select the most specific one.
|
||||
|
||||
Example:
|
||||
The state of a chat, a user or a user in a specific chat could be tracked.
|
||||
For a message in that chat, the state of the user in that chat should be returned if
|
||||
available. Otherwise, the state of the chat should be returned.
|
||||
|
||||
Important:
|
||||
This must be an atomic operation and not e.g. wait for a lock.
|
||||
Instead, if necessary, return a special state indicating that the key is currently
|
||||
busy.
|
||||
"""
|
||||
|
||||
def _do_set_state(
|
||||
self, key: _KT, state: State, version: Optional[int] = None
|
||||
) -> StateInfo[_KT]:
|
||||
"""Protected method to set the state for the specified key.
|
||||
|
||||
The version can be optionally used for optimistic locking. If the version does not match
|
||||
the current version, the state should not be updated.
|
||||
|
||||
Important:
|
||||
This should be used exclusively by methods of this class and subclasses.
|
||||
It should *not* be called directly by users of this class!
|
||||
"""
|
||||
_LOGGER.debug("Setting %s state to %s", key, state)
|
||||
if state is State.ANY:
|
||||
raise ValueError("State.ANY is not supported in set_state")
|
||||
|
||||
if version and version != self._states.get(key, (None, None))[1]:
|
||||
raise ValueError("Optimistic locking failed. Not updating state.")
|
||||
|
||||
if jq := self._get_job_queue(raise_exception=False):
|
||||
# This is a rather tight coupling between FSM and JobQueue
|
||||
# Not sure if we like that. Makes it even harder to replace JobQueue
|
||||
# (or the JQ implementation) with something else.
|
||||
# The upside is that we don't need to maintain any additional internal state
|
||||
# for the jobs and persistence is handled by the JobQueue.
|
||||
cancel_jobs = jq.jobs(pattern=str(hash(key)))
|
||||
for job in cancel_jobs:
|
||||
_LOGGER.debug("Cancelling timeout job %s", job)
|
||||
job.schedule_removal()
|
||||
|
||||
# important to use time.perf_counter_ns() here, as time_ns() is not monotonic
|
||||
self.__states[key] = (state, time.perf_counter_ns())
|
||||
# Doing this *after* do_set_state so that any exceptions are raised before the history
|
||||
# is updated
|
||||
self.store_state_history(key, state)
|
||||
return StateInfo(key, state, self._states[key][1])
|
||||
|
||||
async def set_state(self, key: _KT, state: State, version: Optional[int] = None) -> None:
|
||||
"""Store the state for the specified key."""
|
||||
async with self.get_lock(key):
|
||||
self._do_set_state(key, state, version)
|
||||
|
||||
def set_state_nowait(self, key: _KT, state: State, version: Optional[int] = None) -> None:
|
||||
"""Store the state for the specified key without waiting for a lock."""
|
||||
if self.get_lock(key).locked():
|
||||
raise asyncio.InvalidStateError("Lock is locked")
|
||||
self._do_set_state(key, state, version)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def as_state(self, key: _KT, state: State) -> AsyncIterator[None]:
|
||||
"""Context manager to set the state for the specified key and reset it afterwards."""
|
||||
async with self.get_lock(key):
|
||||
current_state, current_version = self.states[key]
|
||||
new_version = self._do_set_state(key, state, current_version).version
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._do_set_state(key, current_state, new_version)
|
||||
|
||||
@staticmethod
|
||||
def _build_job_name(keys: Sequence[_KT]) -> str:
|
||||
return f"FSM_Job_{'_'.join(str(hash(k)) for k in keys)}"
|
||||
|
||||
def set_job_queue(self, job_queue: "JobQueue") -> None:
|
||||
self.__job_queue = weakref.ref(job_queue)
|
||||
|
||||
@overload
|
||||
def _get_job_queue(self, raise_exception: Literal[False]) -> Optional["JobQueue"]: ...
|
||||
|
||||
@overload
|
||||
def _get_job_queue(self) -> "JobQueue": ...
|
||||
|
||||
def _get_job_queue(self, raise_exception: bool = True) -> Optional["JobQueue"]:
|
||||
if self.__job_queue is None:
|
||||
if raise_exception:
|
||||
raise RuntimeError("JobQueue not set")
|
||||
return None
|
||||
job_queue = self.__job_queue()
|
||||
if job_queue is None:
|
||||
if raise_exception:
|
||||
raise RuntimeError("JobQueue was garbage collected")
|
||||
return None
|
||||
return job_queue
|
||||
|
||||
def schedule_timeout(
|
||||
self,
|
||||
callback: JobCallback,
|
||||
when: Union[float, dtm.timedelta, dtm.datetime, dtm.time],
|
||||
cancel_keys: Optional[Sequence[_KT]] = None,
|
||||
job_kwargs: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Schedule a timeout job. This is a thin wrapper around JobQueue.run_once.
|
||||
The callback will have to take care of resetting any state if necessary.
|
||||
Pass cancel_keys to automatically cancel the job when a new state is set for any of the
|
||||
keys.
|
||||
"""
|
||||
job_kwargs = job_kwargs or {}
|
||||
if cancel_keys:
|
||||
if "name" in job_kwargs:
|
||||
raise ValueError("job_kwargs must not contain a 'name' key")
|
||||
job_kwargs["name"] = self._build_job_name(cancel_keys)
|
||||
self._get_job_queue().run_once(callback, when, **job_kwargs)
|
||||
_LOGGER.debug(
|
||||
"Scheduled timeout. Will be cancelled when a new set state is for either of: %s",
|
||||
cancel_keys or [],
|
||||
)
|
||||
|
||||
|
||||
class SingleStateMachine(FiniteStateMachine[None]):
|
||||
def get_state_info(self, update: object) -> StateInfo[None]: # noqa: ARG002
|
||||
return StateInfo(None, State.IDLE, 0)
|
||||
|
||||
def do_set_state(self, key: None, state: State) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,114 @@
|
||||
"""This Module contains implementations of State classes for Finite State Machines"""
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
from typing import ClassVar, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class State(abc.ABC):
|
||||
__knows_uids: ClassVar[set[str]] = set()
|
||||
__not_cache: ClassVar[dict[str, "_NOTState"]] = {}
|
||||
__or_cache: ClassVar[dict[tuple[str, str], "_ORState"]] = {}
|
||||
__and_cache: ClassVar[dict[tuple[str, str], "_ANDState"]] = {}
|
||||
__xor_cache: ClassVar[dict[tuple[str, str], "_XORState"]] = {}
|
||||
|
||||
IDLE: "State"
|
||||
"""Default State for all Finite State Machines"""
|
||||
ANY: "State"
|
||||
"""Special State that matches any other State. Useful to define fallback behavior.
|
||||
*Not* supported in ``set_state`` method of FSMs.
|
||||
"""
|
||||
|
||||
def __init__(self, uid: Optional[str] = None):
|
||||
effective_uid = uid or uuid4().hex
|
||||
if effective_uid in self.__knows_uids:
|
||||
raise ValueError(f"Duplicate UID: {effective_uid} already registered")
|
||||
self._uid = effective_uid
|
||||
self.__knows_uids.add(effective_uid)
|
||||
|
||||
def __invert__(self) -> "_NOTState":
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__not_cache[self.uid]
|
||||
return self.__not_cache.setdefault(self.uid, _NOTState(self))
|
||||
|
||||
def __or__(self, other: "State") -> "_ORState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__or_cache[key]
|
||||
return self.__or_cache.setdefault(key, _ORState(self, other))
|
||||
|
||||
def __and__(self, other: "State") -> "_ANDState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__and_cache[key]
|
||||
return self.__and_cache.setdefault(key, _ANDState(self, other))
|
||||
|
||||
def __xor__(self, other: "State") -> "_XORState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__xor_cache[key]
|
||||
return self.__xor_cache.setdefault(key, _XORState(self, other))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}: {self.uid}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.uid
|
||||
|
||||
@property
|
||||
def uid(self) -> str:
|
||||
return self._uid
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
if isinstance(state, (_NOTState, _ANDState, _ORState, _XORState)):
|
||||
return state.matches(self)
|
||||
return self.uid == state.uid
|
||||
|
||||
|
||||
class _AnyState(State):
|
||||
def matches(self, state: "State") -> bool: # noqa: ARG002
|
||||
return True
|
||||
|
||||
|
||||
State.IDLE = State("IDLE")
|
||||
State.ANY = _AnyState("ANY")
|
||||
|
||||
|
||||
class _XORState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})^({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) ^ self._state_two.matches(state)
|
||||
|
||||
|
||||
class _ORState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})|({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) or self._state_two.matches(state)
|
||||
|
||||
|
||||
class _ANDState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})&({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) and self._state_two.matches(state)
|
||||
|
||||
|
||||
class _NOTState(State):
|
||||
def __init__(self, state: State):
|
||||
super().__init__(uid=f"!({state.uid})")
|
||||
self._state = state
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return not self._state.matches(state)
|
||||
@@ -97,7 +97,7 @@ class JobQueue(Generic[CCT]):
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_application", "_executor", "scheduler")
|
||||
__slots__ = ("__weakref__", "_application", "_executor", "scheduler")
|
||||
_CRON_MAPPING = ("sun", "mon", "tue", "wed", "thu", "fri", "sat")
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -205,7 +205,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
async def start_polling(
|
||||
self,
|
||||
poll_interval: float = 0.0,
|
||||
timeout: int = 10, # noqa: ASYNC109
|
||||
timeout: int = 10,
|
||||
bootstrap_retries: int = -1,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -341,7 +341,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
async def _start_polling(
|
||||
self,
|
||||
poll_interval: float,
|
||||
timeout: int, # noqa: ASYNC109
|
||||
timeout: int,
|
||||
read_timeout: ODVInput[float],
|
||||
write_timeout: ODVInput[float],
|
||||
connect_timeout: ODVInput[float],
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <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 helper functions related to the std-lib asyncio module.
|
||||
|
||||
.. versionadded:: NEXT.VERSION
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class TrackedBoundedSemaphore(asyncio.BoundedSemaphore):
|
||||
"""Simple subclass of :class:`asyncio.BoundedSemaphore` that tracks the current value of the
|
||||
semaphore. While there is an attribute ``_value`` in the superclass, it's private and we
|
||||
don't want to rely on it.
|
||||
"""
|
||||
|
||||
__slots__ = ("_current_value",)
|
||||
|
||||
def __init__(self, value: int = 1) -> None:
|
||||
super().__init__(value)
|
||||
self._current_value = value
|
||||
|
||||
@property
|
||||
def current_value(self) -> int:
|
||||
return self._current_value
|
||||
|
||||
async def acquire(self) -> Literal[True]:
|
||||
await super().acquire()
|
||||
self._current_value -= 1
|
||||
return True
|
||||
|
||||
def release(self) -> None:
|
||||
super().release()
|
||||
self._current_value += 1
|
||||
@@ -115,6 +115,14 @@ class RequestParameter:
|
||||
"""
|
||||
if isinstance(value, dtm.datetime):
|
||||
return to_timestamp(value), []
|
||||
if isinstance(value, dtm.timedelta):
|
||||
seconds = value.total_seconds()
|
||||
# We convert to int for completeness for whole seconds
|
||||
if seconds.is_integer():
|
||||
return int(seconds), []
|
||||
# The Bot API doesn't document behavior for fractions of seconds so far, but we don't
|
||||
# want to silently drop them
|
||||
return seconds, []
|
||||
if isinstance(value, StringEnum):
|
||||
return value.value, []
|
||||
if isinstance(value, InputFile):
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import asyncio
|
||||
import datetime as dtm
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -138,7 +139,9 @@ class TestAnimationWithoutRequest(AnimationTestBase):
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("local_mode", [True, False])
|
||||
async def test_send_animation_local_files(self, monkeypatch, offline_bot, chat_id, local_mode):
|
||||
async def test_send_animation_local_files(
|
||||
self, monkeypatch, offline_bot, chat_id, local_mode, dummy_message_dict
|
||||
):
|
||||
try:
|
||||
offline_bot._local_mode = local_mode
|
||||
# For just test that the correct paths are passed as we have no local Bot API set up
|
||||
@@ -156,6 +159,7 @@ class TestAnimationWithoutRequest(AnimationTestBase):
|
||||
test_flag = isinstance(data.get("animation"), InputFile) and isinstance(
|
||||
data.get("thumbnail"), InputFile
|
||||
)
|
||||
return dummy_message_dict
|
||||
|
||||
monkeypatch.setattr(offline_bot, "_post", make_assertion)
|
||||
await offline_bot.send_animation(chat_id, file, thumbnail=file)
|
||||
@@ -210,11 +214,14 @@ class TestAnimationWithoutRequest(AnimationTestBase):
|
||||
|
||||
|
||||
class TestAnimationWithRequest(AnimationTestBase):
|
||||
async def test_send_all_args(self, bot, chat_id, animation_file, animation, thumb_file):
|
||||
@pytest.mark.parametrize("duration", [1, dtm.timedelta(seconds=1)])
|
||||
async def test_send_all_args(
|
||||
self, bot, chat_id, animation_file, animation, thumb_file, duration
|
||||
):
|
||||
message = await bot.send_animation(
|
||||
chat_id,
|
||||
animation_file,
|
||||
duration=self.duration,
|
||||
duration=duration,
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
caption=self.caption,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user