Compare commits

...

22 Commits

Author SHA1 Message Date
Hinrich Mahler b1fff6d90a Use Lock instead of semaphore 2025-02-06 12:05:20 +01:00
Hinrich Mahler 31af1a9db8 Add an example on concurrency in FSM 2025-02-06 11:57:18 +01:00
Hinrich Mahler 4441543043 Try setting up infrastructure for optimistic locking. Example will follow 2025-02-05 23:40:15 +01:00
Hinrich Mahler 646ba37391 Move internal state storage to FiniteStateMachine and add state history 2025-02-05 13:13:21 +01:00
Hinrich Mahler 817b71d914 Disable tests harder … 2025-02-05 12:26:02 +01:00
Hinrich Mahler 434cbfade8 Add Some Abstractions for Timeout Jobs 2025-02-05 12:22:07 +01:00
Hinrich Mahler 34832d9db9 Temporarily Disable Tests on this branch 2025-02-05 11:17:47 +01:00
Hinrich Mahler 07225b9a02 Add State.ANY for fallbacks and allow handling multiple states for one update 2025-02-05 10:52:10 +01:00
Hinrich Mahler 0c06ba0a90 Initial FSM PoC 2025-02-04 21:36:40 +01:00
Bibo-Joshi dfb0ae3747 Use Fine Grained Permissions for GitHub Actions Workflows (#4668) 2025-02-02 10:24:46 +01:00
dependabot[bot] 64006aa7ae Bump actions/setup-python from 5.3.0 to 5.4.0 (#4665)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2025-02-02 09:52:35 +01:00
dependabot[bot] 69ddc47a6e Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 (#4666)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-02 09:34:10 +01:00
Bibo-Joshi a2150b3751 Accept datetime.timedelta Input in Bot Method Parameters (#4651) 2025-02-02 09:31:18 +01:00
dependabot[bot] 79acc1ae53 Bump actions/stale from 9.0.0 to 9.1.0 (#4667)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-02 09:22:47 +01:00
dependabot[bot] 4cdb1a0cf7 Bump astral-sh/setup-uv from 5.1.0 to 5.2.2 (#4664)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-02 09:09:55 +01:00
dependabot[bot] 6319f4bae1 Bump codecov/test-results-action from 1.0.1 to 1.0.2 (#4663)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-02 09:09:13 +01:00
Harshil d7e063dbad Overhaul Admonition Insertion in Documentation (#4462)
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2025-01-31 19:23:09 +01:00
Bibo-Joshi 5dd7b8f1e2 Extend Customization Support for Bot.base_(file_)url (#4632) 2025-01-23 06:01:27 +01:00
Bibo-Joshi 61b87ba318 Support allow_paid_broadcast in AIORateLimiter (#4627) 2025-01-23 05:59:39 +01:00
Bibo-Joshi dd592cdd7c Simplify Handling of Empty Data in TelegramObject.de_json and Friends (#4617) 2025-01-14 17:12:55 +01:00
Bibo-Joshi f57dd52100 Add BaseUpdateProcessor.current_concurrent_updates (#4626) 2025-01-14 17:00:20 +01:00
pre-commit-ci[bot] 16605c54d7 Bump pre-commit Hooks to Latest Versions (#4643)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2025-01-07 18:11:35 +01:00
156 changed files with 2633 additions and 1764 deletions
+3 -1
View File
@@ -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:
-32
View File
@@ -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
-48
View File
@@ -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
+4 -2
View File
@@ -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
+2
View File
@@ -4,6 +4,8 @@ on:
pull_request:
types: [opened]
permissions: {}
jobs:
pre-commit-ci:
permissions:
+6
View File
@@ -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:
+9 -1
View File
@@ -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
+9 -1
View File
@@ -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
+6 -1
View File
@@ -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
-49
View File
@@ -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
-21
View File
@@ -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
+1
View File
@@ -67,6 +67,7 @@ docs/_build/
# PyBuilder
target/
.idea/
.run/
# Sublime Text 2
*.sublime*
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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;
}
+3 -1
View File
@@ -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.
+203
View File
@@ -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()
+172
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+1 -6
View File
@@ -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
View File
@@ -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)
+7 -11
View File
@@ -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
View File
@@ -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,
*,
+7 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+3 -7
View File
@@ -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)
+5 -9
View File
@@ -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)
+3 -7
View File
@@ -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)
+4 -8
View File
@@ -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
+7 -11
View File
@@ -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)
+1 -6
View File
@@ -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
+4 -8
View File
@@ -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)
+4 -6
View File
@@ -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
+9 -15
View File
@@ -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
+3 -5
View File
@@ -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)
+5 -8
View File
@@ -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)
+3 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+8 -12
View File
@@ -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)
+1 -5
View File
@@ -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"]:
+6 -10
View File
@@ -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,
+3 -6
View File
@@ -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)
+1 -6
View File
@@ -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
View File
@@ -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
+6 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+3 -7
View File
@@ -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)
+5 -9
View File
@@ -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)
+10 -20
View File
@@ -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
View File
@@ -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)
+27 -40
View File
@@ -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 -25
View File
@@ -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
)
+4 -9
View File
@@ -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)
+4 -11
View File
@@ -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(
+5 -7
View 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)
+4 -8
View File
@@ -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)
+6 -8
View File
@@ -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)
+4 -8
View File
@@ -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)
+6 -16
View File
@@ -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)
+13 -31
View File
@@ -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]
+3 -7
View File
@@ -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
View File
@@ -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)
+4 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+7 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+1 -6
View File
@@ -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)
+80 -2
View File
@@ -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)
+6 -1
View File
@@ -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
View File
@@ -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)
+1 -6
View File
@@ -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)
+5
View File
@@ -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
+42 -20
View File
@@ -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:
+53 -17
View File
@@ -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
+23 -7
View File
@@ -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.
+15 -2
View File
@@ -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,
+23 -3
View File
@@ -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
View File
@@ -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,
*,
+6
View File
@@ -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
+200
View File
@@ -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
+114
View File
@@ -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)
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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],
+55
View File
@@ -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
+8
View File
@@ -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):
+10 -3
View File
@@ -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