Merge branch 'master' into dependabot/pip/apscheduler-gte-3.10.4-and-lt-3.12.0

# Conflicts:
#	telegram/ext/_jobqueue.py
#	tests/auxil/bot_method_checks.py
#	tests/conftest.py
This commit is contained in:
Hinrich Mahler 2024-12-30 19:18:42 +01:00
commit 6e37a386f3
93 changed files with 2543 additions and 2031 deletions

View file

@ -16,14 +16,15 @@ jobs:
- name: Fetch Dependabot metadata
id: dependabot-metadata
uses: dependabot/fetch-metadata@v2.2.0
uses: dependabot/fetch-metadata@dbb049abf0d677abbd7f7eee0375145b417fdd34 # v2.2.0
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- name: Update Version Number in Other Files
uses: jacobtomlinson/gha-find-replace@v3
uses: jacobtomlinson/gha-find-replace@f1069b438f125e5395d84d1c6fd3b559a7880cb5 # v3
with:
find: ${{ steps.dependabot-metadata.outputs.previous-version }}
replace: ${{ steps.dependabot-metadata.outputs.new-version }}
@ -31,7 +32,7 @@ jobs:
exclude: CHANGES.rst
- name: Commit & Push Changes to PR
uses: EndBug/add-and-commit@v9.1.4
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
with:
message: 'Update version number in other files'
committer_name: GitHub Actions

View file

@ -17,9 +17,11 @@ jobs:
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies

View file

@ -18,9 +18,11 @@ jobs:
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@ -34,7 +36,7 @@ jobs:
- name: Build docs
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto
- name: Upload docs
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: HTML Docs
retention-days: 7

31
.github/workflows/gha_security.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: GitHub Actions Security Analysis
on:
push:
branches:
- master
pull_request:
jobs:
zizmor:
name: Security Analysis with zizmor
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0
- name: Run zizmor
run: uvx zizmor --persona=pedantic --format sarif . > results.sarif
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
with:
sarif_file: results.sarif
category: zizmor

View file

@ -11,7 +11,7 @@ jobs:
pull-requests: write # for srvaroa/labeler to add labels in PR
runs-on: ubuntu-latest
steps:
- uses: srvaroa/labeler@v1.12.0
- uses: srvaroa/labeler@fe4b1c73bb8abf2f14a44a6912a8b4fee835d631 # v1.12.0
# Config file at .github/labeler.yml
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View file

@ -8,7 +8,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5.0.1
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
with:
github-token: ${{ github.token }}
issue-inactive-days: '7'

View file

@ -12,9 +12,11 @@ jobs:
TAG: ${{ steps.get_tag.outputs.TAG }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.x"
- name: Install pypa/build
@ -23,7 +25,7 @@ jobs:
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: python-package-distributions
path: dist/
@ -47,12 +49,12 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
compute-signatures:
name: Compute SHA1 Sums and Sign with Sigstore
@ -65,7 +67,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: python-package-distributions
path: dist/
@ -77,13 +79,13 @@ jobs:
sha1sum $file > $file.sha1
done
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Store the distribution packages and signatures
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: python-package-distributions-and-signatures
path: dist/
@ -101,7 +103,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: python-package-distributions-and-signatures
path: dist/
@ -113,7 +115,7 @@ jobs:
# we don't define it through this workflow.
run: >-
gh release create
'${{ env.TAG }}'
"$TAG"
--repo '${{ github.repository }}'
--generate-notes
- name: Upload artifact signatures to GitHub Release
@ -125,5 +127,5 @@ jobs:
# sigstore-produced signatures and certificates.
run: >-
gh release upload
'${{ env.TAG }}' dist/**
"$TAG" dist/**
--repo '${{ github.repository }}'

View file

@ -12,9 +12,11 @@ jobs:
TAG: ${{ steps.get_tag.outputs.TAG }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.x"
- name: Install pypa/build
@ -23,7 +25,7 @@ jobs:
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: python-package-distributions
path: dist/
@ -47,12 +49,12 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: python-package-distributions
path: dist/
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
with:
repository-url: https://test.pypi.org/legacy/
@ -67,7 +69,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: python-package-distributions
path: dist/
@ -79,13 +81,13 @@ jobs:
sha1sum $file > $file.sha1
done
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Store the distribution packages and signatures
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: python-package-distributions-and-signatures
path: dist/
@ -103,7 +105,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: python-package-distributions-and-signatures
path: dist/
@ -115,7 +117,7 @@ jobs:
# we don't define it through this workflow.
run: >-
gh release create
'${{ env.TAG }}'
"$TAG"
--repo '${{ github.repository }}'
--generate-notes
--draft
@ -128,5 +130,5 @@ jobs:
# sigstore-produced signatures and certificates.
run: >-
gh release upload
'${{ env.TAG }}' dist/**
"$TAG" dist/**
--repo '${{ github.repository }}'

View file

@ -7,7 +7,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
# PRs never get stale
days-before-stale: 3

View file

@ -21,9 +21,11 @@ jobs:
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@ -41,7 +43,7 @@ jobs:
- name: Test Summary
id: test_summary
uses: test-summary/action@v2.4
uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
if: always() # always run, even if tests fail
with:
paths: .test_report_official.xml

View file

@ -14,7 +14,7 @@ jobs:
name: test-type-completeness
runs-on: ubuntu-latest
steps:
- uses: Bibo-Joshi/pyright-type-completeness@1.0.1
- uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1
with:
package-name: telegram
python-version: 3.12

View file

@ -9,14 +9,14 @@ jobs:
name: test-type-completeness
runs-on: ubuntu-latest
steps:
- uses: Bibo-Joshi/pyright-type-completeness@1.0.1
- uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1
id: pyright-type-completeness
with:
package-name: telegram
python-version: 3.12
pyright-version: ~=1.1.367
- name: Check Output
uses: jannekem/run-python-script-action@v1
uses: jannekem/run-python-script-action@bbfca66c612a28f3eeca0ae40e1f810265e2ea68 # v1.7
env:
TYPE_COMPLETENESS: ${{ steps.pyright-type-completeness.outputs.base-completeness-score }}
with:

View file

@ -24,9 +24,11 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: False
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@ -79,7 +81,7 @@ jobs:
- name: Test Summary
id: test_summary
uses: test-summary/action@v2.4
uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
if: always() # always run, even if tests fail
with:
paths: |
@ -87,14 +89,14 @@ jobs:
.test_report_optionals_junit.xml
- name: Submit coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1
with:
env_vars: OS,PYTHON
name: ${{ matrix.os }}-${{ matrix.python-version }}
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
uses: codecov/test-results-action@9739113ad922ea0a9abb4b2c0f8bf6a4aa8ef820 # v1.0.1
if: ${{ !cancelled() }}
with:
files: .test_report_no_optionals_junit.xml,.test_report_optionals_junit.xml

View file

@ -31,6 +31,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Ambro17 <https://github.com/Ambro17>`_
- `Andrej Zhilenkov <https://github.com/Andrej730>`_
- `Anton Tagunov <https://github.com/anton-tagunov>`_
- `Anya Marcano <https://github.com/AnyaMarcanito>`
- `Avanatiker <https://github.com/Avanatiker>`_
- `Balduro <https://github.com/Balduro>`_
- `Bibo-Joshi <https://github.com/Bibo-Joshi>`_
@ -58,12 +59,14 @@ The following wonderful people contributed directly or indirectly to this projec
- `gamgi <https://github.com/gamgi>`_
- `Gauthamram Ravichandran <https://github.com/GauthamramRavichandran>`_
- `Harshil <https://github.com/harshil21>`_
- `Henry Galue <https://github.com/henryg311>`
- `Hugo Damer <https://github.com/HakimusGIT>`_
- `ihoru <https://github.com/ihoru>`_
- `Iulian Onofrei <https://github.com/revolter>`_
- `Jainam Oswal <https://github.com/jainamoswal>`_
- `Jasmin Bom <https://github.com/jsmnbom>`_
- `JASON0916 <https://github.com/JASON0916>`_
- `Jeamhowards Montiel <https://github.com/Jeam-zx>`
- `jeffffc <https://github.com/jeffffc>`_
- `Jelle Besseling <https://github.com/pingiun>`_
- `jh0ker <https://github.com/jh0ker>`_
@ -72,6 +75,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Joscha Götzer <https://github.com/Rostgnom>`_
- `jossalgon <https://github.com/jossalgon>`_
- `JRoot3D <https://github.com/JRoot3D>`_
- `Juan Cuevas <https://github.com/cuevasrja>`
- `kenjitagawa <https://github.com/kenjitagawa>`_
- `kennethcheo <https://github.com/kennethcheo>`_
- `Kirill Vasin <https://github.com/vasinkd>`_
@ -87,6 +91,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Michael Dix <https://github.com/Eisberge>`_
- `Michael Elovskikh <https://github.com/wronglink>`_
- `Miguel C. R. <https://github.com/MiguelX413>`_
- `Miguel Salomon <https://github.com/Migueldsc12>`
- `miles <https://github.com/miles170>`_
- `Mischa Krüger <https://github.com/Makman2>`_
- `Mohd Yusuf <https://github.com/mohdyusuf2312>`_

View file

@ -4,6 +4,33 @@
Changelog
=========
Version 21.9
============
*Released 2024-12-07*
This is the technical changelog for version 21.9. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
Major Changes
-------------
- Full Support for Bot API 8.1 (:pr:`4594` closes :issue:`4592`)
Minor Changes
-------------
- Use ``MessageLimit.DEEP_LINK_LENGTH`` in ``helpers.create_deep_linked_url`` (:pr:`4597` by `nemacysts <https://github.com/nemacysts>`_)
- Allow ``Sequence`` Input for ``allowed_updates`` in ``Application`` and ``Updater`` Methods (:pr:`4589` by `nemacysts <https://github.com/nemacysts>`_)
Dependency Updates
------------------
- Update ``aiolimiter`` requirement from ~=1.1.0 to >=1.1,<1.3 (:pr:`4595`)
- Bump ``pytest`` from 8.3.3 to 8.3.4 (:pr:`4596`)
- Bump ``codecov/codecov-action`` from 4 to 5 (:pr:`4585`)
- Bump ``pylint`` to v3.3.2 to Improve Python 3.13 Support (:pr:`4590` by `nemacysts <https://github.com/nemacysts>`_)
- Bump ``srvaroa/labeler`` from 1.11.1 to 1.12.0 (:pr:`4586`)
Version 21.8
============
*Released 2024-12-01*

View file

@ -15,7 +15,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 datetime
import datetime as dtm
from enum import Enum
from docutils.nodes import Element
@ -75,7 +75,7 @@ class TGConstXRefRole(PyXRefRole):
):
return str(value), target
if (
isinstance(value, datetime.datetime)
isinstance(value, dtm.datetime)
and value == telegram.constants.ZERO_DATE
and target in ("telegram.constants.ZERO_DATE",)
):

View file

@ -179,8 +179,8 @@ markers = [
"req",
]
asyncio_mode = "auto"
log_format = "%(funcName)s - Line %(lineno)d - %(message)s"
# log_level = "DEBUG" # uncomment to see DEBUG logs
log_cli_format = "%(funcName)s - Line %(lineno)d - %(message)s"
# log_cli_level = "DEBUG" # uncomment to see DEBUG logs
# MYPY:
[tool.mypy]

View file

@ -271,6 +271,17 @@ __all__ = (
"warnings",
)
from telegram._payment.stars.startransactions import StarTransaction, StarTransactions
from telegram._payment.stars.transactionpartner import (
TransactionPartner,
TransactionPartnerAffiliateProgram,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
TransactionPartnerTelegramApi,
TransactionPartnerUser,
)
from . import _version, constants, error, helpers, request, warnings
from ._birthdate import Birthdate
from ._bot import Bot
@ -470,21 +481,12 @@ from ._payment.refundedpayment import RefundedPayment
from ._payment.shippingaddress import ShippingAddress
from ._payment.shippingoption import ShippingOption
from ._payment.shippingquery import ShippingQuery
from ._payment.stars import (
AffiliateInfo,
from ._payment.stars.affiliateinfo import AffiliateInfo
from ._payment.stars.revenuewithdrawalstate import (
RevenueWithdrawalState,
RevenueWithdrawalStateFailed,
RevenueWithdrawalStatePending,
RevenueWithdrawalStateSucceeded,
StarTransaction,
StarTransactions,
TransactionPartner,
TransactionPartnerAffiliateProgram,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
TransactionPartnerTelegramApi,
TransactionPartnerUser,
)
from ._payment.successfulpayment import SuccessfulPayment
from ._poll import InputPollOption, Poll, PollAnswer, PollOption

View file

@ -17,7 +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/].
"""This module contains an object that represents a Telegram Birthday."""
from datetime import date
import datetime as dtm
from typing import Optional
from telegram._telegramobject import TelegramObject
@ -70,7 +70,7 @@ class Birthdate(TelegramObject):
self._freeze()
def to_date(self, year: Optional[int] = None) -> date:
def to_date(self, year: Optional[int] = None) -> dtm.date:
"""Return the birthdate as a date object.
.. versionchanged:: 21.2
@ -89,4 +89,4 @@ class Birthdate(TelegramObject):
"The `year` argument is required if the `year` attribute was not present."
)
return date(year or self.year, self.month, self.day) # type: ignore[arg-type]
return dtm.date(year or self.year, self.month, self.day) # type: ignore[arg-type]

View file

@ -22,9 +22,9 @@
import asyncio
import contextlib
import copy
import datetime as dtm
import pickle
from collections.abc import Sequence
from datetime import datetime, timedelta
from types import TracebackType
from typing import (
TYPE_CHECKING,
@ -81,7 +81,7 @@ from telegram._inline.preparedinlinemessage import PreparedInlineMessage
from telegram._menubutton import MenuButton
from telegram._message import Message
from telegram._messageid import MessageId
from telegram._payment.stars import StarTransactions
from telegram._payment.stars.startransactions import StarTransactions
from telegram._poll import InputPollOption, Poll
from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
from telegram._reply import ReplyParameters
@ -3815,7 +3815,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
self,
chat_id: Union[str, int],
user_id: int,
until_date: Optional[Union[int, datetime]] = None,
until_date: Optional[Union[int, dtm.datetime]] = None,
revoke_messages: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -5451,7 +5451,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
chat_id: Union[str, int],
user_id: int,
permissions: ChatPermissions,
until_date: Optional[Union[int, datetime]] = None,
until_date: Optional[Union[int, dtm.datetime]] = None,
use_independent_chat_permissions: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -5788,7 +5788,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
async def create_chat_invite_link(
self,
chat_id: Union[str, int],
expire_date: Optional[Union[int, datetime]] = None,
expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@ -5864,7 +5864,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
self,
chat_id: Union[str, int],
invite_link: Union[str, "ChatInviteLink"],
expire_date: Optional[Union[int, datetime]] = None,
expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@ -6233,7 +6233,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
self,
user_id: int,
emoji_status_custom_emoji_id: Optional[str] = None,
emoji_status_expiration_date: Optional[Union[int, datetime]] = None,
emoji_status_expiration_date: Optional[Union[int, dtm.datetime]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6622,7 +6622,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def set_sticker_position_in_set(
self,
sticker: str,
sticker: Union[str, "Sticker"],
position: int,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6634,7 +6634,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"""Use this method to move a sticker in a set created by the bot to a specific position.
Args:
sticker (:obj:`str`): File identifier of the sticker.
sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
position (:obj:`int`): New sticker position in the set, zero-based.
Returns:
@ -6644,7 +6648,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"sticker": sticker, "position": position}
data: JSONDict = {
"sticker": sticker if isinstance(sticker, str) else sticker.file_id,
"position": position,
}
return await self._post(
"setStickerPositionInSet",
data,
@ -6749,7 +6756,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def delete_sticker_from_set(
self,
sticker: str,
sticker: Union[str, "Sticker"],
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6760,7 +6767,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"""Use this method to delete a sticker from a set created by the bot.
Args:
sticker (:obj:`str`): File identifier of the sticker.
sticker (:obj:`str` | :class:`telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@ -6769,7 +6780,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"sticker": sticker}
data: JSONDict = {"sticker": sticker if isinstance(sticker, str) else sticker.file_id}
return await self._post(
"deleteStickerFromSet",
data,
@ -6937,7 +6948,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def set_sticker_emoji_list(
self,
sticker: str,
sticker: Union[str, "Sticker"],
emoji_list: Sequence[str],
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6953,7 +6964,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 20.2
Args:
sticker (:obj:`str`): File identifier of the sticker.
sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
emoji_list (Sequence[:obj:`str`]): A sequence of
:tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI`-
:tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with
@ -6965,7 +6980,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"sticker": sticker, "emoji_list": emoji_list}
data: JSONDict = {
"sticker": sticker if isinstance(sticker, str) else sticker.file_id,
"emoji_list": emoji_list,
}
return await self._post(
"setStickerEmojiList",
data,
@ -6978,7 +6996,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def set_sticker_keywords(
self,
sticker: str,
sticker: Union[str, "Sticker"],
keywords: Optional[Sequence[str]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6994,7 +7012,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 20.2
Args:
sticker (:obj:`str`): File identifier of the sticker.
sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
keywords (Sequence[:obj:`str`]): A sequence of
0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords
for the sticker with total length up to
@ -7006,7 +7028,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"sticker": sticker, "keywords": keywords}
data: JSONDict = {
"sticker": sticker if isinstance(sticker, str) else sticker.file_id,
"keywords": keywords,
}
return await self._post(
"setStickerKeywords",
data,
@ -7019,7 +7044,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def set_sticker_mask_position(
self,
sticker: str,
sticker: Union[str, "Sticker"],
mask_position: Optional[MaskPosition] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -7035,7 +7060,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 20.2
Args:
sticker (:obj:`str`): File identifier of the sticker.
sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
mask_position (:class:`telegram.MaskPosition`, optional): A object with the position
where the mask should be placed on faces. Omit the parameter to remove the mask
position.
@ -7046,7 +7075,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"sticker": sticker, "mask_position": mask_position}
data: JSONDict = {
"sticker": sticker if isinstance(sticker, str) else sticker.file_id,
"mask_position": mask_position,
}
return await self._post(
"setStickerMaskPosition",
data,
@ -7159,7 +7191,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
explanation: Optional[str] = None,
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
open_period: Optional[int] = None,
close_date: Optional[Union[int, datetime]] = None,
close_date: Optional[Union[int, dtm.datetime]] = None,
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
@ -8125,7 +8157,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, timedelta]] = None,
subscription_period: Optional[Union[int, dtm.timedelta]] = None,
business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -8248,7 +8280,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"send_email_to_provider": send_email_to_provider,
"subscription_period": (
subscription_period.total_seconds()
if isinstance(subscription_period, timedelta)
if isinstance(subscription_period, dtm.timedelta)
else subscription_period
),
"business_connection_id": business_connection_id,
@ -9248,7 +9280,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
self,
user_id: int,
name: str,
old_sticker: str,
old_sticker: Union[str, "Sticker"],
sticker: "InputSticker",
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -9266,7 +9298,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Args:
user_id (:obj:`int`): User identifier of the sticker set owner.
name (:obj:`str`): Sticker set name.
old_sticker (:obj:`str`): File identifier of the replaced sticker.
old_sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the replaced
sticker or the sticker object itself.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
sticker (:class:`telegram.InputSticker`): An object with information about the added
sticker. If exactly the same sticker had already been added to the set, then the
set remains unchanged.
@ -9280,7 +9316,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
data: JSONDict = {
"user_id": user_id,
"name": name,
"old_sticker": old_sticker,
"old_sticker": old_sticker if isinstance(old_sticker, str) else old_sticker.file_id,
"sticker": sticker,
}

View file

@ -18,9 +18,8 @@
# 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 Telegram Business related classes."""
import datetime as dtm
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from telegram._chat import Chat
@ -81,7 +80,7 @@ class BusinessConnection(TelegramObject):
id: str,
user: "User",
user_chat_id: int,
date: datetime,
date: dtm.datetime,
can_reply: bool,
is_enabled: bool,
*,
@ -91,7 +90,7 @@ class BusinessConnection(TelegramObject):
self.id: str = id
self.user: User = user
self.user_chat_id: int = user_chat_id
self.date: datetime = date
self.date: dtm.datetime = date
self.can_reply: bool = can_reply
self.is_enabled: bool = is_enabled

View file

@ -18,8 +18,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Chat."""
import datetime as dtm
from collections.abc import Sequence
from datetime import datetime
from html import escape
from typing import TYPE_CHECKING, Final, Optional, Union
@ -384,7 +384,7 @@ class _ChatBase(TelegramObject):
self,
user_id: int,
revoke_messages: Optional[bool] = None,
until_date: Optional[Union[int, datetime]] = None,
until_date: Optional[Union[int, dtm.datetime]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -656,7 +656,7 @@ class _ChatBase(TelegramObject):
self,
user_id: int,
permissions: ChatPermissions,
until_date: Optional[Union[int, datetime]] = None,
until_date: Optional[Union[int, dtm.datetime]] = None,
use_independent_chat_permissions: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -2116,7 +2116,7 @@ class _ChatBase(TelegramObject):
explanation: Optional[str] = None,
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
open_period: Optional[int] = None,
close_date: Optional[Union[int, datetime]] = None,
close_date: Optional[Union[int, dtm.datetime]] = None,
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
@ -2588,7 +2588,7 @@ class _ChatBase(TelegramObject):
async def create_invite_link(
self,
expire_date: Optional[Union[int, datetime]] = None,
expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@ -2632,7 +2632,7 @@ class _ChatBase(TelegramObject):
async def edit_invite_link(
self,
invite_link: Union[str, "ChatInviteLink"],
expire_date: Optional[Union[int, datetime]] = None,
expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,

View file

@ -17,9 +17,8 @@
# 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 classes that represent Telegram ChatBoosts."""
import datetime as dtm
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
@ -274,8 +273,8 @@ class ChatBoost(TelegramObject):
def __init__(
self,
boost_id: str,
add_date: datetime,
expiration_date: datetime,
add_date: dtm.datetime,
expiration_date: dtm.datetime,
source: ChatBoostSource,
*,
api_kwargs: Optional[JSONDict] = None,
@ -283,8 +282,8 @@ class ChatBoost(TelegramObject):
super().__init__(api_kwargs=api_kwargs)
self.boost_id: str = boost_id
self.add_date: datetime = add_date
self.expiration_date: datetime = expiration_date
self.add_date: dtm.datetime = add_date
self.expiration_date: dtm.datetime = expiration_date
self.source: ChatBoostSource = source
self._id_attrs = (self.boost_id, self.add_date, self.expiration_date, self.source)
@ -386,7 +385,7 @@ class ChatBoostRemoved(TelegramObject):
self,
chat: Chat,
boost_id: str,
remove_date: datetime,
remove_date: dtm.datetime,
source: ChatBoostSource,
*,
api_kwargs: Optional[JSONDict] = None,
@ -395,7 +394,7 @@ class ChatBoostRemoved(TelegramObject):
self.chat: Chat = chat
self.boost_id: str = boost_id
self.remove_date: datetime = remove_date
self.remove_date: dtm.datetime = remove_date
self.source: ChatBoostSource = source
self._id_attrs = (self.chat, self.boost_id, self.remove_date, self.source)

View file

@ -18,8 +18,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatFullInfo."""
import datetime as dtm
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from telegram._birthdate import Birthdate
@ -422,7 +422,7 @@ class ChatFullInfo(_ChatBase):
profile_accent_color_id: Optional[int] = None,
profile_background_custom_emoji_id: Optional[str] = None,
emoji_status_custom_emoji_id: Optional[str] = None,
emoji_status_expiration_date: Optional[datetime] = None,
emoji_status_expiration_date: Optional[dtm.datetime] = None,
bio: Optional[str] = None,
has_private_forwards: Optional[bool] = None,
has_restricted_voice_and_video_messages: Optional[bool] = None,
@ -486,7 +486,9 @@ class ChatFullInfo(_ChatBase):
)
self.active_usernames: tuple[str, ...] = parse_sequence_arg(active_usernames)
self.emoji_status_custom_emoji_id: Optional[str] = emoji_status_custom_emoji_id
self.emoji_status_expiration_date: Optional[datetime] = emoji_status_expiration_date
self.emoji_status_expiration_date: Optional[dtm.datetime] = (
emoji_status_expiration_date
)
self.has_aggressive_anti_spam_enabled: Optional[bool] = (
has_aggressive_anti_spam_enabled
)

View file

@ -17,7 +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/].
"""This module contains an object that represents an invite link for a chat."""
import datetime
import datetime as dtm
from typing import TYPE_CHECKING, Optional
from telegram._telegramobject import TelegramObject
@ -139,7 +139,7 @@ class ChatInviteLink(TelegramObject):
creates_join_request: bool,
is_primary: bool,
is_revoked: bool,
expire_date: Optional[datetime.datetime] = None,
expire_date: Optional[dtm.datetime] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
pending_join_request_count: Optional[int] = None,
@ -157,7 +157,7 @@ class ChatInviteLink(TelegramObject):
self.is_revoked: bool = is_revoked
# Optionals
self.expire_date: Optional[datetime.datetime] = expire_date
self.expire_date: Optional[dtm.datetime] = expire_date
self.member_limit: Optional[int] = member_limit
self.name: Optional[str] = name
self.pending_join_request_count: Optional[int] = (

View file

@ -17,7 +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/].
"""This module contains an object that represents a Telegram ChatJoinRequest."""
import datetime
import datetime as dtm
from typing import TYPE_CHECKING, Optional
from telegram._chat import Chat
@ -106,7 +106,7 @@ class ChatJoinRequest(TelegramObject):
self,
chat: Chat,
from_user: User,
date: datetime.datetime,
date: dtm.datetime,
user_chat_id: int,
bio: Optional[str] = None,
invite_link: Optional[ChatInviteLink] = None,
@ -117,7 +117,7 @@ class ChatJoinRequest(TelegramObject):
# Required
self.chat: Chat = chat
self.from_user: User = from_user
self.date: datetime.datetime = date
self.date: dtm.datetime = date
self.user_chat_id: int = user_chat_id
# Optionals

View file

@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatMember."""
import datetime
import datetime as dtm
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
@ -413,13 +413,13 @@ class ChatMemberMember(ChatMember):
def __init__(
self,
user: User,
until_date: Optional[datetime.datetime] = None,
until_date: Optional[dtm.datetime] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(status=ChatMember.MEMBER, user=user, api_kwargs=api_kwargs)
with self._unfrozen():
self.until_date: Optional[datetime.datetime] = until_date
self.until_date: Optional[dtm.datetime] = until_date
class ChatMemberRestricted(ChatMember):
@ -566,7 +566,7 @@ class ChatMemberRestricted(ChatMember):
can_send_other_messages: bool,
can_add_web_page_previews: bool,
can_manage_topics: bool,
until_date: datetime.datetime,
until_date: dtm.datetime,
can_send_audios: bool,
can_send_documents: bool,
can_send_photos: bool,
@ -587,7 +587,7 @@ class ChatMemberRestricted(ChatMember):
self.can_send_other_messages: bool = can_send_other_messages
self.can_add_web_page_previews: bool = can_add_web_page_previews
self.can_manage_topics: bool = can_manage_topics
self.until_date: datetime.datetime = until_date
self.until_date: dtm.datetime = until_date
self.can_send_audios: bool = can_send_audios
self.can_send_documents: bool = can_send_documents
self.can_send_photos: bool = can_send_photos
@ -656,10 +656,10 @@ class ChatMemberBanned(ChatMember):
def __init__(
self,
user: User,
until_date: datetime.datetime,
until_date: dtm.datetime,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(status=ChatMember.BANNED, user=user, api_kwargs=api_kwargs)
with self._unfrozen():
self.until_date: datetime.datetime = until_date
self.until_date: dtm.datetime = until_date

View file

@ -17,7 +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/].
"""This module contains an object that represents a Telegram ChatMemberUpdated."""
import datetime
import datetime as dtm
from typing import TYPE_CHECKING, Optional, Union
from telegram._chat import Chat
@ -108,7 +108,7 @@ class ChatMemberUpdated(TelegramObject):
self,
chat: Chat,
from_user: User,
date: datetime.datetime,
date: dtm.datetime,
old_chat_member: ChatMember,
new_chat_member: ChatMember,
invite_link: Optional[ChatInviteLink] = None,
@ -121,7 +121,7 @@ class ChatMemberUpdated(TelegramObject):
# Required
self.chat: Chat = chat
self.from_user: User = from_user
self.date: datetime.datetime = date
self.date: dtm.datetime = date
self.old_chat_member: ChatMember = old_chat_member
self.new_chat_member: ChatMember = new_chat_member
self.via_chat_folder_invite_link: Optional[bool] = via_chat_folder_invite_link
@ -179,9 +179,7 @@ class ChatMemberUpdated(TelegramObject):
self,
) -> dict[
str,
tuple[
Union[str, bool, datetime.datetime, User], Union[str, bool, datetime.datetime, User]
],
tuple[Union[str, bool, dtm.datetime, User], Union[str, bool, dtm.datetime, User]],
]:
"""Computes the difference between :attr:`old_chat_member` and :attr:`new_chat_member`.

View file

@ -17,7 +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/].
"""This module contains an objects that are related to Telegram giveaways."""
import datetime
import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional
@ -105,7 +105,7 @@ class Giveaway(TelegramObject):
def __init__(
self,
chats: Sequence[Chat],
winners_selection_date: datetime.datetime,
winners_selection_date: dtm.datetime,
winner_count: int,
only_new_members: Optional[bool] = None,
has_public_winners: Optional[bool] = None,
@ -119,7 +119,7 @@ class Giveaway(TelegramObject):
super().__init__(api_kwargs=api_kwargs)
self.chats: tuple[Chat, ...] = tuple(chats)
self.winners_selection_date: datetime.datetime = winners_selection_date
self.winners_selection_date: dtm.datetime = winners_selection_date
self.winner_count: int = winner_count
self.only_new_members: Optional[bool] = only_new_members
self.has_public_winners: Optional[bool] = has_public_winners
@ -260,7 +260,7 @@ class GiveawayWinners(TelegramObject):
self,
chat: Chat,
giveaway_message_id: int,
winners_selection_date: datetime.datetime,
winners_selection_date: dtm.datetime,
winner_count: int,
winners: Sequence[User],
additional_chat_count: Optional[int] = None,
@ -277,7 +277,7 @@ class GiveawayWinners(TelegramObject):
self.chat: Chat = chat
self.giveaway_message_id: int = giveaway_message_id
self.winners_selection_date: datetime.datetime = winners_selection_date
self.winners_selection_date: dtm.datetime = winners_selection_date
self.winner_count: int = winner_count
self.winners: tuple[User, ...] = tuple(winners)
self.additional_chat_count: Optional[int] = additional_chat_count

View file

@ -19,7 +19,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Message."""
import datetime
import datetime as dtm
import re
from collections.abc import Sequence
from html import escape
@ -158,14 +158,14 @@ class MaybeInaccessibleMessage(TelegramObject):
self,
chat: Chat,
message_id: int,
date: datetime.datetime,
date: dtm.datetime,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.chat: Chat = chat
self.message_id: int = message_id
self.date: datetime.datetime = date
self.date: dtm.datetime = date
self._id_attrs = (self.message_id, self.chat)
@ -1024,11 +1024,11 @@ class Message(MaybeInaccessibleMessage):
def __init__(
self,
message_id: int,
date: datetime.datetime,
date: dtm.datetime,
chat: Chat,
from_user: Optional[User] = None,
reply_to_message: Optional["Message"] = None,
edit_date: Optional[datetime.datetime] = None,
edit_date: Optional[dtm.datetime] = None,
text: Optional[str] = None,
entities: Optional[Sequence["MessageEntity"]] = None,
caption_entities: Optional[Sequence["MessageEntity"]] = None,
@ -1119,11 +1119,11 @@ class Message(MaybeInaccessibleMessage):
# Optionals
self.from_user: Optional[User] = from_user
self.sender_chat: Optional[Chat] = sender_chat
self.date: datetime.datetime = date
self.date: dtm.datetime = date
self.chat: Chat = chat
self.is_automatic_forward: Optional[bool] = is_automatic_forward
self.reply_to_message: Optional[Message] = reply_to_message
self.edit_date: Optional[datetime.datetime] = edit_date
self.edit_date: Optional[dtm.datetime] = edit_date
self.has_protected_content: Optional[bool] = has_protected_content
self.text: Optional[str] = text
self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities)
@ -3077,7 +3077,7 @@ class Message(MaybeInaccessibleMessage):
explanation: Optional[str] = None,
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
open_period: Optional[int] = None,
close_date: Optional[Union[int, datetime.datetime]] = None,
close_date: Optional[Union[int, dtm.datetime]] = None,
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: ODVInput[int] = DEFAULT_NONE,

View file

@ -17,7 +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/].
"""This module contains the classes that represent Telegram MessageOigin."""
import datetime
import datetime as dtm
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
@ -78,14 +78,14 @@ class MessageOrigin(TelegramObject):
def __init__(
self,
type: str, # pylint: disable=W0622
date: datetime.datetime,
date: dtm.datetime,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
# Required by all subclasses
self.type: str = enum.get_member(constants.MessageOriginType, type, type)
self.date: datetime.datetime = date
self.date: dtm.datetime = date
self._id_attrs = (
self.type,
@ -152,7 +152,7 @@ class MessageOriginUser(MessageOrigin):
def __init__(
self,
date: datetime.datetime,
date: dtm.datetime,
sender_user: User,
*,
api_kwargs: Optional[JSONDict] = None,
@ -186,7 +186,7 @@ class MessageOriginHiddenUser(MessageOrigin):
def __init__(
self,
date: datetime.datetime,
date: dtm.datetime,
sender_user_name: str,
*,
api_kwargs: Optional[JSONDict] = None,
@ -227,7 +227,7 @@ class MessageOriginChat(MessageOrigin):
def __init__(
self,
date: datetime.datetime,
date: dtm.datetime,
sender_chat: Chat,
author_signature: Optional[str] = None,
*,
@ -271,7 +271,7 @@ class MessageOriginChannel(MessageOrigin):
def __init__(
self,
date: datetime.datetime,
date: dtm.datetime,
chat: Chat,
message_id: int,
author_signature: Optional[str] = None,

View file

@ -17,8 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram MessageReaction Update."""
import datetime as dtm
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from telegram._chat import Chat
@ -70,7 +70,7 @@ class MessageReactionCountUpdated(TelegramObject):
self,
chat: Chat,
message_id: int,
date: datetime,
date: dtm.datetime,
reactions: Sequence[ReactionCount],
*,
api_kwargs: Optional[JSONDict] = None,
@ -79,7 +79,7 @@ class MessageReactionCountUpdated(TelegramObject):
# Required
self.chat: Chat = chat
self.message_id: int = message_id
self.date: datetime = date
self.date: dtm.datetime = date
self.reactions: tuple[ReactionCount, ...] = parse_sequence_arg(reactions)
self._id_attrs = (self.chat, self.message_id, self.date, self.reactions)
@ -157,7 +157,7 @@ class MessageReactionUpdated(TelegramObject):
self,
chat: Chat,
message_id: int,
date: datetime,
date: dtm.datetime,
old_reaction: Sequence[ReactionType],
new_reaction: Sequence[ReactionType],
user: Optional[User] = None,
@ -169,7 +169,7 @@ class MessageReactionUpdated(TelegramObject):
# Required
self.chat: Chat = chat
self.message_id: int = message_id
self.date: datetime = date
self.date: dtm.datetime = date
self.old_reaction: tuple[ReactionType, ...] = parse_sequence_arg(old_reaction)
self.new_reaction: tuple[ReactionType, ...] = parse_sequence_arg(new_reaction)

View file

@ -1,795 +0,0 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
# pylint: disable=redefined-builtin
"""This module contains the classes for Telegram Stars transactions."""
import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._chat import Chat
from telegram._gifts import Gift
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.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class RevenueWithdrawalState(TelegramObject):
"""This object escribes the state of a revenue withdrawal operation. Currently, it can be one
of:
* :class:`telegram.RevenueWithdrawalStatePending`
* :class:`telegram.RevenueWithdrawalStateSucceeded`
* :class:`telegram.RevenueWithdrawalStateFailed`
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal.
.. versionadded:: 21.4
Args:
type (:obj:`str`): The type of the state.
Attributes:
type (:obj:`str`): The type of the state.
"""
__slots__ = ("type",)
PENDING: Final[str] = constants.RevenueWithdrawalStateType.PENDING
""":const:`telegram.constants.RevenueWithdrawalStateType.PENDING`"""
SUCCEEDED: Final[str] = constants.RevenueWithdrawalStateType.SUCCEEDED
""":const:`telegram.constants.RevenueWithdrawalStateType.SUCCEEDED`"""
FAILED: Final[str] = constants.RevenueWithdrawalStateType.FAILED
""":const:`telegram.constants.RevenueWithdrawalStateType.FAILED`"""
def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(api_kwargs=api_kwargs)
self.type: str = enum.get_member(constants.RevenueWithdrawalStateType, type, type)
self._id_attrs = (self.type,)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["RevenueWithdrawalState"]:
"""Converts JSON data to the appropriate :class:`RevenueWithdrawalState` object, i.e. takes
care of selecting the correct subclass.
Args:
data (dict[:obj:`str`, ...]): The JSON data.
bot (:class:`telegram.Bot`): The bot associated with this object.
Returns:
The Telegram object.
"""
data = cls._parse_data(data)
if not data:
return None
_class_mapping: dict[str, type[RevenueWithdrawalState]] = {
cls.PENDING: RevenueWithdrawalStatePending,
cls.SUCCEEDED: RevenueWithdrawalStateSucceeded,
cls.FAILED: RevenueWithdrawalStateFailed,
}
if cls is RevenueWithdrawalState and data.get("type") in _class_mapping:
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
return super().de_json(data=data, bot=bot)
class RevenueWithdrawalStatePending(RevenueWithdrawalState):
"""The withdrawal is in progress.
.. versionadded:: 21.4
Attributes:
type (:obj:`str`): The type of the state, always
:tg-const:`telegram.RevenueWithdrawalState.PENDING`.
"""
__slots__ = ()
def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=RevenueWithdrawalState.PENDING, api_kwargs=api_kwargs)
self._freeze()
class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState):
"""The withdrawal succeeded.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`date` are equal.
.. versionadded:: 21.4
Args:
date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object.
url (:obj:`str`): An HTTPS URL that can be used to see transaction details.
Attributes:
type (:obj:`str`): The type of the state, always
:tg-const:`telegram.RevenueWithdrawalState.SUCCEEDED`.
date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object.
url (:obj:`str`): An HTTPS URL that can be used to see transaction details.
"""
__slots__ = ("date", "url")
def __init__(
self,
date: dtm.datetime,
url: str,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=RevenueWithdrawalState.SUCCEEDED, api_kwargs=api_kwargs)
with self._unfrozen():
self.date: dtm.datetime = date
self.url: str = url
self._id_attrs = (
self.type,
self.date,
)
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class RevenueWithdrawalStateFailed(RevenueWithdrawalState):
"""The withdrawal failed and the transaction was refunded.
.. versionadded:: 21.4
Attributes:
type (:obj:`str`): The type of the state, always
:tg-const:`telegram.RevenueWithdrawalState.FAILED`.
"""
__slots__ = ()
def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=RevenueWithdrawalState.FAILED, api_kwargs=api_kwargs)
self._freeze()
class AffiliateInfo(TelegramObject):
"""Contains information about the affiliate that received a commission via this transaction.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`affiliate_user`, :attr:`affiliate_chat`,
:attr:`commission_per_mille`, :attr:`amount`, and :attr:`nanostar_amount` are equal.
.. versionadded:: NEXT.VERSION
Args:
affiliate_user (:class:`telegram.User`, optional): The bot or the user that received an
affiliate commission if it was received by a bot or a user
affiliate_chat (:class:`telegram.Chat`, optional): The chat that received an affiliate
commission if it was received by a chat
commission_per_mille (:obj:`int`): The number of Telegram Stars received by the affiliate
for each 1000 Telegram Stars received by the bot from referred users
amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the
transaction, rounded to 0; can be negative for refunds
nanostar_amount (:obj:`int`, optional): The number of
:tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
Stars received by the affiliate; from
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`;
can be negative for refunds
Attributes:
affiliate_user (:class:`telegram.User`): Optional. The bot or the user that received an
affiliate commission if it was received by a bot or a user
affiliate_chat (:class:`telegram.Chat`): Optional. The chat that received an affiliate
commission if it was received by a chat
commission_per_mille (:obj:`int`): The number of Telegram Stars received by the affiliate
for each 1000 Telegram Stars received by the bot from referred users
amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the
transaction, rounded to 0; can be negative for refunds
nanostar_amount (:obj:`int`): Optional. The number of
:tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
Stars received by the affiliate; from
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`;
can be negative for refunds
"""
__slots__ = (
"affiliate_chat",
"affiliate_user",
"amount",
"commission_per_mille",
"nanostar_amount",
)
def __init__(
self,
commission_per_mille: int,
amount: int,
affiliate_user: Optional["User"] = None,
affiliate_chat: Optional["Chat"] = None,
nanostar_amount: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(api_kwargs=api_kwargs)
self.affiliate_user: Optional[User] = affiliate_user
self.affiliate_chat: Optional[Chat] = affiliate_chat
self.commission_per_mille: int = commission_per_mille
self.amount: int = amount
self.nanostar_amount: Optional[int] = nanostar_amount
self._id_attrs = (
self.affiliate_user,
self.affiliate_chat,
self.commission_per_mille,
self.amount,
self.nanostar_amount,
)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot)
class TransactionPartner(TelegramObject):
"""This object describes the source of a transaction, or its recipient for outgoing
transactions. Currently, it can be one of:
* :class:`TransactionPartnerUser`
* :class:`TransactionPartnerAffiliateProgram`
* :class:`TransactionPartnerFragment`
* :class:`TransactionPartnerTelegramAds`
* :class:`TransactionPartnerTelegramApi`
* :class:`TransactionPartnerOther`
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal.
.. versionadded:: 21.4
Args:
type (:obj:`str`): The type of the transaction partner.
Attributes:
type (:obj:`str`): The type of the transaction partner.
"""
__slots__ = ("type",)
AFFILIATE_PROGRAM: Final[str] = constants.TransactionPartnerType.AFFILIATE_PROGRAM
""":const:`telegram.constants.TransactionPartnerType.AFFILIATE_PROGRAM`
.. versionadded:: NEXT.VERSION
"""
FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT
""":const:`telegram.constants.TransactionPartnerType.FRAGMENT`"""
OTHER: Final[str] = constants.TransactionPartnerType.OTHER
""":const:`telegram.constants.TransactionPartnerType.OTHER`"""
TELEGRAM_ADS: Final[str] = constants.TransactionPartnerType.TELEGRAM_ADS
""":const:`telegram.constants.TransactionPartnerType.TELEGRAM_ADS`"""
TELEGRAM_API: Final[str] = constants.TransactionPartnerType.TELEGRAM_API
""":const:`telegram.constants.TransactionPartnerType.TELEGRAM_API`"""
USER: Final[str] = constants.TransactionPartnerType.USER
""":const:`telegram.constants.TransactionPartnerType.USER`"""
def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(api_kwargs=api_kwargs)
self.type: str = enum.get_member(constants.TransactionPartnerType, type, type)
self._id_attrs = (self.type,)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["TransactionPartner"]:
"""Converts JSON data to the appropriate :class:`TransactionPartner` object, i.e. takes
care of selecting the correct subclass.
Args:
data (dict[:obj:`str`, ...]): The JSON data.
bot (:class:`telegram.Bot`): The bot associated with this object.
Returns:
The Telegram object.
"""
data = cls._parse_data(data)
if data is None:
return None
if not data and cls is TransactionPartner:
return None
_class_mapping: dict[str, type[TransactionPartner]] = {
cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram,
cls.FRAGMENT: TransactionPartnerFragment,
cls.USER: TransactionPartnerUser,
cls.TELEGRAM_ADS: TransactionPartnerTelegramAds,
cls.TELEGRAM_API: TransactionPartnerTelegramApi,
cls.OTHER: TransactionPartnerOther,
}
if cls is TransactionPartner and data.get("type") in _class_mapping:
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
return super().de_json(data=data, bot=bot)
class TransactionPartnerAffiliateProgram(TransactionPartner):
"""Describes the affiliate program that issued the affiliate commission received via this
transaction.
This object is comparable in terms of equality. Two objects of this class are considered equal,
if their :attr:`commission_per_mille` are equal.
.. versionadded:: NEXT.VERSION
Args:
sponsor_user (:class:`telegram.User`, optional): Information about the bot that sponsored
the affiliate program
commission_per_mille (:obj:`int`): The number of Telegram Stars received by the bot for
each 1000 Telegram Stars received by the affiliate program sponsor from referred users.
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.AFFILIATE_PROGRAM`.
sponsor_user (:class:`telegram.User`): Optional. Information about the bot that sponsored
the affiliate program
commission_per_mille (:obj:`int`): The number of Telegram Stars received by the bot for
each 1000 Telegram Stars received by the affiliate program sponsor from referred users.
"""
__slots__ = ("commission_per_mille", "sponsor_user")
def __init__(
self,
commission_per_mille: int,
sponsor_user: Optional["User"] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.AFFILIATE_PROGRAM, api_kwargs=api_kwargs)
with self._unfrozen():
self.sponsor_user: Optional[User] = sponsor_user
self.commission_per_mille: int = commission_per_mille
self._id_attrs = (
self.type,
self.commission_per_mille,
)
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerFragment(TransactionPartner):
"""Describes a withdrawal transaction with Fragment.
.. versionadded:: 21.4
Args:
withdrawal_state (:class:`telegram.RevenueWithdrawalState`, optional): State of the
transaction if the transaction is outgoing.
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.FRAGMENT`.
withdrawal_state (:class:`telegram.RevenueWithdrawalState`): Optional. State of the
transaction if the transaction is outgoing.
"""
__slots__ = ("withdrawal_state",)
def __init__(
self,
withdrawal_state: Optional["RevenueWithdrawalState"] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.FRAGMENT, api_kwargs=api_kwargs)
with self._unfrozen():
self.withdrawal_state: Optional[RevenueWithdrawalState] = withdrawal_state
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["TransactionPartnerFragment"]:
"""See :meth:`telegram.TransactionPartner.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["withdrawal_state"] = RevenueWithdrawalState.de_json(
data.get("withdrawal_state"), bot
)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerUser(TransactionPartner):
"""Describes a transaction with a user.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`user` are equal.
.. versionadded:: 21.4
Args:
user (:class:`telegram.User`): Information about the user.
affiliate (:class:`telegram.AffiliateInfo`, optional): Information about the affiliate that
received a commission via this transaction
.. versionadded:: NEXT.VERSION
invoice_payload (:obj:`str`, optional): Bot-specified invoice payload.
subscription_period (:class:`datetime.timedelta`, optional): The duration of the paid
subscription
.. versionadded:: 21.8
paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid
media bought by the user.
.. versionadded:: 21.5
paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload.
.. versionadded:: 21.6
gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot
.. versionadded:: 21.8
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.USER`.
user (:class:`telegram.User`): Information about the user.
affiliate (:class:`telegram.AffiliateInfo`): Optional. Information about the affiliate that
received a commission via this transaction
.. versionadded:: NEXT.VERSION
invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload.
subscription_period (:class:`datetime.timedelta`): Optional. The duration of the paid
subscription
.. versionadded:: 21.8
paid_media (tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid
media bought by the user.
.. versionadded:: 21.5
paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload.
.. versionadded:: 21.6
gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot
.. versionadded:: 21.8
"""
__slots__ = (
"affiliate",
"gift",
"invoice_payload",
"paid_media",
"paid_media_payload",
"subscription_period",
"user",
)
def __init__(
self,
user: "User",
invoice_payload: Optional[str] = None,
paid_media: Optional[Sequence[PaidMedia]] = None,
paid_media_payload: Optional[str] = None,
subscription_period: Optional[dtm.timedelta] = None,
gift: Optional[Gift] = None,
affiliate: Optional[AffiliateInfo] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs)
with self._unfrozen():
self.user: User = user
self.affiliate: Optional[AffiliateInfo] = affiliate
self.invoice_payload: Optional[str] = invoice_payload
self.paid_media: Optional[tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media)
self.paid_media_payload: Optional[str] = paid_media_payload
self.subscription_period: Optional[dtm.timedelta] = subscription_period
self.gift: Optional[Gift] = gift
self._id_attrs = (
self.type,
self.user,
)
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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["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)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerOther(TransactionPartner):
"""Describes a transaction with an unknown partner.
.. versionadded:: 21.4
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.OTHER`.
"""
__slots__ = ()
def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=TransactionPartner.OTHER, api_kwargs=api_kwargs)
self._freeze()
class TransactionPartnerTelegramAds(TransactionPartner):
"""Describes a withdrawal transaction to the Telegram Ads platform.
.. versionadded:: 21.4
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.TELEGRAM_ADS`.
"""
__slots__ = ()
def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=TransactionPartner.TELEGRAM_ADS, api_kwargs=api_kwargs)
self._freeze()
class TransactionPartnerTelegramApi(TransactionPartner):
"""Describes a transaction with payment for
`paid broadcasting <https://core.telegram.org/bots/api#paid-broadcasts>`_.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`request_count` is equal.
.. versionadded:: 21.7
Args:
request_count (:obj:`int`): The number of successful requests that exceeded regular limits
and were therefore billed.
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.TELEGRAM_API`.
request_count (:obj:`int`): The number of successful requests that exceeded regular limits
and were therefore billed.
"""
__slots__ = ("request_count",)
def __init__(self, request_count: int, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=TransactionPartner.TELEGRAM_API, api_kwargs=api_kwargs)
with self._unfrozen():
self.request_count: int = request_count
self._id_attrs = (self.request_count,)
class StarTransaction(TelegramObject):
"""Describes a Telegram Star transaction.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id`, :attr:`source`, and :attr:`receiver` are equal.
.. versionadded:: 21.4
Args:
id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer
of the original transaction for refund transactions.
Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for
successful incoming payments from users.
amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction.
nanostar_amount (:obj:`int`, optional): The number of
:tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
Stars transferred by the transaction; from 0 to
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`
.. versionadded:: NEXT.VERSION
date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object.
source (:class:`telegram.TransactionPartner`, optional): Source of an incoming transaction
(e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal).
Only for incoming transactions.
receiver (:class:`telegram.TransactionPartner`, optional): Receiver of an outgoing
transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for
outgoing transactions.
Attributes:
id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer
of the original transaction for refund transactions.
Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for
successful incoming payments from users.
amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction.
nanostar_amount (:obj:`int`): Optional. The number of
:tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
Stars transferred by the transaction; from 0 to
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`
.. versionadded:: NEXT.VERSION
date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object.
source (:class:`telegram.TransactionPartner`): Optional. Source of an incoming transaction
(e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal).
Only for incoming transactions.
receiver (:class:`telegram.TransactionPartner`): Optional. Receiver of an outgoing
transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for
outgoing transactions.
"""
__slots__ = ("amount", "date", "id", "nanostar_amount", "receiver", "source")
def __init__(
self,
id: str,
amount: int,
date: dtm.datetime,
source: Optional[TransactionPartner] = None,
receiver: Optional[TransactionPartner] = None,
nanostar_amount: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(api_kwargs=api_kwargs)
self.id: str = id
self.amount: int = amount
self.date: dtm.datetime = date
self.source: Optional[TransactionPartner] = source
self.receiver: Optional[TransactionPartner] = receiver
self.nanostar_amount: Optional[int] = nanostar_amount
self._id_attrs = (
self.id,
self.source,
self.receiver,
)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot)
class StarTransactions(TelegramObject):
"""
Contains a list of Telegram Star transactions.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`transactions` are equal.
.. versionadded:: 21.4
Args:
transactions (Sequence[:class:`telegram.StarTransaction`]): The list of transactions.
Attributes:
transactions (tuple[:class:`telegram.StarTransaction`]): The list of transactions.
"""
__slots__ = ("transactions",)
def __init__(
self, transactions: Sequence[StarTransaction], *, api_kwargs: Optional[JSONDict] = None
):
super().__init__(api_kwargs=api_kwargs)
self.transactions: tuple[StarTransaction, ...] = parse_sequence_arg(transactions)
self._id_attrs = (self.transactions,)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot)

View file

View file

@ -0,0 +1,121 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
# pylint: disable=redefined-builtin
"""This module contains the classes for Telegram Stars affiliates."""
from typing import TYPE_CHECKING, Optional
from telegram._chat import Chat
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class AffiliateInfo(TelegramObject):
"""Contains information about the affiliate that received a commission via this transaction.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`affiliate_user`, :attr:`affiliate_chat`,
:attr:`commission_per_mille`, :attr:`amount`, and :attr:`nanostar_amount` are equal.
.. versionadded:: 21.9
Args:
affiliate_user (:class:`telegram.User`, optional): The bot or the user that received an
affiliate commission if it was received by a bot or a user
affiliate_chat (:class:`telegram.Chat`, optional): The chat that received an affiliate
commission if it was received by a chat
commission_per_mille (:obj:`int`): The number of Telegram Stars received by the affiliate
for each 1000 Telegram Stars received by the bot from referred users
amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the
transaction, rounded to 0; can be negative for refunds
nanostar_amount (:obj:`int`, optional): The number of
:tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
Stars received by the affiliate; from
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`;
can be negative for refunds
Attributes:
affiliate_user (:class:`telegram.User`): Optional. The bot or the user that received an
affiliate commission if it was received by a bot or a user
affiliate_chat (:class:`telegram.Chat`): Optional. The chat that received an affiliate
commission if it was received by a chat
commission_per_mille (:obj:`int`): The number of Telegram Stars received by the affiliate
for each 1000 Telegram Stars received by the bot from referred users
amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the
transaction, rounded to 0; can be negative for refunds
nanostar_amount (:obj:`int`): Optional. The number of
:tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
Stars received by the affiliate; from
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`;
can be negative for refunds
"""
__slots__ = (
"affiliate_chat",
"affiliate_user",
"amount",
"commission_per_mille",
"nanostar_amount",
)
def __init__(
self,
commission_per_mille: int,
amount: int,
affiliate_user: Optional["User"] = None,
affiliate_chat: Optional["Chat"] = None,
nanostar_amount: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(api_kwargs=api_kwargs)
self.affiliate_user: Optional[User] = affiliate_user
self.affiliate_chat: Optional[Chat] = affiliate_chat
self.commission_per_mille: int = commission_per_mille
self.amount: int = amount
self.nanostar_amount: Optional[int] = nanostar_amount
self._id_attrs = (
self.affiliate_user,
self.affiliate_chat,
self.commission_per_mille,
self.amount,
self.nanostar_amount,
)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot)

View file

@ -0,0 +1,188 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
# pylint: disable=redefined-builtin
"""This module contains the classes for Telegram Stars Revenue Withdrawals."""
import datetime as dtm
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._telegramobject import TelegramObject
from telegram._utils import enum
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class RevenueWithdrawalState(TelegramObject):
"""This object describes the state of a revenue withdrawal operation. Currently, it can be one
of:
* :class:`telegram.RevenueWithdrawalStatePending`
* :class:`telegram.RevenueWithdrawalStateSucceeded`
* :class:`telegram.RevenueWithdrawalStateFailed`
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal.
.. versionadded:: 21.4
Args:
type (:obj:`str`): The type of the state.
Attributes:
type (:obj:`str`): The type of the state.
"""
__slots__ = ("type",)
PENDING: Final[str] = constants.RevenueWithdrawalStateType.PENDING
""":const:`telegram.constants.RevenueWithdrawalStateType.PENDING`"""
SUCCEEDED: Final[str] = constants.RevenueWithdrawalStateType.SUCCEEDED
""":const:`telegram.constants.RevenueWithdrawalStateType.SUCCEEDED`"""
FAILED: Final[str] = constants.RevenueWithdrawalStateType.FAILED
""":const:`telegram.constants.RevenueWithdrawalStateType.FAILED`"""
def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(api_kwargs=api_kwargs)
self.type: str = enum.get_member(constants.RevenueWithdrawalStateType, type, type)
self._id_attrs = (self.type,)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["RevenueWithdrawalState"]:
"""Converts JSON data to the appropriate :class:`RevenueWithdrawalState` object, i.e. takes
care of selecting the correct subclass.
Args:
data (dict[:obj:`str`, ...]): The JSON data.
bot (:class:`telegram.Bot`): The bot associated with this object.
Returns:
The Telegram object.
"""
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,
cls.FAILED: RevenueWithdrawalStateFailed,
}
if cls is RevenueWithdrawalState and data.get("type") in _class_mapping:
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
return super().de_json(data=data, bot=bot)
class RevenueWithdrawalStatePending(RevenueWithdrawalState):
"""The withdrawal is in progress.
.. versionadded:: 21.4
Attributes:
type (:obj:`str`): The type of the state, always
:tg-const:`telegram.RevenueWithdrawalState.PENDING`.
"""
__slots__ = ()
def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=RevenueWithdrawalState.PENDING, api_kwargs=api_kwargs)
self._freeze()
class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState):
"""The withdrawal succeeded.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`date` are equal.
.. versionadded:: 21.4
Args:
date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object.
url (:obj:`str`): An HTTPS URL that can be used to see transaction details.
Attributes:
type (:obj:`str`): The type of the state, always
:tg-const:`telegram.RevenueWithdrawalState.SUCCEEDED`.
date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object.
url (:obj:`str`): An HTTPS URL that can be used to see transaction details.
"""
__slots__ = ("date", "url")
def __init__(
self,
date: dtm.datetime,
url: str,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=RevenueWithdrawalState.SUCCEEDED, api_kwargs=api_kwargs)
with self._unfrozen():
self.date: dtm.datetime = date
self.url: str = url
self._id_attrs = (
self.type,
self.date,
)
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class RevenueWithdrawalStateFailed(RevenueWithdrawalState):
"""The withdrawal failed and the transaction was refunded.
.. versionadded:: 21.4
Attributes:
type (:obj:`str`): The type of the state, always
:tg-const:`telegram.RevenueWithdrawalState.FAILED`.
"""
__slots__ = ()
def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=RevenueWithdrawalState.FAILED, api_kwargs=api_kwargs)
self._freeze()

View file

@ -0,0 +1,172 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
# pylint: disable=redefined-builtin
"""This module contains the classes for Telegram Stars transactions."""
import datetime as dtm
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.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
from .transactionpartner import TransactionPartner
if TYPE_CHECKING:
from telegram import Bot
class StarTransaction(TelegramObject):
"""Describes a Telegram Star transaction.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id`, :attr:`source`, and :attr:`receiver` are equal.
.. versionadded:: 21.4
Args:
id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer
of the original transaction for refund transactions.
Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for
successful incoming payments from users.
amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction.
nanostar_amount (:obj:`int`, optional): The number of
:tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
Stars transferred by the transaction; from 0 to
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`
.. versionadded:: 21.9
date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object.
source (:class:`telegram.TransactionPartner`, optional): Source of an incoming transaction
(e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal).
Only for incoming transactions.
receiver (:class:`telegram.TransactionPartner`, optional): Receiver of an outgoing
transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for
outgoing transactions.
Attributes:
id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer
of the original transaction for refund transactions.
Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for
successful incoming payments from users.
amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction.
nanostar_amount (:obj:`int`): Optional. The number of
:tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
Stars transferred by the transaction; from 0 to
:tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`
.. versionadded:: 21.9
date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object.
source (:class:`telegram.TransactionPartner`): Optional. Source of an incoming transaction
(e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal).
Only for incoming transactions.
receiver (:class:`telegram.TransactionPartner`): Optional. Receiver of an outgoing
transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for
outgoing transactions.
"""
__slots__ = ("amount", "date", "id", "nanostar_amount", "receiver", "source")
def __init__(
self,
id: str,
amount: int,
date: dtm.datetime,
source: Optional[TransactionPartner] = None,
receiver: Optional[TransactionPartner] = None,
nanostar_amount: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(api_kwargs=api_kwargs)
self.id: str = id
self.amount: int = amount
self.date: dtm.datetime = date
self.source: Optional[TransactionPartner] = source
self.receiver: Optional[TransactionPartner] = receiver
self.nanostar_amount: Optional[int] = nanostar_amount
self._id_attrs = (
self.id,
self.source,
self.receiver,
)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot)
class StarTransactions(TelegramObject):
"""
Contains a list of Telegram Star transactions.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`transactions` are equal.
.. versionadded:: 21.4
Args:
transactions (Sequence[:class:`telegram.StarTransaction`]): The list of transactions.
Attributes:
transactions (tuple[:class:`telegram.StarTransaction`]): The list of transactions.
"""
__slots__ = ("transactions",)
def __init__(
self, transactions: Sequence[StarTransaction], *, api_kwargs: Optional[JSONDict] = None
):
super().__init__(api_kwargs=api_kwargs)
self.transactions: tuple[StarTransaction, ...] = parse_sequence_arg(transactions)
self._id_attrs = (self.transactions,)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot)

View file

@ -0,0 +1,405 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
# pylint: disable=redefined-builtin
"""This module contains the classes for Telegram Stars transaction partners."""
import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._gifts import Gift
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.types import JSONDict
from .affiliateinfo import AffiliateInfo
from .revenuewithdrawalstate import RevenueWithdrawalState
if TYPE_CHECKING:
from telegram import Bot
class TransactionPartner(TelegramObject):
"""This object describes the source of a transaction, or its recipient for outgoing
transactions. Currently, it can be one of:
* :class:`TransactionPartnerUser`
* :class:`TransactionPartnerAffiliateProgram`
* :class:`TransactionPartnerFragment`
* :class:`TransactionPartnerTelegramAds`
* :class:`TransactionPartnerTelegramApi`
* :class:`TransactionPartnerOther`
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal.
.. versionadded:: 21.4
Args:
type (:obj:`str`): The type of the transaction partner.
Attributes:
type (:obj:`str`): The type of the transaction partner.
"""
__slots__ = ("type",)
AFFILIATE_PROGRAM: Final[str] = constants.TransactionPartnerType.AFFILIATE_PROGRAM
""":const:`telegram.constants.TransactionPartnerType.AFFILIATE_PROGRAM`
.. versionadded:: 21.9
"""
FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT
""":const:`telegram.constants.TransactionPartnerType.FRAGMENT`"""
OTHER: Final[str] = constants.TransactionPartnerType.OTHER
""":const:`telegram.constants.TransactionPartnerType.OTHER`"""
TELEGRAM_ADS: Final[str] = constants.TransactionPartnerType.TELEGRAM_ADS
""":const:`telegram.constants.TransactionPartnerType.TELEGRAM_ADS`"""
TELEGRAM_API: Final[str] = constants.TransactionPartnerType.TELEGRAM_API
""":const:`telegram.constants.TransactionPartnerType.TELEGRAM_API`"""
USER: Final[str] = constants.TransactionPartnerType.USER
""":const:`telegram.constants.TransactionPartnerType.USER`"""
def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(api_kwargs=api_kwargs)
self.type: str = enum.get_member(constants.TransactionPartnerType, type, type)
self._id_attrs = (self.type,)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["TransactionPartner"]:
"""Converts JSON data to the appropriate :class:`TransactionPartner` object, i.e. takes
care of selecting the correct subclass.
Args:
data (dict[:obj:`str`, ...]): The JSON data.
bot (:class:`telegram.Bot`): The bot associated with this object.
Returns:
The Telegram object.
"""
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,
cls.USER: TransactionPartnerUser,
cls.TELEGRAM_ADS: TransactionPartnerTelegramAds,
cls.TELEGRAM_API: TransactionPartnerTelegramApi,
cls.OTHER: TransactionPartnerOther,
}
if cls is TransactionPartner and data.get("type") in _class_mapping:
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
return super().de_json(data=data, bot=bot)
class TransactionPartnerAffiliateProgram(TransactionPartner):
"""Describes the affiliate program that issued the affiliate commission received via this
transaction.
This object is comparable in terms of equality. Two objects of this class are considered equal,
if their :attr:`commission_per_mille` are equal.
.. versionadded:: 21.9
Args:
sponsor_user (:class:`telegram.User`, optional): Information about the bot that sponsored
the affiliate program
commission_per_mille (:obj:`int`): The number of Telegram Stars received by the bot for
each 1000 Telegram Stars received by the affiliate program sponsor from referred users.
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.AFFILIATE_PROGRAM`.
sponsor_user (:class:`telegram.User`): Optional. Information about the bot that sponsored
the affiliate program
commission_per_mille (:obj:`int`): The number of Telegram Stars received by the bot for
each 1000 Telegram Stars received by the affiliate program sponsor from referred users.
"""
__slots__ = ("commission_per_mille", "sponsor_user")
def __init__(
self,
commission_per_mille: int,
sponsor_user: Optional["User"] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.AFFILIATE_PROGRAM, api_kwargs=api_kwargs)
with self._unfrozen():
self.sponsor_user: Optional[User] = sponsor_user
self.commission_per_mille: int = commission_per_mille
self._id_attrs = (
self.type,
self.commission_per_mille,
)
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerFragment(TransactionPartner):
"""Describes a withdrawal transaction with Fragment.
.. versionadded:: 21.4
Args:
withdrawal_state (:class:`telegram.RevenueWithdrawalState`, optional): State of the
transaction if the transaction is outgoing.
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.FRAGMENT`.
withdrawal_state (:class:`telegram.RevenueWithdrawalState`): Optional. State of the
transaction if the transaction is outgoing.
"""
__slots__ = ("withdrawal_state",)
def __init__(
self,
withdrawal_state: Optional["RevenueWithdrawalState"] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.FRAGMENT, api_kwargs=api_kwargs)
with self._unfrozen():
self.withdrawal_state: Optional[RevenueWithdrawalState] = withdrawal_state
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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
)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerUser(TransactionPartner):
"""Describes a transaction with a user.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`user` are equal.
.. versionadded:: 21.4
Args:
user (:class:`telegram.User`): Information about the user.
affiliate (:class:`telegram.AffiliateInfo`, optional): Information about the affiliate that
received a commission via this transaction
.. versionadded:: 21.9
invoice_payload (:obj:`str`, optional): Bot-specified invoice payload.
subscription_period (:class:`datetime.timedelta`, optional): The duration of the paid
subscription
.. versionadded:: 21.8
paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid
media bought by the user.
.. versionadded:: 21.5
paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload.
.. versionadded:: 21.6
gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot
.. versionadded:: 21.8
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.USER`.
user (:class:`telegram.User`): Information about the user.
affiliate (:class:`telegram.AffiliateInfo`): Optional. Information about the affiliate that
received a commission via this transaction
.. versionadded:: 21.9
invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload.
subscription_period (:class:`datetime.timedelta`): Optional. The duration of the paid
subscription
.. versionadded:: 21.8
paid_media (tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid
media bought by the user.
.. versionadded:: 21.5
paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload.
.. versionadded:: 21.6
gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot
.. versionadded:: 21.8
"""
__slots__ = (
"affiliate",
"gift",
"invoice_payload",
"paid_media",
"paid_media_payload",
"subscription_period",
"user",
)
def __init__(
self,
user: "User",
invoice_payload: Optional[str] = None,
paid_media: Optional[Sequence[PaidMedia]] = None,
paid_media_payload: Optional[str] = None,
subscription_period: Optional[dtm.timedelta] = None,
gift: Optional[Gift] = None,
affiliate: Optional[AffiliateInfo] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs)
with self._unfrozen():
self.user: User = user
self.affiliate: Optional[AffiliateInfo] = affiliate
self.invoice_payload: Optional[str] = invoice_payload
self.paid_media: Optional[tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media)
self.paid_media_payload: Optional[str] = paid_media_payload
self.subscription_period: Optional[dtm.timedelta] = subscription_period
self.gift: Optional[Gift] = gift
self._id_attrs = (
self.type,
self.user,
)
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["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["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)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerOther(TransactionPartner):
"""Describes a transaction with an unknown partner.
.. versionadded:: 21.4
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.OTHER`.
"""
__slots__ = ()
def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=TransactionPartner.OTHER, api_kwargs=api_kwargs)
self._freeze()
class TransactionPartnerTelegramAds(TransactionPartner):
"""Describes a withdrawal transaction to the Telegram Ads platform.
.. versionadded:: 21.4
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.TELEGRAM_ADS`.
"""
__slots__ = ()
def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=TransactionPartner.TELEGRAM_ADS, api_kwargs=api_kwargs)
self._freeze()
class TransactionPartnerTelegramApi(TransactionPartner):
"""Describes a transaction with payment for
`paid broadcasting <https://core.telegram.org/bots/api#paid-broadcasts>`_.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`request_count` is equal.
.. versionadded:: 21.7
Args:
request_count (:obj:`int`): The number of successful requests that exceeded regular limits
and were therefore billed.
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.TELEGRAM_API`.
request_count (:obj:`int`): The number of successful requests that exceeded regular limits
and were therefore billed.
"""
__slots__ = ("request_count",)
def __init__(self, request_count: int, *, api_kwargs: Optional[JSONDict] = None) -> None:
super().__init__(type=TransactionPartner.TELEGRAM_API, api_kwargs=api_kwargs)
with self._unfrozen():
self.request_count: int = request_count
self._id_attrs = (self.request_count,)

View file

@ -17,7 +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/].
"""This module contains an object that represents a Telegram Poll."""
import datetime
import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Final, Optional
@ -446,7 +446,7 @@ class Poll(TelegramObject):
explanation: Optional[str] = None,
explanation_entities: Optional[Sequence[MessageEntity]] = None,
open_period: Optional[int] = None,
close_date: Optional[datetime.datetime] = None,
close_date: Optional[dtm.datetime] = None,
question_entities: Optional[Sequence[MessageEntity]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
@ -466,7 +466,7 @@ class Poll(TelegramObject):
explanation_entities
)
self.open_period: Optional[int] = open_period
self.close_date: Optional[datetime.datetime] = close_date
self.close_date: Optional[dtm.datetime] = close_date
self.question_entities: tuple[MessageEntity, ...] = parse_sequence_arg(question_entities)
self._id_attrs = (self.id,)

View file

@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram Objects."""
import contextlib
import datetime
import datetime as dtm
import inspect
import json
from collections.abc import Iterator, Mapping, Sized
@ -634,9 +634,9 @@ class TelegramObject:
val.append(item)
out[key] = val
elif isinstance(value, datetime.datetime):
elif isinstance(value, dtm.datetime):
out[key] = to_timestamp(value)
elif isinstance(value, datetime.timedelta):
elif isinstance(value, dtm.timedelta):
out[key] = value.total_seconds()
for key in pop_keys:

View file

@ -18,8 +18,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram User."""
import datetime as dtm
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Optional, Union
from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton
@ -1582,7 +1582,7 @@ class User(TelegramObject):
explanation: Optional[str] = None,
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
open_period: Optional[int] = None,
close_date: Optional[Union[int, datetime]] = None,
close_date: Optional[Union[int, dtm.datetime]] = None,
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,

View file

@ -51,6 +51,6 @@ class Version(NamedTuple):
__version_info__: Final[Version] = Version(
major=21, minor=8, micro=0, releaselevel="final", serial=0
major=21, minor=9, micro=0, releaselevel="final", serial=0
)
__version__: Final[str] = str(__version_info__)

View file

@ -18,8 +18,8 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram WebhookInfo."""
import datetime as dtm
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from telegram._telegramobject import TelegramObject
@ -126,12 +126,12 @@ class WebhookInfo(TelegramObject):
url: str,
has_custom_certificate: bool,
pending_update_count: int,
last_error_date: Optional[datetime] = None,
last_error_date: Optional[dtm.datetime] = None,
last_error_message: Optional[str] = None,
max_connections: Optional[int] = None,
allowed_updates: Optional[Sequence[str]] = None,
ip_address: Optional[str] = None,
last_synchronization_error_date: Optional[datetime] = None,
last_synchronization_error_date: Optional[dtm.datetime] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@ -143,11 +143,13 @@ class WebhookInfo(TelegramObject):
# Optional
self.ip_address: Optional[str] = ip_address
self.last_error_date: Optional[datetime] = last_error_date
self.last_error_date: Optional[dtm.datetime] = last_error_date
self.last_error_message: Optional[str] = last_error_message
self.max_connections: Optional[int] = max_connections
self.allowed_updates: tuple[str, ...] = parse_sequence_arg(allowed_updates)
self.last_synchronization_error_date: Optional[datetime] = last_synchronization_error_date
self.last_synchronization_error_date: Optional[dtm.datetime] = (
last_synchronization_error_date
)
self._id_attrs = (
self.url,

View file

@ -107,7 +107,7 @@ __all__ = [
"WebhookLimit",
]
import datetime
import datetime as dtm
import sys
from enum import Enum
from typing import Final, NamedTuple, Optional
@ -172,7 +172,7 @@ SUPPORTED_WEBHOOK_PORTS: Final[list[int]] = [443, 80, 88, 8443]
#: This date literal is used in :class:`telegram.InaccessibleMessage`
#:
#: .. versionadded:: 20.8
ZERO_DATE: Final[datetime.datetime] = datetime.datetime(1970, 1, 1, tzinfo=UTC)
ZERO_DATE: Final[dtm.datetime] = dtm.datetime(1970, 1, 1, tzinfo=UTC)
class AccentColor(Enum):
@ -2466,7 +2466,7 @@ class StarTransactions(FloatEnum):
The enum members of this enumeration are instances of :class:`float` and can be treated as
such.
.. versionadded:: NEXT.VERSION
.. versionadded:: 21.9
"""
__slots__ = ()
@ -2499,7 +2499,7 @@ class StarTransactionsLimit(IntEnum):
""":obj:`int`: Minimum value allowed for :paramref:`~telegram.AffiliateInfo.nanostar_amount`
parameter of :class:`telegram.AffiliateInfo`.
.. versionadded:: NEXT.VERSION
.. versionadded:: 21.9
"""
NANOSTAR_MAX_AMOUNT = 999999999
""":obj:`int`: Maximum value allowed for :paramref:`~telegram.StarTransaction.nanostar_amount`
@ -2507,7 +2507,7 @@ class StarTransactionsLimit(IntEnum):
:paramref:`~telegram.AffiliateInfo.nanostar_amount` parameter of
:class:`telegram.AffiliateInfo`.
.. versionadded:: NEXT.VERSION
.. versionadded:: 21.9
"""
@ -2656,7 +2656,7 @@ class TransactionPartnerType(StringEnum):
AFFILIATE_PROGRAM = "affiliate_program"
""":obj:`str`: Transaction with Affiliate Program.
.. versionadded:: NEXT.VERSION
.. versionadded:: 21.9
"""
FRAGMENT = "fragment"
""":obj:`str`: Withdrawal transaction with Fragment."""
@ -2954,7 +2954,7 @@ class InvoiceLimit(IntEnum):
.. versionadded:: 21.6
"""
SUBSCRIPTION_PERIOD = datetime.timedelta(days=30).total_seconds()
SUBSCRIPTION_PERIOD = dtm.timedelta(days=30).total_seconds()
""":obj:`int`: The period of time for which the subscription is active before
the next payment, passed as :paramref:`~telegram.Bot.create_invoice_link.subscription_period`
parameter of :meth:`telegram.Bot.create_invoice_link`.
@ -2965,7 +2965,7 @@ class InvoiceLimit(IntEnum):
""":obj:`int`: The maximum price of a subscription created wtih
:meth:`telegram.Bot.create_invoice_link`.
.. versionadded:: NEXT.VERSION
.. versionadded:: 21.9
"""

View file

@ -826,7 +826,7 @@ class Application(
allowed_updates (Sequence[:obj:`str`], optional): Passed to
:meth:`telegram.Bot.get_updates`.
.. versionchanged:: NEXT.VERSION
.. versionchanged:: 21.9
Accepts any :class:`collections.abc.Sequence` as input instead of just a list
close_loop (:obj:`bool`, optional): If :obj:`True`, the current event loop will be
closed upon shutdown. Defaults to :obj:`True`.
@ -960,7 +960,7 @@ class Application(
allowed_updates (Sequence[:obj:`str`], optional): Passed to
:meth:`telegram.Bot.set_webhook`.
.. versionchanged:: NEXT.VERSION
.. versionchanged:: 21.9
Accepts any :class:`collections.abc.Sequence` as input instead of just a list
drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
Telegram servers before actually starting to poll. Default is :obj:`False`.

View file

@ -17,9 +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 CallbackDataCache class."""
import datetime as dtm
import time
from collections.abc import MutableMapping
from datetime import datetime
from typing import TYPE_CHECKING, Any, Optional, Union, cast
from uuid import uuid4
@ -431,7 +431,9 @@ class CallbackDataCache:
with contextlib.suppress(KeyError):
self._keyboard_data.pop(keyboard_uuid)
def clear_callback_data(self, time_cutoff: Optional[Union[float, datetime]] = None) -> None:
def clear_callback_data(
self, time_cutoff: Optional[Union[float, dtm.datetime]] = None
) -> None:
"""Clears the stored callback data.
Args:
@ -447,13 +449,13 @@ class CallbackDataCache:
self.__clear(self._callback_queries)
def __clear(
self, mapping: MutableMapping, time_cutoff: Optional[Union[float, datetime]] = None
self, mapping: MutableMapping, time_cutoff: Optional[Union[float, dtm.datetime]] = None
) -> None:
if not time_cutoff:
mapping.clear()
return
if isinstance(time_cutoff, datetime):
if isinstance(time_cutoff, dtm.datetime):
effective_cutoff = to_float_timestamp(
time_cutoff, tzinfo=self.bot.defaults.tzinfo if self.bot.defaults else None
)

View file

@ -17,7 +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/].
"""This module contains the class Defaults, which allows passing default values to Application."""
import datetime
import datetime as dtm
from typing import Any, NoReturn, Optional, final
from telegram import LinkPreviewOptions
@ -132,7 +132,7 @@ class Defaults:
disable_notification: Optional[bool] = None,
disable_web_page_preview: Optional[bool] = None,
quote: Optional[bool] = None,
tzinfo: datetime.tzinfo = UTC,
tzinfo: dtm.tzinfo = UTC,
block: bool = True,
allow_sending_without_reply: Optional[bool] = None,
protect_content: Optional[bool] = None,
@ -142,7 +142,7 @@ class Defaults:
self._parse_mode: Optional[str] = parse_mode
self._disable_notification: Optional[bool] = disable_notification
self._allow_sending_without_reply: Optional[bool] = allow_sending_without_reply
self._tzinfo: datetime.tzinfo = tzinfo
self._tzinfo: dtm.tzinfo = tzinfo
self._block: bool = block
self._protect_content: Optional[bool] = protect_content
@ -356,7 +356,7 @@ class Defaults:
raise AttributeError("You can not assign a new value to quote after initialization.")
@property
def tzinfo(self) -> datetime.tzinfo:
def tzinfo(self) -> dtm.tzinfo:
""":obj:`tzinfo`: A timezone to be used for all date(time) objects appearing
throughout PTB.
"""

View file

@ -18,9 +18,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 an object that represents a Telegram Bot with convenience extensions."""
import datetime as dtm
from collections.abc import Sequence
from copy import copy
from datetime import datetime, timedelta
from typing import (
TYPE_CHECKING,
Any,
@ -448,7 +448,7 @@ class ExtBot(Bot, Generic[RLARGS]):
data[key] = self.defaults.api_defaults.get(key, val.value)
# 2)
elif isinstance(val, datetime):
elif isinstance(val, dtm.datetime):
data[key] = to_timestamp(val, tzinfo=self.defaults.tzinfo)
# 3)
@ -1110,7 +1110,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self,
chat_id: Union[str, int],
user_id: int,
until_date: Optional[Union[int, datetime]] = None,
until_date: Optional[Union[int, dtm.datetime]] = None,
revoke_messages: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -1157,7 +1157,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def create_chat_invite_link(
self,
chat_id: Union[str, int],
expire_date: Optional[Union[int, datetime]] = None,
expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@ -1204,7 +1204,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, timedelta]] = None,
subscription_period: Optional[Union[int, dtm.timedelta]] = None,
business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -1426,7 +1426,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def delete_sticker_from_set(
self,
sticker: str,
sticker: Union[str, "Sticker"],
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -1468,7 +1468,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self,
chat_id: Union[str, int],
invite_link: Union[str, "ChatInviteLink"],
expire_date: Optional[Union[int, datetime]] = None,
expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@ -2375,7 +2375,7 @@ class ExtBot(Bot, Generic[RLARGS]):
chat_id: Union[str, int],
user_id: int,
permissions: ChatPermissions,
until_date: Optional[Union[int, datetime]] = None,
until_date: Optional[Union[int, dtm.datetime]] = None,
use_independent_chat_permissions: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -3055,7 +3055,7 @@ class ExtBot(Bot, Generic[RLARGS]):
explanation: Optional[str] = None,
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
open_period: Optional[int] = None,
close_date: Optional[Union[int, datetime]] = None,
close_date: Optional[Union[int, dtm.datetime]] = None,
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
@ -3426,7 +3426,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self,
user_id: int,
emoji_status_custom_emoji_id: Optional[str] = None,
emoji_status_expiration_date: Optional[Union[int, datetime]] = None,
emoji_status_expiration_date: Optional[Union[int, dtm.datetime]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -3660,7 +3660,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def set_sticker_position_in_set(
self,
sticker: str,
sticker: Union[str, "Sticker"],
position: int,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -4114,7 +4114,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def set_sticker_emoji_list(
self,
sticker: str,
sticker: Union[str, "Sticker"],
emoji_list: Sequence[str],
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -4136,7 +4136,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def set_sticker_keywords(
self,
sticker: str,
sticker: Union[str, "Sticker"],
keywords: Optional[Sequence[str]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -4158,7 +4158,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def set_sticker_mask_position(
self,
sticker: str,
sticker: Union[str, "Sticker"],
mask_position: Optional[MaskPosition] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -4250,7 +4250,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self,
user_id: int,
name: str,
old_sticker: str,
old_sticker: Union[str, "Sticker"],
sticker: "InputSticker",
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,

View file

@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the ConversationHandler."""
import asyncio
import datetime
import datetime as dtm
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Final, Generic, NoReturn, Optional, Union, cast
@ -291,7 +291,7 @@ class ConversationHandler(BaseHandler[Update, CCT, object]):
per_chat: bool = True,
per_user: bool = True,
per_message: bool = False,
conversation_timeout: Optional[Union[float, datetime.timedelta]] = None,
conversation_timeout: Optional[Union[float, dtm.timedelta]] = None,
name: Optional[str] = None,
persistent: bool = False,
map_to_parent: Optional[dict[object, object]] = None,
@ -319,9 +319,7 @@ class ConversationHandler(BaseHandler[Update, CCT, object]):
self._per_user: bool = per_user
self._per_chat: bool = per_chat
self._per_message: bool = per_message
self._conversation_timeout: Optional[Union[float, datetime.timedelta]] = (
conversation_timeout
)
self._conversation_timeout: Optional[Union[float, dtm.timedelta]] = conversation_timeout
self._name: Optional[str] = name
self._map_to_parent: Optional[dict[object, object]] = map_to_parent
@ -530,7 +528,7 @@ class ConversationHandler(BaseHandler[Update, CCT, object]):
@property
def conversation_timeout(
self,
) -> Optional[Union[float, datetime.timedelta]]:
) -> Optional[Union[float, dtm.timedelta]]:
""":obj:`float` | :obj:`datetime.timedelta`: Optional. When this
handler is inactive more than this timeout (in seconds), it will be automatically
ended.

View file

@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes JobQueue and Job."""
import asyncio
import datetime
import datetime as dtm
import weakref
from typing import TYPE_CHECKING, Any, Generic, Optional, Union, cast, overload
@ -155,7 +155,7 @@ class JobQueue(Generic[CCT]):
dict[:obj:`str`, :obj:`object`]: The configuration values as dictionary.
"""
timezone: datetime.tzinfo = UTC
timezone: dtm.tzinfo = UTC
if (
self._application
and isinstance(self.application.bot, ExtBot)
@ -168,8 +168,8 @@ class JobQueue(Generic[CCT]):
"executors": {"default": self._executor},
}
def _tz_now(self) -> datetime.datetime:
return datetime.datetime.now(self.scheduler.timezone)
def _tz_now(self) -> dtm.datetime:
return dtm.datetime.now(self.scheduler.timezone)
@overload
def _parse_time_input(self, time: None, shift_day: bool = False) -> None: ...
@ -177,29 +177,29 @@ class JobQueue(Generic[CCT]):
@overload
def _parse_time_input(
self,
time: Union[float, datetime.timedelta, datetime.datetime, datetime.time],
time: Union[float, dtm.timedelta, dtm.datetime, dtm.time],
shift_day: bool = False,
) -> datetime.datetime: ...
) -> dtm.datetime: ...
def _parse_time_input(
self,
time: Union[float, datetime.timedelta, datetime.datetime, datetime.time, None],
time: Union[float, dtm.timedelta, dtm.datetime, dtm.time, None],
shift_day: bool = False,
) -> Optional[datetime.datetime]:
) -> Optional[dtm.datetime]:
if time is None:
return None
if isinstance(time, (int, float)):
return self._tz_now() + datetime.timedelta(seconds=time)
if isinstance(time, datetime.timedelta):
return self._tz_now() + dtm.timedelta(seconds=time)
if isinstance(time, dtm.timedelta):
return self._tz_now() + time
if isinstance(time, datetime.time):
date_time = datetime.datetime.combine(
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
if isinstance(time, dtm.time):
date_time = dtm.datetime.combine(
dtm.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
)
if date_time.tzinfo is None:
date_time = localize(date_time, self.scheduler.timezone)
if shift_day and date_time <= datetime.datetime.now(UTC):
date_time += datetime.timedelta(days=1)
if shift_day and date_time <= dtm.datetime.now(UTC):
date_time += dtm.timedelta(days=1)
return date_time
return time
@ -242,7 +242,7 @@ class JobQueue(Generic[CCT]):
def run_once(
self,
callback: JobCallback[CCT],
when: Union[float, datetime.timedelta, datetime.datetime, datetime.time],
when: Union[float, dtm.timedelta, dtm.datetime, dtm.time],
data: Optional[object] = None,
name: Optional[str] = None,
chat_id: Optional[int] = None,
@ -326,9 +326,9 @@ class JobQueue(Generic[CCT]):
def run_repeating(
self,
callback: JobCallback[CCT],
interval: Union[float, datetime.timedelta],
first: Optional[Union[float, datetime.timedelta, datetime.datetime, datetime.time]] = None,
last: Optional[Union[float, datetime.timedelta, datetime.datetime, datetime.time]] = None,
interval: Union[float, dtm.timedelta],
first: Optional[Union[float, dtm.timedelta, dtm.datetime, dtm.time]] = None,
last: Optional[Union[float, dtm.timedelta, dtm.datetime, dtm.time]] = None,
data: Optional[object] = None,
name: Optional[str] = None,
chat_id: Optional[int] = None,
@ -433,7 +433,7 @@ class JobQueue(Generic[CCT]):
if dt_last and dt_first and dt_last < dt_first:
raise ValueError("'last' must not be before 'first'!")
if isinstance(interval, datetime.timedelta):
if isinstance(interval, dtm.timedelta):
interval = interval.total_seconds()
j = self.scheduler.add_job(
@ -453,7 +453,7 @@ class JobQueue(Generic[CCT]):
def run_monthly(
self,
callback: JobCallback[CCT],
when: datetime.time,
when: dtm.time,
day: int,
data: Optional[object] = None,
name: Optional[str] = None,
@ -531,7 +531,7 @@ class JobQueue(Generic[CCT]):
def run_daily(
self,
callback: JobCallback[CCT],
time: datetime.time,
time: dtm.time,
days: tuple[int, ...] = _ALL_DAYS,
data: Optional[object] = None,
name: Optional[str] = None,
@ -904,7 +904,7 @@ class Job(Generic[CCT]):
self._enabled = status
@property
def next_t(self) -> Optional[datetime.datetime]:
def next_t(self) -> Optional[dtm.datetime]:
"""
:class:`datetime.datetime`: Datetime for the next job execution.
Datetime is localized according to :attr:`datetime.datetime.tzinfo`.

View file

@ -269,7 +269,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
allowed_updates (Sequence[:obj:`str`], optional): Passed to
:meth:`telegram.Bot.get_updates`.
.. versionchanged:: NEXT.VERSION
.. versionchanged:: 21.9
Accepts any :class:`collections.abc.Sequence` as input instead of just a list
drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
Telegram servers before actually starting to poll. Default is :obj:`False`.
@ -523,7 +523,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
allowed_updates (Sequence[:obj:`str`], optional): Passed to
:meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`.
.. versionchanged:: NEXT.VERSION
.. versionchanged:: 21.9
Accepts any :class:`collections.abc.Sequence` as input instead of just a list
max_connections (:obj:`int`, optional): Passed to
:meth:`telegram.Bot.set_webhook`. Defaults to ``40``.

View file

@ -17,10 +17,10 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains a class that describes a single parameter of a request to the Bot API."""
import datetime as dtm
import json
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, final
from telegram._files.inputfile import InputFile
@ -113,7 +113,7 @@ class RequestParameter:
* if a user passes a custom enum, it's unlikely that we can actually properly handle it
even with some special casing.
"""
if isinstance(value, datetime):
if isinstance(value, dtm.datetime):
return to_timestamp(value), []
if isinstance(value, StringEnum):
return value.value, []

View file

@ -699,6 +699,54 @@ class TestStickerSetWithoutRequest(StickerSetTestBase):
monkeypatch.setattr(sticker.get_bot(), "get_file", make_assertion)
assert await sticker.get_file()
async def test_delete_sticker_from_set_sticker_input(self, offline_bot, sticker, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.delete_sticker_from_set(sticker)
async def test_replace_sticker_in_set_sticker_input(self, offline_bot, sticker, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["old_sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.replace_sticker_in_set(
user_id=1, name="name", sticker="sticker", old_sticker=sticker
)
async def test_set_sticker_emoji_list_sticker_input(self, offline_bot, sticker, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.set_sticker_emoji_list(sticker, ["emoji"])
async def test_set_sticker_mask_position_sticker_input(
self, offline_bot, sticker, monkeypatch
):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.set_sticker_mask_position(sticker, MaskPosition("eyes", 1, 2, 3))
async def test_set_sticker_position_in_set_sticker_input(
self, offline_bot, sticker, monkeypatch
):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.set_sticker_position_in_set(sticker, 1)
async def test_set_sticker_keywords_sticker_input(self, offline_bot, sticker, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.set_sticker_keywords(sticker, ["keyword"])
@pytest.mark.xdist_group("stickerset")
class TestStickerSetWithRequest:

View file

View file

@ -0,0 +1,151 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
import pytest
from telegram import AffiliateInfo, Chat, Dice, User
from tests.auxil.slots import mro_slots
@pytest.fixture
def affiliate_info():
return AffiliateInfo(
affiliate_user=AffiliateInfoTestBase.affiliate_user,
affiliate_chat=AffiliateInfoTestBase.affiliate_chat,
commission_per_mille=AffiliateInfoTestBase.commission_per_mille,
amount=AffiliateInfoTestBase.amount,
nanostar_amount=AffiliateInfoTestBase.nanostar_amount,
)
class AffiliateInfoTestBase:
affiliate_user = User(id=1, is_bot=True, first_name="affiliate_user", username="username")
affiliate_chat = Chat(id=2, type="private", title="affiliate_chat")
commission_per_mille = 13
amount = 14
nanostar_amount = -42
class TestAffiliateInfoWithoutRequest(AffiliateInfoTestBase):
def test_slot_behaviour(self, affiliate_info):
inst = affiliate_info
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"affiliate_user": self.affiliate_user.to_dict(),
"affiliate_chat": self.affiliate_chat.to_dict(),
"commission_per_mille": self.commission_per_mille,
"amount": self.amount,
"nanostar_amount": self.nanostar_amount,
}
ai = AffiliateInfo.de_json(json_dict, offline_bot)
assert ai.api_kwargs == {}
assert ai.affiliate_user == self.affiliate_user
assert ai.affiliate_chat == self.affiliate_chat
assert ai.commission_per_mille == self.commission_per_mille
assert ai.amount == self.amount
assert ai.nanostar_amount == self.nanostar_amount
assert AffiliateInfo.de_json(None, offline_bot) is None
assert AffiliateInfo.de_json({}, offline_bot) is None
def test_to_dict(self, affiliate_info):
ai_dict = affiliate_info.to_dict()
assert isinstance(ai_dict, dict)
assert ai_dict["affiliate_user"] == affiliate_info.affiliate_user.to_dict()
assert ai_dict["affiliate_chat"] == affiliate_info.affiliate_chat.to_dict()
assert ai_dict["commission_per_mille"] == affiliate_info.commission_per_mille
assert ai_dict["amount"] == affiliate_info.amount
assert ai_dict["nanostar_amount"] == affiliate_info.nanostar_amount
def test_equality(self, affiliate_info, offline_bot):
a = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
b = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
c = AffiliateInfo(
affiliate_user=User(id=3, is_bot=True, first_name="first_name", username="username"),
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
d = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=Chat(id=3, type="private", title="title"),
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
e = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=1,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
f = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=1,
nanostar_amount=self.nanostar_amount,
)
g = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=1,
)
h = Dice(4, "emoji")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
assert a != f
assert hash(a) != hash(f)
assert a != g
assert hash(a) != hash(g)
assert a != h
assert hash(a) != hash(h)

View file

@ -0,0 +1,235 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
import datetime as dtm
import pytest
from telegram import (
Dice,
RevenueWithdrawalState,
RevenueWithdrawalStateFailed,
RevenueWithdrawalStatePending,
RevenueWithdrawalStateSucceeded,
)
from telegram._utils.datetime import UTC, to_timestamp
from telegram.constants import RevenueWithdrawalStateType
from tests.auxil.slots import mro_slots
@pytest.fixture
def revenue_withdrawal_state():
return RevenueWithdrawalState(RevenueWithdrawalStateTestBase.state)
@pytest.fixture
def revenue_withdrawal_state_pending():
return RevenueWithdrawalStatePending()
@pytest.fixture
def revenue_withdrawal_state_succeeded():
return RevenueWithdrawalStateSucceeded(
date=RevenueWithdrawalStateTestBase.date, url=RevenueWithdrawalStateTestBase.url
)
@pytest.fixture
def revenue_withdrawal_state_failed():
return RevenueWithdrawalStateFailed()
class RevenueWithdrawalStateTestBase:
state = "failed"
date = dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)
url = "url"
class TestRevenueWithdrawalStateWithoutRequest(RevenueWithdrawalStateTestBase):
def test_slot_behaviour(self, revenue_withdrawal_state):
inst = revenue_withdrawal_state
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_type_enum_conversion(self):
assert type(RevenueWithdrawalState("failed").type) is RevenueWithdrawalStateType
assert RevenueWithdrawalState("unknown").type == "unknown"
def test_de_json(self, offline_bot):
json_dict = {"type": "unknown"}
rws = RevenueWithdrawalState.de_json(json_dict, offline_bot)
assert rws.api_kwargs == {}
assert rws.type == "unknown"
assert RevenueWithdrawalState.de_json(None, offline_bot) is None
assert RevenueWithdrawalState.de_json({}, offline_bot) is None
@pytest.mark.parametrize(
("state", "subclass"),
[
("pending", RevenueWithdrawalStatePending),
("succeeded", RevenueWithdrawalStateSucceeded),
("failed", RevenueWithdrawalStateFailed),
],
)
def test_de_json_subclass(self, offline_bot, state, subclass):
json_dict = {"type": state, "date": to_timestamp(self.date), "url": self.url}
rws = RevenueWithdrawalState.de_json(json_dict, offline_bot)
assert type(rws) is subclass
assert set(rws.api_kwargs.keys()) == {"date", "url"} - set(subclass.__slots__)
assert rws.type == state
def test_to_dict(self, revenue_withdrawal_state):
json_dict = revenue_withdrawal_state.to_dict()
assert json_dict == {"type": self.state}
def test_equality(self, revenue_withdrawal_state):
a = revenue_withdrawal_state
b = RevenueWithdrawalState(self.state)
c = RevenueWithdrawalState("pending")
d = Dice(value=1, emoji="🎲")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
class TestRevenueWithdrawalStatePendingWithoutRequest(RevenueWithdrawalStateTestBase):
def test_slot_behaviour(self, revenue_withdrawal_state_pending):
inst = revenue_withdrawal_state_pending
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {}
rws = RevenueWithdrawalStatePending.de_json(json_dict, offline_bot)
assert rws.api_kwargs == {}
assert rws.type == "pending"
assert RevenueWithdrawalStatePending.de_json(None, offline_bot) is None
def test_to_dict(self, revenue_withdrawal_state_pending):
json_dict = revenue_withdrawal_state_pending.to_dict()
assert json_dict == {"type": "pending"}
def test_equality(self, revenue_withdrawal_state_pending):
a = revenue_withdrawal_state_pending
b = RevenueWithdrawalStatePending()
c = RevenueWithdrawalState("unknown")
d = Dice(value=1, emoji="🎲")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
class TestRevenueWithdrawalStateSucceededWithoutRequest(RevenueWithdrawalStateTestBase):
state = RevenueWithdrawalStateType.SUCCEEDED
def test_slot_behaviour(self, revenue_withdrawal_state_succeeded):
inst = revenue_withdrawal_state_succeeded
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {"date": to_timestamp(self.date), "url": self.url}
rws = RevenueWithdrawalStateSucceeded.de_json(json_dict, offline_bot)
assert rws.api_kwargs == {}
assert rws.type == "succeeded"
assert rws.date == self.date
assert rws.url == self.url
assert RevenueWithdrawalStateSucceeded.de_json(None, offline_bot) is None
def test_to_dict(self, revenue_withdrawal_state_succeeded):
json_dict = revenue_withdrawal_state_succeeded.to_dict()
assert json_dict["type"] == "succeeded"
assert json_dict["date"] == to_timestamp(self.date)
assert json_dict["url"] == self.url
def test_equality(self, revenue_withdrawal_state_succeeded):
a = revenue_withdrawal_state_succeeded
b = RevenueWithdrawalStateSucceeded(date=self.date, url=self.url)
c = RevenueWithdrawalStateSucceeded(date=self.date, url="other_url")
d = RevenueWithdrawalStateSucceeded(
date=dtm.datetime(1900, 1, 1, 0, 0, 0, 0, tzinfo=UTC), url=self.url
)
e = Dice(value=1, emoji="🎲")
assert a == b
assert hash(a) == hash(b)
assert a == c
assert hash(a) == hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
class TestRevenueWithdrawalStateFailedWithoutRequest(RevenueWithdrawalStateTestBase):
state = RevenueWithdrawalStateType.FAILED
def test_slot_behaviour(self, revenue_withdrawal_state_failed):
inst = revenue_withdrawal_state_failed
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {}
rws = RevenueWithdrawalStateFailed.de_json(json_dict, offline_bot)
assert rws.api_kwargs == {}
assert rws.type == "failed"
assert RevenueWithdrawalStateFailed.de_json(None, offline_bot) is None
def test_to_dict(self, revenue_withdrawal_state_failed):
json_dict = revenue_withdrawal_state_failed.to_dict()
assert json_dict == {"type": "failed"}
def test_equality(self, revenue_withdrawal_state_failed):
a = revenue_withdrawal_state_failed
b = RevenueWithdrawalStateFailed()
c = RevenueWithdrawalState("unknown")
d = Dice(value=1, emoji="🎲")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)

View file

@ -0,0 +1,207 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
import datetime as dtm
import pytest
from telegram import (
StarTransaction,
StarTransactions,
TransactionPartnerOther,
TransactionPartnerUser,
User,
)
from telegram._utils.datetime import UTC, from_timestamp, to_timestamp
from tests.auxil.slots import mro_slots
def star_transaction_factory():
return StarTransaction(
id=StarTransactionTestBase.id,
amount=StarTransactionTestBase.amount,
nanostar_amount=StarTransactionTestBase.nanostar_amount,
date=from_timestamp(StarTransactionTestBase.date),
source=StarTransactionTestBase.source,
receiver=StarTransactionTestBase.receiver,
)
@pytest.fixture
def star_transaction():
return star_transaction_factory()
@pytest.fixture
def star_transactions():
return StarTransactions(
transactions=[
star_transaction_factory(),
star_transaction_factory(),
]
)
class StarTransactionTestBase:
id = "2"
amount = 2
nanostar_amount = 365
date = to_timestamp(dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC))
source = TransactionPartnerUser(
user=User(
id=2,
is_bot=False,
first_name="first_name",
),
)
receiver = TransactionPartnerOther()
class TestStarTransactionWithoutRequest(StarTransactionTestBase):
def test_slot_behaviour(self, star_transaction):
inst = star_transaction
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"id": self.id,
"amount": self.amount,
"nanostar_amount": self.nanostar_amount,
"date": self.date,
"source": self.source.to_dict(),
"receiver": self.receiver.to_dict(),
}
st = StarTransaction.de_json(json_dict, offline_bot)
st_none = StarTransaction.de_json(None, offline_bot)
assert st.api_kwargs == {}
assert st.id == self.id
assert st.amount == self.amount
assert st.nanostar_amount == self.nanostar_amount
assert st.date == from_timestamp(self.date)
assert st.source == self.source
assert st.receiver == self.receiver
assert st_none is None
def test_de_json_star_transaction_localization(
self, tz_bot, offline_bot, raw_bot, star_transaction
):
json_dict = star_transaction.to_dict()
st_raw = StarTransaction.de_json(json_dict, raw_bot)
st_bot = StarTransaction.de_json(json_dict, offline_bot)
st_tz = StarTransaction.de_json(json_dict, tz_bot)
# comparing utcoffsets because comparing timezones is unpredicatable
st_offset = st_tz.date.utcoffset()
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(st_tz.date.replace(tzinfo=None))
assert st_raw.date.tzinfo == UTC
assert st_bot.date.tzinfo == UTC
assert st_offset == tz_bot_offset
def test_to_dict(self, star_transaction):
expected_dict = {
"id": self.id,
"amount": self.amount,
"nanostar_amount": self.nanostar_amount,
"date": self.date,
"source": self.source.to_dict(),
"receiver": self.receiver.to_dict(),
}
assert star_transaction.to_dict() == expected_dict
def test_equality(self):
a = StarTransaction(
id=self.id,
amount=self.amount,
date=self.date,
source=self.source,
receiver=self.receiver,
)
b = StarTransaction(
id=self.id,
amount=self.amount,
date=None,
source=self.source,
receiver=self.receiver,
)
c = StarTransaction(
id="3",
amount=3,
date=to_timestamp(dtm.datetime.utcnow()),
source=TransactionPartnerUser(
user=User(
id=3,
is_bot=False,
first_name="first_name",
),
),
receiver=TransactionPartnerOther(),
)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
class StarTransactionsTestBase:
transactions = [star_transaction_factory(), star_transaction_factory()]
class TestStarTransactionsWithoutRequest(StarTransactionsTestBase):
def test_slot_behaviour(self, star_transactions):
inst = star_transactions
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"transactions": [t.to_dict() for t in self.transactions],
}
st = StarTransactions.de_json(json_dict, offline_bot)
st_none = StarTransactions.de_json(None, offline_bot)
assert st.api_kwargs == {}
assert st.transactions == tuple(self.transactions)
assert st_none is None
def test_to_dict(self, star_transactions):
expected_dict = {
"transactions": [t.to_dict() for t in self.transactions],
}
assert star_transactions.to_dict() == expected_dict
def test_equality(self, star_transaction):
a = StarTransactions(
transactions=self.transactions,
)
b = StarTransactions(
transactions=self.transactions,
)
c = StarTransactions(
transactions=[star_transaction],
)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)

View file

@ -0,0 +1,469 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
import datetime as dtm
import pytest
from telegram import (
AffiliateInfo,
Gift,
PaidMediaVideo,
RevenueWithdrawalStatePending,
Sticker,
TransactionPartner,
TransactionPartnerAffiliateProgram,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
TransactionPartnerTelegramApi,
TransactionPartnerUser,
User,
Video,
)
from telegram.constants import TransactionPartnerType
from tests.auxil.slots import mro_slots
@pytest.fixture
def transaction_partner():
return TransactionPartner(TransactionPartnerTestBase.type)
class TransactionPartnerTestBase:
type = TransactionPartnerType.AFFILIATE_PROGRAM
commission_per_mille = 42
sponsor_user = User(
id=1,
is_bot=False,
first_name="sponsor",
last_name="user",
)
withdrawal_state = RevenueWithdrawalStatePending()
user = User(
id=2,
is_bot=False,
first_name="user",
last_name="user",
)
invoice_payload = "invoice_payload"
paid_media = (
PaidMediaVideo(
video=Video(
file_id="file_id",
file_unique_id="file_unique_id",
width=10,
height=10,
duration=10,
)
),
)
paid_media_payload = "paid_media_payload"
subscription_period = dtm.timedelta(days=1)
gift = Gift(
id="gift_id",
sticker=Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=10,
height=10,
is_animated=False,
is_video=False,
type="type",
),
total_count=42,
remaining_count=42,
star_count=42,
)
affiliate = AffiliateInfo(
commission_per_mille=42,
amount=42,
)
request_count = 42
class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
def test_slot_behaviour(self, transaction_partner):
inst = transaction_partner
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_type_enum_conversion(self, transaction_partner):
assert type(TransactionPartner("affiliate_program").type) is TransactionPartnerType
assert TransactionPartner("unknown").type == "unknown"
def test_de_json(self, offline_bot):
data = {"type": "unknown"}
transaction_partner = TransactionPartner.de_json(data, offline_bot)
assert transaction_partner.api_kwargs == {}
assert transaction_partner.type == "unknown"
assert TransactionPartner.de_json(None, offline_bot) is None
assert TransactionPartner.de_json({}, offline_bot) is None
@pytest.mark.parametrize(
("tp_type", "subclass"),
[
("affiliate_program", TransactionPartnerAffiliateProgram),
("fragment", TransactionPartnerFragment),
("user", TransactionPartnerUser),
("telegram_ads", TransactionPartnerTelegramAds),
("telegram_api", TransactionPartnerTelegramApi),
("other", TransactionPartnerOther),
],
)
def test_subclass(self, offline_bot, tp_type, subclass):
json_dict = {
"type": tp_type,
"commission_per_mille": self.commission_per_mille,
"user": self.user.to_dict(),
"request_count": self.request_count,
}
tp = TransactionPartner.de_json(json_dict, offline_bot)
assert type(tp) is subclass
assert set(tp.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
"type"
}
assert tp.type == tp_type
def test_to_dict(self, transaction_partner):
data = transaction_partner.to_dict()
assert data == {"type": self.type}
def test_equality(self, transaction_partner):
a = transaction_partner
b = TransactionPartner(self.type)
c = TransactionPartner("unknown")
d = User(id=1, is_bot=False, first_name="user", last_name="user")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
@pytest.fixture
def transaction_partner_affiliate_program():
return TransactionPartnerAffiliateProgram(
commission_per_mille=TransactionPartnerTestBase.commission_per_mille,
sponsor_user=TransactionPartnerTestBase.sponsor_user,
)
class TestTransactionPartnerAffiliateProgramWithoutRequest(TransactionPartnerTestBase):
type = TransactionPartnerType.AFFILIATE_PROGRAM
def test_slot_behaviour(self, transaction_partner_affiliate_program):
inst = transaction_partner_affiliate_program
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"commission_per_mille": self.commission_per_mille,
"sponsor_user": self.sponsor_user.to_dict(),
}
tp = TransactionPartnerAffiliateProgram.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.type == "affiliate_program"
assert tp.commission_per_mille == self.commission_per_mille
assert tp.sponsor_user == self.sponsor_user
assert TransactionPartnerAffiliateProgram.de_json(None, offline_bot) is None
assert TransactionPartnerAffiliateProgram.de_json({}, offline_bot) is None
def test_to_dict(self, transaction_partner_affiliate_program):
json_dict = transaction_partner_affiliate_program.to_dict()
assert json_dict["type"] == self.type
assert json_dict["commission_per_mille"] == self.commission_per_mille
assert json_dict["sponsor_user"] == self.sponsor_user.to_dict()
def test_equality(self, transaction_partner_affiliate_program):
a = transaction_partner_affiliate_program
b = TransactionPartnerAffiliateProgram(
commission_per_mille=self.commission_per_mille,
)
c = TransactionPartnerAffiliateProgram(
commission_per_mille=0,
)
d = User(id=1, is_bot=False, first_name="user", last_name="user")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
@pytest.fixture
def transaction_partner_fragment():
return TransactionPartnerFragment(
withdrawal_state=TransactionPartnerTestBase.withdrawal_state,
)
class TestTransactionPartnerFragmentWithoutRequest(TransactionPartnerTestBase):
type = TransactionPartnerType.FRAGMENT
def test_slot_behaviour(self, transaction_partner_fragment):
inst = transaction_partner_fragment
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {"withdrawal_state": self.withdrawal_state.to_dict()}
tp = TransactionPartnerFragment.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.type == "fragment"
assert tp.withdrawal_state == self.withdrawal_state
assert TransactionPartnerFragment.de_json(None, offline_bot) is None
def test_to_dict(self, transaction_partner_fragment):
json_dict = transaction_partner_fragment.to_dict()
assert json_dict["type"] == self.type
assert json_dict["withdrawal_state"] == self.withdrawal_state.to_dict()
def test_equality(self, transaction_partner_fragment):
a = transaction_partner_fragment
b = TransactionPartnerFragment(withdrawal_state=self.withdrawal_state)
c = TransactionPartnerFragment(withdrawal_state=None)
d = User(id=1, is_bot=False, first_name="user", last_name="user")
assert a == b
assert hash(a) == hash(b)
assert a == c
assert hash(a) == hash(c)
assert a != d
assert hash(a) != hash(d)
@pytest.fixture
def transaction_partner_user():
return TransactionPartnerUser(
user=TransactionPartnerTestBase.user,
invoice_payload=TransactionPartnerTestBase.invoice_payload,
paid_media=TransactionPartnerTestBase.paid_media,
paid_media_payload=TransactionPartnerTestBase.paid_media_payload,
subscription_period=TransactionPartnerTestBase.subscription_period,
)
class TestTransactionPartnerUserWithoutRequest(TransactionPartnerTestBase):
type = TransactionPartnerType.USER
def test_slot_behaviour(self, transaction_partner_user):
inst = transaction_partner_user
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"user": self.user.to_dict(),
"invoice_payload": self.invoice_payload,
"paid_media": [pm.to_dict() for pm in self.paid_media],
"paid_media_payload": self.paid_media_payload,
"subscription_period": self.subscription_period.total_seconds(),
}
tp = TransactionPartnerUser.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.type == "user"
assert tp.user == self.user
assert tp.invoice_payload == self.invoice_payload
assert tp.paid_media == self.paid_media
assert tp.paid_media_payload == self.paid_media_payload
assert tp.subscription_period == self.subscription_period
assert TransactionPartnerUser.de_json(None, offline_bot) is None
assert TransactionPartnerUser.de_json({}, offline_bot) is None
def test_to_dict(self, transaction_partner_user):
json_dict = transaction_partner_user.to_dict()
assert json_dict["type"] == self.type
assert json_dict["user"] == self.user.to_dict()
assert json_dict["invoice_payload"] == self.invoice_payload
assert json_dict["paid_media"] == [pm.to_dict() for pm in self.paid_media]
assert json_dict["paid_media_payload"] == self.paid_media_payload
assert json_dict["subscription_period"] == self.subscription_period.total_seconds()
def test_equality(self, transaction_partner_user):
a = transaction_partner_user
b = TransactionPartnerUser(
user=self.user,
)
c = TransactionPartnerUser(
user=User(id=1, is_bot=False, first_name="user", last_name="user"),
)
d = User(id=1, is_bot=False, first_name="user", last_name="user")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
@pytest.fixture
def transaction_partner_other():
return TransactionPartnerOther()
class TestTransactionPartnerOtherWithoutRequest(TransactionPartnerTestBase):
type = TransactionPartnerType.OTHER
def test_slot_behaviour(self, transaction_partner_other):
inst = transaction_partner_other
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {}
tp = TransactionPartnerOther.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.type == "other"
assert TransactionPartnerOther.de_json(None, offline_bot) is None
def test_to_dict(self, transaction_partner_other):
json_dict = transaction_partner_other.to_dict()
assert json_dict == {"type": self.type}
def test_equality(self, transaction_partner_other):
a = transaction_partner_other
b = TransactionPartnerOther()
c = TransactionPartnerOther()
d = User(id=1, is_bot=False, first_name="user", last_name="user")
assert a == b
assert hash(a) == hash(b)
assert a == c
assert hash(a) == hash(c)
assert a != d
assert hash(a) != hash(d)
@pytest.fixture
def transaction_partner_telegram_ads():
return TransactionPartnerTelegramAds()
class TestTransactionPartnerTelegramAdsWithoutRequest(TransactionPartnerTestBase):
type = TransactionPartnerType.TELEGRAM_ADS
def test_slot_behaviour(self, transaction_partner_telegram_ads):
inst = transaction_partner_telegram_ads
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {}
tp = TransactionPartnerTelegramAds.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.type == "telegram_ads"
assert TransactionPartnerTelegramAds.de_json(None, offline_bot) is None
def test_to_dict(self, transaction_partner_telegram_ads):
json_dict = transaction_partner_telegram_ads.to_dict()
assert json_dict == {"type": self.type}
def test_equality(self, transaction_partner_telegram_ads):
a = transaction_partner_telegram_ads
b = TransactionPartnerTelegramAds()
c = TransactionPartnerTelegramAds()
d = User(id=1, is_bot=False, first_name="user", last_name="user")
assert a == b
assert hash(a) == hash(b)
assert a == c
assert hash(a) == hash(c)
assert a != d
assert hash(a) != hash(d)
@pytest.fixture
def transaction_partner_telegram_api():
return TransactionPartnerTelegramApi(
request_count=TransactionPartnerTestBase.request_count,
)
class TestTransactionPartnerTelegramApiWithoutRequest(TransactionPartnerTestBase):
type = TransactionPartnerType.TELEGRAM_API
def test_slot_behaviour(self, transaction_partner_telegram_api):
inst = transaction_partner_telegram_api
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {"request_count": self.request_count}
tp = TransactionPartnerTelegramApi.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.type == "telegram_api"
assert tp.request_count == self.request_count
assert TransactionPartnerTelegramApi.de_json(None, offline_bot) is None
def test_to_dict(self, transaction_partner_telegram_api):
json_dict = transaction_partner_telegram_api.to_dict()
assert json_dict["type"] == self.type
assert json_dict["request_count"] == self.request_count
def test_equality(self, transaction_partner_telegram_api):
a = transaction_partner_telegram_api
b = TransactionPartnerTelegramApi(
request_count=self.request_count,
)
c = TransactionPartnerTelegramApi(
request_count=0,
)
d = User(id=1, is_bot=False, first_name="user", last_name="user")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)

View file

@ -17,7 +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/].
"""Provides functions to test both methods."""
import datetime
import datetime as dtm
import functools
import inspect
import re
@ -38,6 +38,7 @@ from telegram import (
InputTextMessageContent,
LinkPreviewOptions,
ReplyParameters,
Sticker,
TelegramObject,
)
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
@ -318,6 +319,16 @@ def build_kwargs(
kws["error_message"] = "error"
elif name == "options":
kws[name] = ["option1", "option2"]
elif name in ("sticker", "old_sticker"):
kws[name] = Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=1,
height=1,
is_animated=False,
is_video=False,
type="regular",
)
else:
kws[name] = True
@ -337,12 +348,10 @@ def build_kwargs(
elif name == "until_date":
if manually_passed_value not in [None, DEFAULT_NONE]:
# Europe/Berlin
kws[name] = datetime.datetime(
2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")
)
kws[name] = dtm.datetime(2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin"))
else:
# naive UTC
kws[name] = datetime.datetime(2000, 1, 1, 0)
kws[name] = dtm.datetime(2000, 1, 1, 0)
elif name == "reply_parameters":
kws[name] = telegram.ReplyParameters(
message_id=1,

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import re
from telegram import Chat, Message, MessageEntity, Update, User
@ -24,7 +24,7 @@ from tests.auxil.ci_bots import BOT_INFO_PROVIDER
from tests.auxil.pytest_classes import make_bot
CMD_PATTERN = re.compile(r"/[\da-z_]{1,32}(?:@\w{1,32})?")
DATE = datetime.datetime.now()
DATE = dtm.datetime.now()
def make_message(text: str, offline: bool = True, **kwargs):

View file

@ -16,10 +16,10 @@
#
# 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 datetime
import datetime as dtm
class BasicTimezone(datetime.tzinfo):
class BasicTimezone(dtm.tzinfo):
def __init__(self, offset, name):
self.offset = offset
self.name = name
@ -28,4 +28,4 @@ class BasicTimezone(datetime.tzinfo):
return self.offset
def dst(self, dt):
return datetime.timedelta(0)
return dtm.timedelta(0)

View file

@ -17,7 +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
import datetime as dtm
import pytest
@ -71,7 +71,7 @@ def false_update(request):
@pytest.fixture(scope="class")
def time():
return datetime.datetime.now(tz=UTC)
return dtm.datetime.now(tz=UTC)
@pytest.fixture(scope="class")
@ -80,7 +80,7 @@ def business_connection(bot):
id="1",
user_chat_id=1,
user=User(1, "name", username="user_a", is_bot=False),
date=datetime.datetime.now(tz=UTC),
date=dtm.datetime.now(tz=UTC),
can_reply=True,
is_enabled=True,
)

View file

@ -17,7 +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
import datetime as dtm
import pytest
@ -71,7 +71,7 @@ def false_update(request):
@pytest.fixture(scope="class")
def time():
return datetime.datetime.now(tz=UTC)
return dtm.datetime.now(tz=UTC)
@pytest.fixture(scope="class")

View file

@ -16,9 +16,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/].
import datetime as dtm
import time
from copy import deepcopy
from datetime import datetime
from uuid import uuid4
import pytest
@ -181,7 +181,9 @@ class TestCallbackDataCache:
callback_data_cache.clear_callback_data()
chat = Chat(1, "private")
effective_message = Message(message_id=1, date=datetime.now(), chat=chat, reply_markup=out)
effective_message = Message(
message_id=1, date=dtm.datetime.now(), chat=chat, reply_markup=out
)
effective_message._unfreeze()
effective_message.reply_to_message = deepcopy(effective_message)
effective_message.pinned_message = deepcopy(effective_message)
@ -374,9 +376,9 @@ class TestCallbackDataCache:
if time_method == "time":
cutoff = time.time()
elif time_method == "datetime":
cutoff = datetime.now(UTC)
cutoff = dtm.datetime.now(UTC)
else:
cutoff = datetime.now(tz_bot.defaults.tzinfo).replace(tzinfo=None)
cutoff = dtm.datetime.now(tz_bot.defaults.tzinfo).replace(tzinfo=None)
callback_data_cache.bot = tz_bot
time.sleep(0.1)

View file

@ -17,7 +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
import datetime as dtm
import pytest
@ -72,7 +72,7 @@ def false_update(request):
@pytest.fixture(scope="class")
def time():
return datetime.datetime.now(tz=UTC)
return dtm.datetime.now(tz=UTC)
@pytest.fixture(scope="class")

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import inspect
import re
@ -49,14 +49,12 @@ def update():
0,
Message(
0,
datetime.datetime.utcnow(),
dtm.datetime.utcnow(),
Chat(0, "private"),
from_user=User(0, "Testuser", False),
via_bot=User(0, "Testbot", True),
sender_chat=Chat(0, "Channel"),
forward_origin=MessageOriginUser(
datetime.datetime.utcnow(), User(0, "Testuser", False)
),
forward_origin=MessageOriginUser(dtm.datetime.utcnow(), User(0, "Testuser", False)),
),
)
update._unfreeze()
@ -86,7 +84,7 @@ def base_class(request):
@pytest.fixture(scope="class")
def message_origin_user():
return MessageOriginUser(datetime.datetime.utcnow(), User(1, "TestOther", False))
return MessageOriginUser(dtm.datetime.utcnow(), User(1, "TestOther", False))
class TestFilters:
@ -155,7 +153,7 @@ class TestFilters:
not key.startswith("_")
# exclude imported stuff
and getattr(member, "__module__", "unknown module") == "telegram.ext.filters"
and key != "sys"
and key not in ("sys", "dtm")
)
}
actual = set(filters.__all__)
@ -616,7 +614,7 @@ class TestFilters:
def test_filters_reply(self, update):
another_message = Message(
1,
datetime.datetime.utcnow(),
dtm.datetime.utcnow(),
Chat(0, "private"),
from_user=User(1, "TestOther", False),
)
@ -1107,7 +1105,7 @@ class TestFilters:
def test_filters_forwarded(self, update, message_origin_user):
assert filters.FORWARDED.check_update(update)
update.message.forward_origin = MessageOriginHiddenUser(datetime.datetime.utcnow(), 1)
update.message.forward_origin = MessageOriginHiddenUser(dtm.datetime.utcnow(), 1)
assert filters.FORWARDED.check_update(update)
update.message.forward_origin = None
assert not filters.FORWARDED.check_update(update)
@ -2681,7 +2679,7 @@ class TestFilters:
0,
Message(
0,
datetime.datetime.utcnow(),
dtm.datetime.utcnow(),
Chat(0, "private"),
document=Document("str", "other_str"),
),

View file

@ -35,9 +35,7 @@ if TEST_WITH_OPT_DEPS:
UTC = pytz.utc
else:
import datetime
UTC = datetime.timezone.utc
UTC = dtm.timezone.utc
class CustomContext(CallbackContext):

View file

@ -17,7 +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
import datetime as dtm
import pytest
@ -74,7 +74,7 @@ def false_update(request):
@pytest.fixture(scope="class")
def time():
return datetime.datetime.now(tz=UTC)
return dtm.datetime.now(tz=UTC)
@pytest.fixture(scope="class")

View file

@ -17,7 +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
import datetime as dtm
import pytest
@ -71,7 +71,7 @@ def false_update(request):
@pytest.fixture(scope="class")
def time():
return datetime.datetime.now(tz=UTC)
return dtm.datetime.now(tz=UTC)
@pytest.fixture(scope="class")

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import gzip
import os
import pickle
@ -229,7 +229,7 @@ def pickle_files_wo_callback_data(user_data, chat_data, bot_data, conversations)
def update(bot):
user = User(id=321, first_name="test_user", is_bot=False)
chat = Chat(id=123, type="group")
message = Message(1, datetime.datetime.now(), chat, from_user=user, text="Hi there")
message = Message(1, dtm.datetime.now(), chat, from_user=user, text="Hi there")
message.set_bot(bot)
return Update(0, message=message)
@ -289,7 +289,7 @@ class TestPicklePersistence:
async def test_pickle_behaviour_with_slots(self, pickle_persistence):
bot_data = await pickle_persistence.get_bot_data()
bot_data["message"] = Message(3, datetime.datetime.now(), Chat(2, type="supergroup"))
bot_data["message"] = Message(3, dtm.datetime.now(), Chat(2, type="supergroup"))
await pickle_persistence.update_bot_data(bot_data)
retrieved = await pickle_persistence.get_bot_data()
assert retrieved == bot_data

View file

@ -22,10 +22,10 @@ We mostly test on directly on AIORateLimiter here, b/c BaseRateLimiter doesn't c
notable
"""
import asyncio
import datetime as dtm
import json
import platform
import time
from datetime import datetime
from http import HTTPStatus
import pytest
@ -181,7 +181,7 @@ class TestAIORateLimiter:
{
"ok": True,
"result": Message(
message_id=1, date=datetime.now(), chat=Chat(1, "chat")
message_id=1, date=dtm.datetime.now(), chat=Chat(1, "chat")
).to_dict(),
}
).encode(),

View file

@ -16,7 +16,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 datetime
import datetime as dtm
from collections.abc import Sequence
import pytest
@ -82,14 +82,14 @@ class TestRequestParameterWithoutRequest:
({1: 1.0}, {1: 1.0}),
(ChatType.PRIVATE, "private"),
(MessageEntity("type", 1, 1), {"type": "type", "offset": 1, "length": 1}),
(datetime.datetime(2019, 11, 11, 0, 26, 16, 10**5), 1573431976),
(dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5), 1573431976),
(
[
True,
"str",
MessageEntity("type", 1, 1),
ChatType.PRIVATE,
datetime.datetime(2019, 11, 11, 0, 26, 16, 10**5),
dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5),
],
[True, "str", {"type": "type", "offset": 1, "length": 1}, "private", 1573431976],
),

View file

@ -16,7 +16,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/].
from datetime import date
import datetime as dtm
import pytest
@ -72,10 +72,10 @@ class TestBirthdateWithoutRequest(BirthdateTestBase):
assert hash(bd1) != hash(bd4)
def test_to_date(self, birthdate):
assert isinstance(birthdate.to_date(), date)
assert birthdate.to_date() == date(self.year, self.month, self.day)
assert isinstance(birthdate.to_date(), dtm.date)
assert birthdate.to_date() == dtm.date(self.year, self.month, self.day)
new_bd = birthdate.to_date(2023)
assert new_bd == date(2023, self.month, self.day)
assert new_bd == dtm.date(2023, self.month, self.day)
def test_to_date_no_year(self):
bd = Birthdate(1, 1)

View file

@ -16,7 +16,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/].
from datetime import datetime
import datetime as dtm
import pytest
@ -40,7 +40,7 @@ class BusinessTestBase:
id_ = "123"
user = User(123, "test_user", False)
user_chat_id = 123
date = datetime.now(tz=UTC).replace(microsecond=0)
date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
can_reply = True
is_enabled = True
message_ids = (123, 321)

View file

@ -17,7 +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/].
from datetime import datetime
import datetime as dtm
import pytest
@ -58,7 +58,9 @@ class CallbackQueryTestBase:
id_ = "id"
from_user = User(1, "test_user", False)
chat_instance = "chat_instance"
message = Message(3, datetime.utcnow(), Chat(4, "private"), from_user=User(5, "bot", False))
message = Message(
3, dtm.datetime.utcnow(), Chat(4, "private"), from_user=User(5, "bot", False)
)
data = "data"
inline_message_id = "inline_message_id"
game_short_name = "the_game"

View file

@ -15,7 +15,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 datetime
import datetime as dtm
import inspect
from copy import deepcopy
@ -48,7 +48,7 @@ class ChatBoostDefaults:
is_unclaimed = False
chat = Chat(1, "group")
user = User(1, "user", False)
date = to_timestamp(datetime.datetime.utcnow())
date = to_timestamp(dtm.datetime.utcnow())
default_source = ChatBoostSourcePremium(user)
prize_star_count = 99
@ -148,7 +148,7 @@ def iter_args(
if param.name in ignored:
continue
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
if isinstance(json_at, datetime.datetime): # Convert datetime to int
if isinstance(json_at, dtm.datetime): # Convert dtm to int
json_at = to_timestamp(json_at)
if (
param.default is not inspect.Parameter.empty and include_optional
@ -275,8 +275,8 @@ class TestChatBoostWithoutRequest(ChatBoostDefaults):
cb = ChatBoost.de_json(json_dict, offline_bot)
assert isinstance(cb, ChatBoost)
assert isinstance(cb.add_date, datetime.datetime)
assert isinstance(cb.expiration_date, datetime.datetime)
assert isinstance(cb.add_date, dtm.datetime)
assert isinstance(cb.expiration_date, dtm.datetime)
assert isinstance(cb.source, ChatBoostSource)
with cb._unfrozen():
cb.add_date = to_timestamp(cb.add_date)

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import pytest
@ -116,7 +116,7 @@ class ChatFullInfoTestBase:
is_forum = True
active_usernames = ["These", "Are", "Usernames!"]
emoji_status_custom_emoji_id = "VeryUniqueCustomEmojiID"
emoji_status_expiration_date = datetime.datetime.now(tz=UTC).replace(microsecond=0)
emoji_status_expiration_date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
has_aggressive_anti_spam_enabled = True
has_hidden_members = True
available_reactions = [

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import pytest
@ -52,7 +52,7 @@ class ChatInviteLinkTestBase:
creates_join_request = False
primary = True
revoked = False
expire_date = datetime.datetime.now(datetime.timezone.utc)
expire_date = dtm.datetime.now(dtm.timezone.utc)
member_limit = 42
name = "LinkName"
pending_join_request_count = 42
@ -107,7 +107,7 @@ class TestChatInviteLinkWithoutRequest(ChatInviteLinkTestBase):
assert invite_link.creates_join_request == self.creates_join_request
assert invite_link.is_primary == self.primary
assert invite_link.is_revoked == self.revoked
assert abs(invite_link.expire_date - self.expire_date) < datetime.timedelta(seconds=1)
assert abs(invite_link.expire_date - self.expire_date) < dtm.timedelta(seconds=1)
assert to_timestamp(invite_link.expire_date) == to_timestamp(self.expire_date)
assert invite_link.member_limit == self.member_limit
assert invite_link.name == self.name

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import pytest
@ -32,7 +32,7 @@ from tests.auxil.slots import mro_slots
@pytest.fixture(scope="module")
def time():
return datetime.datetime.now(tz=UTC)
return dtm.datetime.now(tz=UTC)
@pytest.fixture(scope="module")
@ -82,7 +82,7 @@ class TestChatJoinRequestWithoutRequest(ChatJoinRequestTestBase):
assert chat_join_request.chat == self.chat
assert chat_join_request.from_user == self.from_user
assert abs(chat_join_request.date - time) < datetime.timedelta(seconds=1)
assert abs(chat_join_request.date - time) < dtm.timedelta(seconds=1)
assert to_timestamp(chat_join_request.date) == to_timestamp(time)
assert chat_join_request.user_chat_id == self.from_user.id
@ -92,7 +92,7 @@ class TestChatJoinRequestWithoutRequest(ChatJoinRequestTestBase):
assert chat_join_request.chat == self.chat
assert chat_join_request.from_user == self.from_user
assert abs(chat_join_request.date - time) < datetime.timedelta(seconds=1)
assert abs(chat_join_request.date - time) < dtm.timedelta(seconds=1)
assert to_timestamp(chat_join_request.date) == to_timestamp(time)
assert chat_join_request.user_chat_id == self.from_user.id
assert chat_join_request.bio == self.bio
@ -133,9 +133,7 @@ class TestChatJoinRequestWithoutRequest(ChatJoinRequestTestBase):
a = chat_join_request
b = ChatJoinRequest(self.chat, self.from_user, time, self.from_user.id)
c = ChatJoinRequest(self.chat, self.from_user, time, self.from_user.id, bio="bio")
d = ChatJoinRequest(
self.chat, self.from_user, time + datetime.timedelta(1), self.from_user.id
)
d = ChatJoinRequest(self.chat, self.from_user, time + dtm.timedelta(1), self.from_user.id)
e = ChatJoinRequest(self.chat, User(-1, "last_name", True), time, -1)
f = User(456, "", False)

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import inspect
from copy import deepcopy
@ -43,7 +43,7 @@ class CMDefaults:
user = User(1, "First name", False)
custom_title: str = "PTB"
is_anonymous: bool = True
until_date: datetime.datetime = to_timestamp(datetime.datetime.utcnow())
until_date: dtm.datetime = to_timestamp(dtm.datetime.utcnow())
can_be_edited: bool = False
can_change_info: bool = True
can_post_messages: bool = True
@ -173,7 +173,7 @@ def iter_args(instance: ChatMember, de_json_inst: ChatMember, include_optional:
if param.name in ignored:
continue
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
if isinstance(json_at, datetime.datetime): # Convert datetime to int
if isinstance(json_at, dtm.datetime): # Convert dtm to int
json_at = to_timestamp(json_at)
if (
param.default is not inspect.Parameter.empty and include_optional

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import inspect
import pytest
@ -72,7 +72,7 @@ def new_chat_member(user):
@pytest.fixture(scope="module")
def time():
return datetime.datetime.now(tz=UTC)
return dtm.datetime.now(tz=UTC)
@pytest.fixture(scope="module")
@ -115,7 +115,7 @@ class TestChatMemberUpdatedWithoutRequest(ChatMemberUpdatedTestBase):
assert chat_member_updated.chat == chat
assert chat_member_updated.from_user == user
assert abs(chat_member_updated.date - time) < datetime.timedelta(seconds=1)
assert abs(chat_member_updated.date - time) < dtm.timedelta(seconds=1)
assert to_timestamp(chat_member_updated.date) == to_timestamp(time)
assert chat_member_updated.old_chat_member == old_chat_member
assert chat_member_updated.new_chat_member == new_chat_member
@ -141,7 +141,7 @@ class TestChatMemberUpdatedWithoutRequest(ChatMemberUpdatedTestBase):
assert chat_member_updated.chat == chat
assert chat_member_updated.from_user == user
assert abs(chat_member_updated.date - time) < datetime.timedelta(seconds=1)
assert abs(chat_member_updated.date - time) < dtm.timedelta(seconds=1)
assert to_timestamp(chat_member_updated.date) == to_timestamp(time)
assert chat_member_updated.old_chat_member == old_chat_member
assert chat_member_updated.new_chat_member == new_chat_member
@ -221,7 +221,7 @@ class TestChatMemberUpdatedWithoutRequest(ChatMemberUpdatedTestBase):
c = ChatMemberUpdated(
Chat(1, "chat"),
User(1, "", False),
time + datetime.timedelta(hours=1),
time + dtm.timedelta(hours=1),
old_chat_member,
new_chat_member,
)
@ -264,7 +264,7 @@ class TestChatMemberUpdatedWithoutRequest(ChatMemberUpdatedTestBase):
old_chat_member = ChatMember(user, "old_status")
new_chat_member = ChatMember(user, "new_status")
chat_member_updated = ChatMemberUpdated(
chat, user, datetime.datetime.utcnow(), old_chat_member, new_chat_member
chat, user, dtm.datetime.utcnow(), old_chat_member, new_chat_member
)
assert chat_member_updated.difference() == {"status": ("old_status", "new_status")}
@ -273,7 +273,7 @@ class TestChatMemberUpdatedWithoutRequest(ChatMemberUpdatedTestBase):
new_user = User(1, "First name", False, last_name="last name")
new_chat_member = ChatMember(new_user, "new_status")
chat_member_updated = ChatMemberUpdated(
chat, user, datetime.datetime.utcnow(), old_chat_member, new_chat_member
chat, user, dtm.datetime.utcnow(), old_chat_member, new_chat_member
)
assert chat_member_updated.difference() == {
"status": ("old_status", "new_status"),
@ -321,22 +321,22 @@ class TestChatMemberUpdatedWithoutRequest(ChatMemberUpdatedTestBase):
can_post_stories=True,
)
chat_member_updated = ChatMemberUpdated(
chat, user, datetime.datetime.utcnow(), old_chat_member, new_chat_member
chat, user, dtm.datetime.utcnow(), old_chat_member, new_chat_member
)
assert chat_member_updated.difference() == {optional_attribute: (old_value, new_value)}
def test_difference_different_classes(self, user, chat):
old_chat_member = ChatMemberOwner(user=user, is_anonymous=False)
new_chat_member = ChatMemberBanned(user=user, until_date=datetime.datetime(2021, 1, 1))
new_chat_member = ChatMemberBanned(user=user, until_date=dtm.datetime(2021, 1, 1))
chat_member_updated = ChatMemberUpdated(
chat=chat,
from_user=user,
date=datetime.datetime.utcnow(),
date=dtm.datetime.utcnow(),
old_chat_member=old_chat_member,
new_chat_member=new_chat_member,
)
diff = chat_member_updated.difference()
assert diff.pop("is_anonymous") == (False, None)
assert diff.pop("until_date") == (None, datetime.datetime(2021, 1, 1))
assert diff.pop("until_date") == (None, dtm.datetime(2021, 1, 1))
assert diff.pop("status") == (ChatMember.OWNER, ChatMember.BANNED)
assert diff == {}

View file

@ -58,7 +58,7 @@ class TestConstantsWithoutRequest:
not key.startswith("_")
# exclude imported stuff
and getattr(member, "__module__", "telegram.constants") == "telegram.constants"
and key not in ("sys", "datetime")
and key not in ("sys", "dtm")
)
}
actual = set(constants.__all__)

View file

@ -17,7 +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
import datetime as dtm
import pytest
@ -243,7 +243,7 @@ class TestForumMethodsWithRequest:
async def test_edit_general_forum_topic(self, bot, forum_group_id):
result = await bot.edit_general_forum_topic(
chat_id=forum_group_id,
name=f"GENERAL_{datetime.datetime.now().timestamp()}",
name=f"GENERAL_{dtm.datetime.now().timestamp()}",
)
assert result is True, "Failed to edit general forum topic"
# no way of checking the edited name, just the boolean result

View file

@ -16,9 +16,10 @@
#
# 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 contextlib
import datetime as dtm
from copy import copy
from datetime import datetime
import pytest
@ -112,10 +113,10 @@ def message(bot):
params=[
{
"reply_to_message": Message(
50, datetime.utcnow(), Chat(13, "channel"), User(9, "i", False)
50, dtm.datetime.utcnow(), Chat(13, "channel"), User(9, "i", False)
)
},
{"edit_date": datetime.utcnow()},
{"edit_date": dtm.datetime.utcnow()},
{
"text": "a text message",
"entities": [MessageEntity("bold", 10, 4), MessageEntity("italic", 16, 7)],
@ -161,7 +162,7 @@ def message(bot):
{"migrate_from_chat_id": -54321},
{
"pinned_message": Message(
7, datetime.utcnow(), Chat(13, "channel"), User(9, "i", False)
7, dtm.datetime.utcnow(), Chat(13, "channel"), User(9, "i", False)
)
},
{"invoice": Invoice("my invoice", "invoice", "start", "EUR", 243)},
@ -210,7 +211,7 @@ def message(bot):
User(1, "John", False), User(2, "Doe", False), 42
)
},
{"video_chat_scheduled": VideoChatScheduled(datetime.utcnow())},
{"video_chat_scheduled": VideoChatScheduled(dtm.datetime.utcnow())},
{"video_chat_started": VideoChatStarted()},
{"video_chat_ended": VideoChatEnded(100)},
{
@ -234,7 +235,7 @@ def message(bot):
{
"giveaway": Giveaway(
chats=[Chat(1, Chat.SUPERGROUP)],
winners_selection_date=datetime.utcnow().replace(microsecond=0),
winners_selection_date=dtm.datetime.utcnow().replace(microsecond=0),
winner_count=5,
)
},
@ -243,7 +244,7 @@ def message(bot):
"giveaway_winners": GiveawayWinners(
chat=Chat(1, Chat.CHANNEL),
giveaway_message_id=123456789,
winners_selection_date=datetime.utcnow().replace(microsecond=0),
winners_selection_date=dtm.datetime.utcnow().replace(microsecond=0),
winner_count=42,
winners=[User(1, "user1", False), User(2, "user2", False)],
)
@ -266,11 +267,11 @@ def message(bot):
},
{
"external_reply": ExternalReplyInfo(
MessageOriginChat(datetime.utcnow(), Chat(1, Chat.PRIVATE))
MessageOriginChat(dtm.datetime.utcnow(), Chat(1, Chat.PRIVATE))
)
},
{"quote": TextQuote("a text quote", 1)},
{"forward_origin": MessageOriginChat(datetime.utcnow(), Chat(1, Chat.PRIVATE))},
{"forward_origin": MessageOriginChat(dtm.datetime.utcnow(), Chat(1, Chat.PRIVATE))},
{"reply_to_story": Story(Chat(1, Chat.PRIVATE), 0)},
{"boost_added": ChatBoostAdded(100)},
{"sender_boost_count": 1},
@ -372,7 +373,7 @@ def message_params(bot, request):
class MessageTestBase:
id_ = 1
from_user = User(2, "testuser", False)
date = datetime.utcnow()
date = dtm.datetime.utcnow()
chat = Chat(3, "private")
test_entities = [
{"length": 4, "offset": 10, "type": "bold"},
@ -591,9 +592,9 @@ class TestMessageWithoutRequest(MessageTestBase):
json_dict = {
"message_id": 12,
"from_user": None,
"date": int(datetime.now().timestamp()),
"date": int(dtm.datetime.now().timestamp()),
"chat": None,
"edit_date": int(datetime.now().timestamp()),
"edit_date": int(dtm.datetime.now().timestamp()),
}
message_raw = Message.de_json(json_dict, raw_bot)

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import inspect
from copy import deepcopy
@ -39,7 +39,7 @@ ignored = ["self", "api_kwargs"]
class MODefaults:
date: datetime.datetime = to_timestamp(datetime.datetime.utcnow())
date: dtm.datetime = to_timestamp(dtm.datetime.utcnow())
chat = Chat(1, Chat.CHANNEL)
message_id = 123
author_signautre = "PTB"
@ -106,7 +106,7 @@ def iter_args(
if param.name in ignored:
continue
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
if isinstance(json_at, datetime.datetime): # Convert datetime to int
if isinstance(json_at, dtm.datetime): # Convert datetime to int
json_at = to_timestamp(json_at)
if (
param.default is not inspect.Parameter.empty and include_optional

View file

@ -20,11 +20,11 @@
match the official API. It also checks if the type annotations are correct and if the parameters
are required or not."""
import datetime as dtm
import inspect
import logging
import re
from collections.abc import Sequence
from datetime import datetime, timedelta
from types import FunctionType
from typing import Any
@ -38,6 +38,7 @@ from tests.test_official.helpers import (
_get_params_base,
_unionizer,
cached_type_hints,
extract_mappings,
resolve_forward_refs_in_type,
wrap_with_none,
)
@ -144,7 +145,7 @@ def check_param_type(
)
# CHECKING:
# Each branch manipulates the `mapped_type` (except for 4) ) to match the `ptb_annotation`.
# Each branch manipulates the `mapped_type` (except for 5) ) to match the `ptb_annotation`.
# 1) HANDLING ARRAY TYPES:
# Now let's do the checking, starting with "Array of ..." types.
@ -174,9 +175,11 @@ def check_param_type(
# 2) HANDLING OTHER TYPES:
# Special case for send_* methods where we accept more types than the official API:
elif ptb_param.name in PTCE.ADDITIONAL_TYPES and obj.__name__.startswith("send"):
log("Checking that `%s` has an additional argument!\n", ptb_param.name)
mapped_type = mapped_type | PTCE.ADDITIONAL_TYPES[ptb_param.name]
elif additional_types := extract_mappings(PTCE.ADDITIONAL_TYPES, obj, ptb_param.name):
log("Checking that `%s` accepts additional types for some parameters!\n", obj.__name__)
for at in additional_types:
log("Checking that `%s` is an additional type for `%s`!\n", at, ptb_param.name)
mapped_type = mapped_type | at
# 3) HANDLING DATETIMES:
elif (
@ -190,7 +193,7 @@ def check_param_type(
if ptb_param.name in PTCE.DATETIME_EXCEPTIONS:
return True, mapped_type
# If it's a class, we only accept datetime as the parameter
mapped_type = datetime if is_class else mapped_type | datetime
mapped_type = dtm.datetime if is_class else mapped_type | dtm.datetime
# 4) HANDLING TIMEDELTA:
elif re.search(TIMEDELTA_REGEX, ptb_param.name) and obj.__name__ in (
@ -201,15 +204,14 @@ def check_param_type(
# and `create_invoice_link`.
# See https://github.com/python-telegram-bot/python-telegram-bot/issues/4575
log("Checking that `%s` is a timedelta!\n", ptb_param.name)
mapped_type = timedelta if is_class else mapped_type | timedelta
mapped_type = dtm.timedelta if is_class else mapped_type | dtm.timedelta
# 5) COMPLEX TYPES:
# Some types are too complicated, so we replace our annotation with a simpler type:
elif any(ptb_param.name in key for key in PTCE.COMPLEX_TYPES):
elif overrides := extract_mappings(PTCE.COMPLEX_TYPES, obj, ptb_param.name):
exception_type = overrides[0]
log("Converting `%s` to a simpler type!\n", ptb_param.name)
for (param_name, is_expected_class), exception_type in PTCE.COMPLEX_TYPES.items():
if ptb_param.name == param_name and is_class is is_expected_class:
ptb_annotation = wrap_with_none(tg_parameter, exception_type, obj)
ptb_annotation = wrap_with_none(tg_parameter, exception_type, obj)
# 6) HANDLING DEFAULTS PARAMETERS:
# Classes whose parameters are all ODVInput should be converted and checked.

View file

@ -35,16 +35,25 @@ GLOBALLY_IGNORED_PARAMETERS = {
class ParamTypeCheckingExceptions:
# Types for certain parameters accepted by PTB but not in the official API
# structure: method/class_name/regex: {param_name/regex: type}
ADDITIONAL_TYPES = {
"photo": PhotoSize,
"video": Video,
"video_note": VideoNote,
"audio": Audio,
"document": Document,
"animation": Animation,
"voice": Voice,
"sticker": Sticker,
"gift_id": Gift,
r"send_\w*": {
"photo$": PhotoSize,
"video$": Video,
"video_note": VideoNote,
"audio": Audio,
"document": Document,
"animation": Animation,
"voice": Voice,
"sticker": Sticker,
"gift_id": Gift,
},
"(delete|set)_sticker.*": {
"sticker$": Sticker,
},
"replace_sticker_in_set": {
"old_sticker$": Sticker,
},
}
# TODO: Look into merging this with COMPLEX_TYPES
@ -61,19 +70,29 @@ class ParamTypeCheckingExceptions:
}
# Special cases for other parameters that accept more types than the official API, and are
# too complex to compare/predict with official API:
# too complex to compare/predict with official API
# structure: class/method_name: {param_name: reduced form of annotation}
COMPLEX_TYPES = (
{ # (param_name, is_class (i.e appears in a class?)): reduced form of annotation
("correct_option_id", False): int, # actual: Literal
("file_id", False): str, # actual: Union[str, objs_with_file_id_attr]
("invite_link", False): str, # actual: Union[str, ChatInviteLink]
("provider_data", False): str, # actual: Union[str, obj]
("callback_data", True): str, # actual: Union[str, obj]
("media", True): str, # actual: Union[str, InputMedia*, FileInput]
(
"data",
True,
): str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress]
"send_poll": {"correct_option_id": int}, # actual: Literal
"get_file": {
"file_id": str, # actual: Union[str, objs_with_file_id_attr]
},
r"\w+invite_link": {
"invite_link": str, # actual: Union[str, ChatInviteLink]
},
"send_invoice|create_invoice_link": {
"provider_data": str, # actual: Union[str, obj]
},
"InlineKeyboardButton": {
"callback_data": str, # actual: Union[str, obj]
},
"Input(Paid)?Media.*": {
"media": str, # actual: Union[str, InputMedia*, FileInput]
},
"EncryptedPassportElement": {
"data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress]
},
}
)

View file

@ -21,7 +21,7 @@
import functools
import re
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, _eval_type, get_type_hints
from typing import TYPE_CHECKING, Any, Optional, TypeVar, _eval_type, get_type_hints
from bs4 import PageElement, Tag
@ -110,3 +110,22 @@ def cached_type_hints(obj: Any, is_class: bool) -> dict[str, Any]:
def resolve_forward_refs_in_type(obj: type) -> type:
"""Resolves forward references in a type hint."""
return _eval_type(obj, localns=tg_objects, globalns=None)
T = TypeVar("T")
def extract_mappings(
exceptions: dict[str, dict[str, T]], obj: object, param_name: str
) -> Optional[list[T]]:
mappings = (
mapping for pattern, mapping in exceptions.items() if (re.match(pattern, obj.__name__))
)
out = [
value
for mapping in mappings
for key, value in mapping.items()
if re.match(key, param_name)
]
return None or out

View file

@ -15,7 +15,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/].
from datetime import datetime, timedelta, timezone
import datetime as dtm
import pytest
@ -297,7 +297,7 @@ class PollTestBase:
).decode("unicode-escape")
explanation_entities = [MessageEntity(13, 17, MessageEntity.URL)]
open_period = 42
close_date = datetime.now(timezone.utc)
close_date = dtm.datetime.now(dtm.timezone.utc)
question_entities = [
MessageEntity(MessageEntity.BOLD, 0, 4),
MessageEntity(MessageEntity.ITALIC, 5, 8),
@ -339,7 +339,7 @@ class TestPollWithoutRequest(PollTestBase):
assert poll.explanation == self.explanation
assert poll.explanation_entities == tuple(self.explanation_entities)
assert poll.open_period == self.open_period
assert abs(poll.close_date - self.close_date) < timedelta(seconds=1)
assert abs(poll.close_date - self.close_date) < dtm.timedelta(seconds=1)
assert to_timestamp(poll.close_date) == to_timestamp(self.close_date)
assert poll.question_entities == tuple(self.question_entities)

View file

@ -1,849 +0,0 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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/].
import datetime
from collections.abc import Sequence
from copy import deepcopy
import pytest
from telegram import (
Chat,
Dice,
Gift,
PaidMediaPhoto,
PhotoSize,
RevenueWithdrawalState,
RevenueWithdrawalStateFailed,
RevenueWithdrawalStatePending,
RevenueWithdrawalStateSucceeded,
StarTransaction,
StarTransactions,
Sticker,
TelegramObject,
TransactionPartner,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
TransactionPartnerTelegramApi,
TransactionPartnerUser,
User,
)
from telegram._payment.stars import AffiliateInfo, TransactionPartnerAffiliateProgram
from telegram._utils.datetime import UTC, from_timestamp, to_timestamp
from telegram.constants import RevenueWithdrawalStateType, TransactionPartnerType
from tests.auxil.slots import mro_slots
def withdrawal_state_succeeded():
return RevenueWithdrawalStateSucceeded(
date=datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC),
url="url",
)
@pytest.fixture
def withdrawal_state_failed():
return RevenueWithdrawalStateFailed()
@pytest.fixture
def withdrawal_state_pending():
return RevenueWithdrawalStatePending()
def transaction_partner_user():
return TransactionPartnerUser(
user=User(id=1, is_bot=False, first_name="first_name", username="username"),
affiliate=AffiliateInfo(
affiliate_user=User(id=2, is_bot=True, first_name="first_name", username="username"),
affiliate_chat=Chat(id=3, type="private", title="title"),
commission_per_mille=1,
amount=2,
nanostar_amount=3,
),
invoice_payload="payload",
paid_media=[
PaidMediaPhoto(
photo=[
PhotoSize(
file_id="file_id", width=1, height=1, file_unique_id="file_unique_id"
)
]
)
],
paid_media_payload="payload",
subscription_period=datetime.timedelta(days=1),
gift=Gift(
id="some_id",
sticker=Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=512,
height=512,
is_animated=False,
is_video=False,
type="regular",
),
star_count=5,
total_count=10,
remaining_count=5,
),
)
def transaction_partner_affiliate_program():
return TransactionPartnerAffiliateProgram(
sponsor_user=User(id=1, is_bot=True, first_name="first_name", username="username"),
commission_per_mille=42,
)
def transaction_partner_fragment():
return TransactionPartnerFragment(
withdrawal_state=withdrawal_state_succeeded(),
)
def star_transaction():
return StarTransaction(
id="1",
amount=1,
nanostar_amount=365,
date=to_timestamp(datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)),
source=transaction_partner_user(),
receiver=transaction_partner_fragment(),
)
@pytest.fixture
def star_transactions():
return StarTransactions(
transactions=[
star_transaction(),
star_transaction(),
]
)
@pytest.fixture(
scope="module",
params=[
TransactionPartner.AFFILIATE_PROGRAM,
TransactionPartner.FRAGMENT,
TransactionPartner.OTHER,
TransactionPartner.TELEGRAM_ADS,
TransactionPartner.TELEGRAM_API,
TransactionPartner.USER,
],
)
def tp_scope_type(request):
return request.param
@pytest.fixture(
scope="module",
params=[
TransactionPartnerAffiliateProgram,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
TransactionPartnerTelegramApi,
TransactionPartnerUser,
],
ids=[
TransactionPartner.AFFILIATE_PROGRAM,
TransactionPartner.FRAGMENT,
TransactionPartner.OTHER,
TransactionPartner.TELEGRAM_ADS,
TransactionPartner.TELEGRAM_API,
TransactionPartner.USER,
],
)
def tp_scope_class(request):
return request.param
@pytest.fixture(
scope="module",
params=[
(TransactionPartnerAffiliateProgram, TransactionPartner.AFFILIATE_PROGRAM),
(TransactionPartnerFragment, TransactionPartner.FRAGMENT),
(TransactionPartnerOther, TransactionPartner.OTHER),
(TransactionPartnerTelegramAds, TransactionPartner.TELEGRAM_ADS),
(TransactionPartnerTelegramApi, TransactionPartner.TELEGRAM_API),
(TransactionPartnerUser, TransactionPartner.USER),
],
ids=[
TransactionPartner.AFFILIATE_PROGRAM,
TransactionPartner.FRAGMENT,
TransactionPartner.OTHER,
TransactionPartner.TELEGRAM_ADS,
TransactionPartner.TELEGRAM_API,
TransactionPartner.USER,
],
)
def tp_scope_class_and_type(request):
return request.param
@pytest.fixture(scope="module")
def transaction_partner(tp_scope_class_and_type):
# We use de_json here so that we don't have to worry about which class gets which arguments
return tp_scope_class_and_type[0].de_json(
{
"type": tp_scope_class_and_type[1],
"invoice_payload": TransactionPartnerTestBase.invoice_payload,
"withdrawal_state": TransactionPartnerTestBase.withdrawal_state.to_dict(),
"user": TransactionPartnerTestBase.user.to_dict(),
"affiliate": TransactionPartnerTestBase.affiliate.to_dict(),
"request_count": TransactionPartnerTestBase.request_count,
"sponsor_user": TransactionPartnerTestBase.sponsor_user.to_dict(),
"commission_per_mille": TransactionPartnerTestBase.commission_per_mille,
"gift": TransactionPartnerTestBase.gift.to_dict(),
"paid_media": [m.to_dict() for m in TransactionPartnerTestBase.paid_media],
"paid_media_payload": TransactionPartnerTestBase.paid_media_payload,
"subscription_period": TransactionPartnerTestBase.subscription_period.total_seconds(),
},
bot=None,
)
@pytest.fixture(
scope="module",
params=[
RevenueWithdrawalState.FAILED,
RevenueWithdrawalState.SUCCEEDED,
RevenueWithdrawalState.PENDING,
],
)
def rws_scope_type(request):
return request.param
@pytest.fixture(
scope="module",
params=[
RevenueWithdrawalStateFailed,
RevenueWithdrawalStateSucceeded,
RevenueWithdrawalStatePending,
],
ids=[
RevenueWithdrawalState.FAILED,
RevenueWithdrawalState.SUCCEEDED,
RevenueWithdrawalState.PENDING,
],
)
def rws_scope_class(request):
return request.param
@pytest.fixture(
scope="module",
params=[
(RevenueWithdrawalStateFailed, RevenueWithdrawalState.FAILED),
(RevenueWithdrawalStateSucceeded, RevenueWithdrawalState.SUCCEEDED),
(RevenueWithdrawalStatePending, RevenueWithdrawalState.PENDING),
],
ids=[
RevenueWithdrawalState.FAILED,
RevenueWithdrawalState.SUCCEEDED,
RevenueWithdrawalState.PENDING,
],
)
def rws_scope_class_and_type(request):
return request.param
@pytest.fixture(scope="module")
def revenue_withdrawal_state(rws_scope_class_and_type):
# We use de_json here so that we don't have to worry about which class gets which arguments
return rws_scope_class_and_type[0].de_json(
{
"type": rws_scope_class_and_type[1],
"date": to_timestamp(RevenueWithdrawalStateTestBase.date),
"url": RevenueWithdrawalStateTestBase.url,
},
bot=None,
)
class StarTransactionTestBase:
id = "2"
amount = 2
nanostar_amount = 365
date = to_timestamp(datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC))
source = TransactionPartnerUser(
user=User(
id=2,
is_bot=False,
first_name="first_name",
),
)
receiver = TransactionPartnerOther()
class TestStarTransactionWithoutRequest(StarTransactionTestBase):
def test_slot_behaviour(self):
inst = star_transaction()
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"id": self.id,
"amount": self.amount,
"nanostar_amount": self.nanostar_amount,
"date": self.date,
"source": self.source.to_dict(),
"receiver": self.receiver.to_dict(),
}
st = StarTransaction.de_json(json_dict, offline_bot)
st_none = StarTransaction.de_json(None, offline_bot)
assert st.api_kwargs == {}
assert st.id == self.id
assert st.amount == self.amount
assert st.nanostar_amount == self.nanostar_amount
assert st.date == from_timestamp(self.date)
assert st.source == self.source
assert st.receiver == self.receiver
assert st_none is None
def test_de_json_star_transaction_localization(self, tz_bot, offline_bot, raw_bot):
json_dict = star_transaction().to_dict()
st_raw = StarTransaction.de_json(json_dict, raw_bot)
st_bot = StarTransaction.de_json(json_dict, offline_bot)
st_tz = StarTransaction.de_json(json_dict, tz_bot)
# comparing utcoffsets because comparing timezones is unpredicatable
st_offset = st_tz.date.utcoffset()
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(st_tz.date.replace(tzinfo=None))
assert st_raw.date.tzinfo == UTC
assert st_bot.date.tzinfo == UTC
assert st_offset == tz_bot_offset
def test_to_dict(self):
st = star_transaction()
expected_dict = {
"id": "1",
"amount": 1,
"nanostar_amount": 365,
"date": st.date,
"source": st.source.to_dict(),
"receiver": st.receiver.to_dict(),
}
assert st.to_dict() == expected_dict
def test_equality(self):
a = StarTransaction(
id=self.id,
amount=self.amount,
date=self.date,
source=self.source,
receiver=self.receiver,
)
b = StarTransaction(
id=self.id,
amount=self.amount,
date=None,
source=self.source,
receiver=self.receiver,
)
c = StarTransaction(
id="3",
amount=3,
date=to_timestamp(datetime.datetime.utcnow()),
source=TransactionPartnerUser(
user=User(
id=3,
is_bot=False,
first_name="first_name",
),
),
receiver=TransactionPartnerOther(),
)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
class StarTransactionsTestBase:
transactions = [star_transaction(), star_transaction()]
class TestStarTransactionsWithoutRequest(StarTransactionsTestBase):
def test_slot_behaviour(self, star_transactions):
inst = star_transactions
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"transactions": [t.to_dict() for t in self.transactions],
}
st = StarTransactions.de_json(json_dict, offline_bot)
st_none = StarTransactions.de_json(None, offline_bot)
assert st.api_kwargs == {}
assert st.transactions == tuple(self.transactions)
assert st_none is None
def test_to_dict(self, star_transactions):
expected_dict = {
"transactions": [t.to_dict() for t in self.transactions],
}
assert star_transactions.to_dict() == expected_dict
def test_equality(self):
a = StarTransactions(
transactions=self.transactions,
)
b = StarTransactions(
transactions=self.transactions,
)
c = StarTransactions(
transactions=[star_transaction()],
)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
class TransactionPartnerTestBase:
withdrawal_state = withdrawal_state_succeeded()
user = transaction_partner_user().user
affiliate = transaction_partner_user().affiliate
invoice_payload = "payload"
request_count = 42
sponsor_user = transaction_partner_affiliate_program().sponsor_user
commission_per_mille = transaction_partner_affiliate_program().commission_per_mille
gift = transaction_partner_user().gift
paid_media = transaction_partner_user().paid_media
paid_media_payload = transaction_partner_user().paid_media_payload
subscription_period = transaction_partner_user().subscription_period
class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
def test_slot_behaviour(self, transaction_partner):
inst = transaction_partner
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot, tp_scope_class_and_type):
cls = tp_scope_class_and_type[0]
type_ = tp_scope_class_and_type[1]
json_dict = {
"type": type_,
"invoice_payload": self.invoice_payload,
"withdrawal_state": self.withdrawal_state.to_dict(),
"user": self.user.to_dict(),
"affiliate": self.affiliate.to_dict(),
"request_count": self.request_count,
"sponsor_user": self.sponsor_user.to_dict(),
"commission_per_mille": self.commission_per_mille,
}
tp = TransactionPartner.de_json(json_dict, offline_bot)
assert set(tp.api_kwargs.keys()) == {
"user",
"affiliate",
"withdrawal_state",
"invoice_payload",
"request_count",
"sponsor_user",
"commission_per_mille",
} - set(cls.__slots__)
assert isinstance(tp, TransactionPartner)
assert type(tp) is cls
assert tp.type == type_
for key in json_dict:
if key in cls.__slots__:
assert getattr(tp, key) == getattr(self, key)
assert cls.de_json(None, offline_bot) is None
assert TransactionPartner.de_json({}, offline_bot) is None
def test_de_json_invalid_type(self, offline_bot):
json_dict = {
"type": "invalid",
"invoice_payload": self.invoice_payload,
"withdrawal_state": self.withdrawal_state.to_dict(),
"user": self.user.to_dict(),
"affiliate": self.affiliate.to_dict(),
"request_count": self.request_count,
"sponsor_user": self.sponsor_user.to_dict(),
"commission_per_mille": self.commission_per_mille,
}
tp = TransactionPartner.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {
"withdrawal_state": self.withdrawal_state.to_dict(),
"user": self.user.to_dict(),
"affiliate": self.affiliate.to_dict(),
"invoice_payload": self.invoice_payload,
"request_count": self.request_count,
"sponsor_user": self.sponsor_user.to_dict(),
"commission_per_mille": self.commission_per_mille,
}
assert type(tp) is TransactionPartner
assert tp.type == "invalid"
def test_de_json_subclass(self, tp_scope_class, offline_bot):
"""This makes sure that e.g. TransactionPartnerUser(data) never returns a
TransactionPartnerFragment instance."""
json_dict = {
"type": "invalid",
"invoice_payload": self.invoice_payload,
"withdrawal_state": self.withdrawal_state.to_dict(),
"user": self.user.to_dict(),
"affiliate": self.affiliate.to_dict(),
"request_count": self.request_count,
"commission_per_mille": self.commission_per_mille,
}
assert type(tp_scope_class.de_json(json_dict, offline_bot)) is tp_scope_class
def test_to_dict(self, transaction_partner):
tp_dict = transaction_partner.to_dict()
assert isinstance(tp_dict, dict)
assert tp_dict["type"] == transaction_partner.type
for attr in transaction_partner.__slots__:
attribute = getattr(transaction_partner, attr)
if isinstance(attribute, TelegramObject):
assert tp_dict[attr] == attribute.to_dict()
elif not isinstance(attribute, str) and isinstance(attribute, Sequence):
assert tp_dict[attr] == [a.to_dict() for a in attribute]
elif isinstance(attribute, datetime.timedelta):
assert tp_dict[attr] == attribute.total_seconds()
else:
assert tp_dict[attr] == attribute
def test_type_enum_conversion(self):
assert type(TransactionPartner("other").type) is TransactionPartnerType
assert TransactionPartner("unknown").type == "unknown"
def test_equality(self, transaction_partner, offline_bot):
a = TransactionPartner("base_type")
b = TransactionPartner("base_type")
c = transaction_partner
d = deepcopy(transaction_partner)
e = Dice(4, "emoji")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
assert c == d
assert hash(c) == hash(d)
assert c != e
assert hash(c) != hash(e)
if hasattr(c, "user"):
json_dict = c.to_dict()
json_dict["user"] = User(2, "something", True).to_dict()
f = c.__class__.de_json(json_dict, offline_bot)
assert c != f
assert hash(c) != hash(f)
if hasattr(c, "request_count"):
json_dict = c.to_dict()
json_dict["request_count"] = 1
f = c.__class__.de_json(json_dict, offline_bot)
assert c != f
assert hash(c) != hash(f)
class TestTransactionPartnerUserWithoutRequest(TransactionPartnerTestBase):
def test_de_json_required(self, offline_bot):
json_dict = {
"user": transaction_partner_user().user.to_dict(),
}
tp = TransactionPartnerUser.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.user == transaction_partner_user().user
# This test is here mainly to check that the below cases work
assert tp.subscription_period is None
assert tp.gift is None
class RevenueWithdrawalStateTestBase:
date = datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)
url = "url"
class TestRevenueWithdrawalStateWithoutRequest(RevenueWithdrawalStateTestBase):
def test_slot_behaviour(self, revenue_withdrawal_state):
inst = revenue_withdrawal_state
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot, rws_scope_class_and_type):
cls = rws_scope_class_and_type[0]
type_ = rws_scope_class_and_type[1]
json_dict = {
"type": type_,
"date": to_timestamp(self.date),
"url": self.url,
}
rws = RevenueWithdrawalState.de_json(json_dict, offline_bot)
assert set(rws.api_kwargs.keys()) == {"date", "url"} - set(cls.__slots__)
assert isinstance(rws, RevenueWithdrawalState)
assert type(rws) is cls
assert rws.type == type_
if "date" in cls.__slots__:
assert rws.date == self.date
if "url" in cls.__slots__:
assert rws.url == self.url
assert cls.de_json(None, offline_bot) is None
assert RevenueWithdrawalState.de_json({}, offline_bot) is None
def test_de_json_invalid_type(self, offline_bot):
json_dict = {
"type": "invalid",
"date": to_timestamp(self.date),
"url": self.url,
}
rws = RevenueWithdrawalState.de_json(json_dict, offline_bot)
assert rws.api_kwargs == {
"date": to_timestamp(self.date),
"url": self.url,
}
assert type(rws) is RevenueWithdrawalState
assert rws.type == "invalid"
def test_de_json_subclass(self, rws_scope_class, offline_bot):
"""This makes sure that e.g. RevenueWithdrawalState(data) never returns a
RevenueWithdrawalStateFailed instance."""
json_dict = {
"type": "invalid",
"date": to_timestamp(self.date),
"url": self.url,
}
assert type(rws_scope_class.de_json(json_dict, offline_bot)) is rws_scope_class
def test_to_dict(self, revenue_withdrawal_state):
rws_dict = revenue_withdrawal_state.to_dict()
assert isinstance(rws_dict, dict)
assert rws_dict["type"] == revenue_withdrawal_state.type
if hasattr(revenue_withdrawal_state, "date"):
assert rws_dict["date"] == to_timestamp(revenue_withdrawal_state.date)
if hasattr(revenue_withdrawal_state, "url"):
assert rws_dict["url"] == revenue_withdrawal_state.url
def test_type_enum_conversion(self):
assert type(RevenueWithdrawalState("failed").type) is RevenueWithdrawalStateType
assert RevenueWithdrawalState("unknown").type == "unknown"
def test_equality(self, revenue_withdrawal_state, offline_bot):
a = RevenueWithdrawalState("base_type")
b = RevenueWithdrawalState("base_type")
c = revenue_withdrawal_state
d = deepcopy(revenue_withdrawal_state)
e = Dice(4, "emoji")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
assert c == d
assert hash(c) == hash(d)
assert c != e
assert hash(c) != hash(e)
if hasattr(c, "url"):
json_dict = c.to_dict()
json_dict["url"] = "something"
f = c.__class__.de_json(json_dict, offline_bot)
assert c == f
assert hash(c) == hash(f)
if hasattr(c, "date"):
json_dict = c.to_dict()
json_dict["date"] = to_timestamp(datetime.datetime.utcnow())
f = c.__class__.de_json(json_dict, offline_bot)
assert c != f
assert hash(c) != hash(f)
@pytest.fixture
def affiliate_info():
return AffiliateInfo(
affiliate_user=AffiliateInfoTestBase.affiliate_user,
affiliate_chat=AffiliateInfoTestBase.affiliate_chat,
commission_per_mille=AffiliateInfoTestBase.commission_per_mille,
amount=AffiliateInfoTestBase.amount,
nanostar_amount=AffiliateInfoTestBase.nanostar_amount,
)
class AffiliateInfoTestBase:
affiliate_user = User(id=1, is_bot=True, first_name="affiliate_user", username="username")
affiliate_chat = Chat(id=2, type="private", title="affiliate_chat")
commission_per_mille = 13
amount = 14
nanostar_amount = -42
class TestAffiliateInfoWithoutRequest(AffiliateInfoTestBase):
def test_slot_behaviour(self, affiliate_info):
inst = affiliate_info
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"affiliate_user": self.affiliate_user.to_dict(),
"affiliate_chat": self.affiliate_chat.to_dict(),
"commission_per_mille": self.commission_per_mille,
"amount": self.amount,
"nanostar_amount": self.nanostar_amount,
}
ai = AffiliateInfo.de_json(json_dict, offline_bot)
assert ai.api_kwargs == {}
assert ai.affiliate_user == self.affiliate_user
assert ai.affiliate_chat == self.affiliate_chat
assert ai.commission_per_mille == self.commission_per_mille
assert ai.amount == self.amount
assert ai.nanostar_amount == self.nanostar_amount
assert AffiliateInfo.de_json(None, offline_bot) is None
assert AffiliateInfo.de_json({}, offline_bot) is None
def test_to_dict(self, affiliate_info):
ai_dict = affiliate_info.to_dict()
assert isinstance(ai_dict, dict)
assert ai_dict["affiliate_user"] == affiliate_info.affiliate_user.to_dict()
assert ai_dict["affiliate_chat"] == affiliate_info.affiliate_chat.to_dict()
assert ai_dict["commission_per_mille"] == affiliate_info.commission_per_mille
assert ai_dict["amount"] == affiliate_info.amount
assert ai_dict["nanostar_amount"] == affiliate_info.nanostar_amount
def test_equality(self, affiliate_info, offline_bot):
a = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
b = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
c = AffiliateInfo(
affiliate_user=User(id=3, is_bot=True, first_name="first_name", username="username"),
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
d = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=Chat(id=3, type="private", title="title"),
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
e = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=1,
amount=self.amount,
nanostar_amount=self.nanostar_amount,
)
f = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=1,
nanostar_amount=self.nanostar_amount,
)
g = AffiliateInfo(
affiliate_user=self.affiliate_user,
affiliate_chat=self.affiliate_chat,
commission_per_mille=self.commission_per_mille,
amount=self.amount,
nanostar_amount=1,
)
h = Dice(4, "emoji")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
assert a != f
assert hash(a) != hash(f)
assert a != g
assert hash(a) != hash(g)
assert a != h
assert hash(a) != hash(h)

View file

@ -16,7 +16,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 datetime
import datetime as dtm
import inspect
import pickle
import re
@ -176,9 +176,7 @@ class TestTelegramObject:
assert to.to_dict() == {"foo": "bar"}
def test_to_dict_missing_attribute(self):
message = Message(
1, datetime.datetime.now(), Chat(1, "private"), from_user=User(1, "", False)
)
message = Message(1, dtm.datetime.now(), Chat(1, "private"), from_user=User(1, "", False))
message._unfreeze()
del message.chat
@ -288,7 +286,7 @@ class TestTelegramObject:
def test_pickle(self, bot):
chat = Chat(2, Chat.PRIVATE)
user = User(3, "first_name", False)
date = datetime.datetime.now()
date = dtm.datetime.now()
photo = PhotoSize("file_id", "unique", 21, 21)
photo.set_bot(bot)
msg = Message(
@ -432,7 +430,7 @@ class PicklePropertyTest(TelegramObject):
def test_deepcopy_telegram_obj(self, bot):
chat = Chat(2, Chat.PRIVATE)
user = User(3, "first_name", False)
date = datetime.datetime.now()
date = dtm.datetime.now()
photo = PhotoSize("file_id", "unique", 21, 21)
photo.set_bot(bot)
msg = Message(

View file

@ -16,9 +16,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/].
import datetime as dtm
import time
from copy import deepcopy
from datetime import datetime
import pytest
@ -57,7 +57,7 @@ from tests.auxil.slots import mro_slots
message = Message(
1,
datetime.utcnow(),
dtm.datetime.utcnow(),
Chat(1, ""),
from_user=User(1, "", False),
text="Text",
@ -65,7 +65,7 @@ message = Message(
)
channel_post = Message(
1,
datetime.utcnow(),
dtm.datetime.utcnow(),
Chat(1, ""),
text="Text",
sender_chat=Chat(1, ""),
@ -139,7 +139,7 @@ deleted_business_messages = BusinessMessagesDeleted(
business_message = Message(
1,
datetime.utcnow(),
dtm.datetime.utcnow(),
Chat(1, ""),
User(1, "", False),
)

View file

@ -16,8 +16,8 @@
#
# 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 datetime as dtm
import time
from datetime import datetime
import pytest
@ -89,12 +89,12 @@ class TestWebhookInfoWithoutRequest(WebhookInfoTestBase):
assert webhook_info.url == self.url
assert webhook_info.has_custom_certificate == self.has_custom_certificate
assert webhook_info.pending_update_count == self.pending_update_count
assert isinstance(webhook_info.last_error_date, datetime)
assert isinstance(webhook_info.last_error_date, dtm.datetime)
assert webhook_info.last_error_date == from_timestamp(self.last_error_date)
assert webhook_info.max_connections == self.max_connections
assert webhook_info.allowed_updates == tuple(self.allowed_updates)
assert webhook_info.ip_address == self.ip_address
assert isinstance(webhook_info.last_synchronization_error_date, datetime)
assert isinstance(webhook_info.last_synchronization_error_date, dtm.datetime)
assert webhook_info.last_synchronization_error_date == from_timestamp(
self.last_synchronization_error_date
)