Replace datetime.datetime with DateTime across codebase (#1285)

* #1277  Replace datetime.datetime with DateTime across codebase

Replaced all instances of standard library 'datetime.datetime' with a new 'DateTime' type from `.custom` module. This change is necessary to make all date-time values compatible with the Telegram Bot API (it uses Unix time). This will simplify the conversion process and eliminate potential errors related to date-time format mismatches. Changed codebase, butcher files, and modified 'pyproject.toml' to shift the typing-extensions dependency. The 'aiogram/custom_types.py' file was renamed to 'aiogram/types/custom.py' to better reflect its nature as a location for custom types used in the aiogram library.
This commit is contained in:
Alex Root Junior 2023-08-27 17:09:56 +03:00 committed by GitHub
parent 397f30b58b
commit 6eb5ef2606
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 65 additions and 43 deletions

View file

@ -2,4 +2,4 @@ annotations:
emoji_status_expiration_date: emoji_status_expiration_date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,4 +2,4 @@ annotations:
expire_date: expire_date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,4 +2,4 @@ annotations:
date: date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,4 +2,4 @@ annotations:
until_date: until_date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,4 +2,4 @@ annotations:
until_date: until_date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,4 +2,4 @@ annotations:
until_date: until_date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,4 +2,4 @@ annotations:
date: date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,8 +2,8 @@ annotations:
date: date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime
forward_date: forward_date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,4 +2,4 @@ annotations:
close_date: close_date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,4 +2,4 @@ annotations:
start_date: start_date:
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime

View file

@ -2,5 +2,5 @@ annotations:
last_error_date: &date last_error_date: &date
parsed_type: parsed_type:
type: std type: std
name: datetime.datetime name: DateTime
last_synchronization_error_date: *date last_synchronization_error_date: *date

2
CHANGES/1277.bugfix.rst Normal file
View file

@ -0,0 +1,2 @@
Replaced :code:`datetime.datetime` with `DateTime` type wrapper across types to make dumped JSONs object
more compatible with data that is sent by Telegram.

View file

@ -37,6 +37,7 @@ from .chat_photo import ChatPhoto
from .chat_shared import ChatShared from .chat_shared import ChatShared
from .chosen_inline_result import ChosenInlineResult from .chosen_inline_result import ChosenInlineResult
from .contact import Contact from .contact import Contact
from .custom import DateTime
from .dice import Dice from .dice import Dice
from .document import Document from .document import Document
from .downloadable import Downloadable from .downloadable import Downloadable
@ -197,6 +198,7 @@ __all__ = (
"ChosenInlineResult", "ChosenInlineResult",
"Contact", "Contact",
"ContentType", "ContentType",
"DateTime",
"Dice", "Dice",
"Document", "Document",
"Downloadable", "Downloadable",

View file

@ -4,6 +4,7 @@ import datetime
from typing import TYPE_CHECKING, Any, List, Optional, Union from typing import TYPE_CHECKING, Any, List, Optional, Union
from .base import TelegramObject from .base import TelegramObject
from .custom import DateTime
if TYPE_CHECKING: if TYPE_CHECKING:
from ..methods import ( from ..methods import (
@ -70,7 +71,7 @@ class Chat(TelegramObject):
"""*Optional*. If non-empty, the list of all `active chat usernames <https://telegram.org/blog/topics-in-groups-collectible-usernames#collectible-usernames>`_; for private chats, supergroups and channels. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. If non-empty, the list of all `active chat usernames <https://telegram.org/blog/topics-in-groups-collectible-usernames#collectible-usernames>`_; for private chats, supergroups and channels. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
emoji_status_custom_emoji_id: Optional[str] = None emoji_status_custom_emoji_id: Optional[str] = None
"""*Optional*. Custom emoji identifier of emoji status of the other party in a private chat. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. Custom emoji identifier of emoji status of the other party in a private chat. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
emoji_status_expiration_date: Optional[datetime.datetime] = None emoji_status_expiration_date: Optional[DateTime] = None
"""*Optional*. Expiration date of the emoji status of the other party in a private chat, if any. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. Expiration date of the emoji status of the other party in a private chat, if any. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
bio: Optional[str] = None bio: Optional[str] = None
"""*Optional*. Bio of the other party in a private chat. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. Bio of the other party in a private chat. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
@ -126,7 +127,7 @@ class Chat(TelegramObject):
photo: Optional[ChatPhoto] = None, photo: Optional[ChatPhoto] = None,
active_usernames: Optional[List[str]] = None, active_usernames: Optional[List[str]] = None,
emoji_status_custom_emoji_id: Optional[str] = None, emoji_status_custom_emoji_id: Optional[str] = None,
emoji_status_expiration_date: Optional[datetime.datetime] = None, emoji_status_expiration_date: Optional[DateTime] = None,
bio: Optional[str] = None, bio: Optional[str] = None,
has_private_forwards: Optional[bool] = None, has_private_forwards: Optional[bool] = None,
has_restricted_voice_and_video_messages: Optional[bool] = None, has_restricted_voice_and_video_messages: Optional[bool] = None,

View file

@ -1,9 +1,9 @@
from __future__ import annotations from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Optional
from .base import TelegramObject from .base import TelegramObject
from .custom import DateTime
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User from .user import User
@ -28,7 +28,7 @@ class ChatInviteLink(TelegramObject):
""":code:`True`, if the link is revoked""" """:code:`True`, if the link is revoked"""
name: Optional[str] = None name: Optional[str] = None
"""*Optional*. Invite link name""" """*Optional*. Invite link name"""
expire_date: Optional[datetime.datetime] = None expire_date: Optional[DateTime] = None
"""*Optional*. Point in time (Unix timestamp) when the link will expire or has been expired""" """*Optional*. Point in time (Unix timestamp) when the link will expire or has been expired"""
member_limit: Optional[int] = None member_limit: Optional[int] = None
"""*Optional*. The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999""" """*Optional*. The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999"""
@ -48,7 +48,7 @@ class ChatInviteLink(TelegramObject):
is_primary: bool, is_primary: bool,
is_revoked: bool, is_revoked: bool,
name: Optional[str] = None, name: Optional[str] = None,
expire_date: Optional[datetime.datetime] = None, expire_date: Optional[DateTime] = None,
member_limit: Optional[int] = None, member_limit: Optional[int] = None,
pending_join_request_count: Optional[int] = None, pending_join_request_count: Optional[int] = None,
**__pydantic_kwargs: Any, **__pydantic_kwargs: Any,

View file

@ -11,6 +11,7 @@ from .base import (
UNSET_PROTECT_CONTENT, UNSET_PROTECT_CONTENT,
TelegramObject, TelegramObject,
) )
from .custom import DateTime
if TYPE_CHECKING: if TYPE_CHECKING:
from ..methods import ( from ..methods import (
@ -63,7 +64,7 @@ class ChatJoinRequest(TelegramObject):
"""User that sent the join request""" """User that sent the join request"""
user_chat_id: int user_chat_id: int
"""Identifier of a private chat with the user who sent the join request. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier. The bot can use this identifier for 24 hours to send messages until the join request is processed, assuming no other administrator contacted the user.""" """Identifier of a private chat with the user who sent the join request. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier. The bot can use this identifier for 24 hours to send messages until the join request is processed, assuming no other administrator contacted the user."""
date: datetime.datetime date: DateTime
"""Date the request was sent in Unix time""" """Date the request was sent in Unix time"""
bio: Optional[str] = None bio: Optional[str] = None
"""*Optional*. Bio of the user.""" """*Optional*. Bio of the user."""
@ -80,7 +81,7 @@ class ChatJoinRequest(TelegramObject):
chat: Chat, chat: Chat,
from_user: User, from_user: User,
user_chat_id: int, user_chat_id: int,
date: datetime.datetime, date: DateTime,
bio: Optional[str] = None, bio: Optional[str] = None,
invite_link: Optional[ChatInviteLink] = None, invite_link: Optional[ChatInviteLink] = None,
**__pydantic_kwargs: Any, **__pydantic_kwargs: Any,

View file

@ -1,10 +1,10 @@
from __future__ import annotations from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any, Literal from typing import TYPE_CHECKING, Any, Literal
from ..enums import ChatMemberStatus from ..enums import ChatMemberStatus
from .chat_member import ChatMember from .chat_member import ChatMember
from .custom import DateTime
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User from .user import User
@ -21,7 +21,7 @@ class ChatMemberBanned(ChatMember):
"""The member's status in the chat, always 'kicked'""" """The member's status in the chat, always 'kicked'"""
user: User user: User
"""Information about the user""" """Information about the user"""
until_date: datetime.datetime until_date: DateTime
"""Date when restrictions will be lifted for this user; unix time. If 0, then the user is banned forever""" """Date when restrictions will be lifted for this user; unix time. If 0, then the user is banned forever"""
if TYPE_CHECKING: if TYPE_CHECKING:
@ -33,7 +33,7 @@ class ChatMemberBanned(ChatMember):
*, *,
status: Literal[ChatMemberStatus.KICKED] = ChatMemberStatus.KICKED, status: Literal[ChatMemberStatus.KICKED] = ChatMemberStatus.KICKED,
user: User, user: User,
until_date: datetime.datetime, until_date: DateTime,
**__pydantic_kwargs: Any, **__pydantic_kwargs: Any,
) -> None: ) -> None:
# DO NOT EDIT MANUALLY!!! # DO NOT EDIT MANUALLY!!!

View file

@ -1,10 +1,10 @@
from __future__ import annotations from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any, Literal from typing import TYPE_CHECKING, Any, Literal
from ..enums import ChatMemberStatus from ..enums import ChatMemberStatus
from .chat_member import ChatMember from .chat_member import ChatMember
from .custom import DateTime
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User from .user import User
@ -51,7 +51,7 @@ class ChatMemberRestricted(ChatMember):
""":code:`True`, if the user is allowed to pin messages""" """:code:`True`, if the user is allowed to pin messages"""
can_manage_topics: bool can_manage_topics: bool
""":code:`True`, if the user is allowed to create forum topics""" """:code:`True`, if the user is allowed to create forum topics"""
until_date: datetime.datetime until_date: DateTime
"""Date when restrictions will be lifted for this user; unix time. If 0, then the user is restricted forever""" """Date when restrictions will be lifted for this user; unix time. If 0, then the user is restricted forever"""
if TYPE_CHECKING: if TYPE_CHECKING:
@ -78,7 +78,7 @@ class ChatMemberRestricted(ChatMember):
can_invite_users: bool, can_invite_users: bool,
can_pin_messages: bool, can_pin_messages: bool,
can_manage_topics: bool, can_manage_topics: bool,
until_date: datetime.datetime, until_date: DateTime,
**__pydantic_kwargs: Any, **__pydantic_kwargs: Any,
) -> None: ) -> None:
# DO NOT EDIT MANUALLY!!! # DO NOT EDIT MANUALLY!!!

View file

@ -11,6 +11,7 @@ from .base import (
UNSET_PROTECT_CONTENT, UNSET_PROTECT_CONTENT,
TelegramObject, TelegramObject,
) )
from .custom import DateTime
if TYPE_CHECKING: if TYPE_CHECKING:
from ..methods import ( from ..methods import (
@ -65,7 +66,7 @@ class ChatMemberUpdated(TelegramObject):
"""Chat the user belongs to""" """Chat the user belongs to"""
from_user: User = Field(..., alias="from") from_user: User = Field(..., alias="from")
"""Performer of the action, which resulted in the change""" """Performer of the action, which resulted in the change"""
date: datetime.datetime date: DateTime
"""Date the change was done in Unix time""" """Date the change was done in Unix time"""
old_chat_member: Union[ old_chat_member: Union[
ChatMemberOwner, ChatMemberOwner,
@ -99,7 +100,7 @@ class ChatMemberUpdated(TelegramObject):
*, *,
chat: Chat, chat: Chat,
from_user: User, from_user: User,
date: datetime.datetime, date: DateTime,
old_chat_member: Union[ old_chat_member: Union[
ChatMemberOwner, ChatMemberOwner,
ChatMemberAdministrator, ChatMemberAdministrator,

14
aiogram/types/custom.py Normal file
View file

@ -0,0 +1,14 @@
from datetime import datetime
from pydantic import PlainSerializer
from typing_extensions import Annotated
# Make datetime compatible with Telegram Bot API (unixtime)
DateTime = Annotated[
datetime,
PlainSerializer(
func=lambda dt: int(dt.timestamp()),
return_type=int,
when_used="json-unless-none",
),
]

View file

@ -18,6 +18,7 @@ from .base import (
UNSET_PROTECT_CONTENT, UNSET_PROTECT_CONTENT,
TelegramObject, TelegramObject,
) )
from .custom import DateTime
if TYPE_CHECKING: if TYPE_CHECKING:
from ..methods import ( from ..methods import (
@ -109,7 +110,7 @@ class Message(TelegramObject):
message_id: int message_id: int
"""Unique message identifier inside this chat""" """Unique message identifier inside this chat"""
date: datetime.datetime date: DateTime
"""Date the message was sent in Unix time""" """Date the message was sent in Unix time"""
chat: Chat chat: Chat
"""Conversation the message belongs to""" """Conversation the message belongs to"""
@ -129,7 +130,7 @@ class Message(TelegramObject):
"""*Optional*. For forwarded messages that were originally sent in channels or by an anonymous chat administrator, signature of the message sender if present""" """*Optional*. For forwarded messages that were originally sent in channels or by an anonymous chat administrator, signature of the message sender if present"""
forward_sender_name: Optional[str] = None forward_sender_name: Optional[str] = None
"""*Optional*. Sender's name for messages forwarded from users who disallow adding a link to their account in forwarded messages""" """*Optional*. Sender's name for messages forwarded from users who disallow adding a link to their account in forwarded messages"""
forward_date: Optional[datetime.datetime] = None forward_date: Optional[DateTime] = None
"""*Optional*. For forwarded messages, date the original message was sent in Unix time""" """*Optional*. For forwarded messages, date the original message was sent in Unix time"""
is_topic_message: Optional[bool] = None is_topic_message: Optional[bool] = None
"""*Optional*. :code:`True`, if the message is sent to a forum topic""" """*Optional*. :code:`True`, if the message is sent to a forum topic"""
@ -260,7 +261,7 @@ class Message(TelegramObject):
__pydantic__self__, __pydantic__self__,
*, *,
message_id: int, message_id: int,
date: datetime.datetime, date: DateTime,
chat: Chat, chat: Chat,
message_thread_id: Optional[int] = None, message_thread_id: Optional[int] = None,
from_user: Optional[User] = None, from_user: Optional[User] = None,
@ -270,7 +271,7 @@ class Message(TelegramObject):
forward_from_message_id: Optional[int] = None, forward_from_message_id: Optional[int] = None,
forward_signature: Optional[str] = None, forward_signature: Optional[str] = None,
forward_sender_name: Optional[str] = None, forward_sender_name: Optional[str] = None,
forward_date: Optional[datetime.datetime] = None, forward_date: Optional[DateTime] = None,
is_topic_message: Optional[bool] = None, is_topic_message: Optional[bool] = None,
is_automatic_forward: Optional[bool] = None, is_automatic_forward: Optional[bool] = None,
reply_to_message: Optional[Message] = None, reply_to_message: Optional[Message] = None,

View file

@ -1,9 +1,9 @@
from __future__ import annotations from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any, List, Optional from typing import TYPE_CHECKING, Any, List, Optional
from .base import TelegramObject from .base import TelegramObject
from .custom import DateTime
if TYPE_CHECKING: if TYPE_CHECKING:
from .message_entity import MessageEntity from .message_entity import MessageEntity
@ -41,7 +41,7 @@ class Poll(TelegramObject):
"""*Optional*. Special entities like usernames, URLs, bot commands, etc. that appear in the *explanation*""" """*Optional*. Special entities like usernames, URLs, bot commands, etc. that appear in the *explanation*"""
open_period: Optional[int] = None open_period: Optional[int] = None
"""*Optional*. Amount of time in seconds the poll will be active after creation""" """*Optional*. Amount of time in seconds the poll will be active after creation"""
close_date: Optional[datetime.datetime] = None close_date: Optional[DateTime] = None
"""*Optional*. Point in time (Unix timestamp) when the poll will be automatically closed""" """*Optional*. Point in time (Unix timestamp) when the poll will be automatically closed"""
if TYPE_CHECKING: if TYPE_CHECKING:
@ -63,7 +63,7 @@ class Poll(TelegramObject):
explanation: Optional[str] = None, explanation: Optional[str] = None,
explanation_entities: Optional[List[MessageEntity]] = None, explanation_entities: Optional[List[MessageEntity]] = None,
open_period: Optional[int] = None, open_period: Optional[int] = None,
close_date: Optional[datetime.datetime] = None, close_date: Optional[DateTime] = None,
**__pydantic_kwargs: Any, **__pydantic_kwargs: Any,
) -> None: ) -> None:
# DO NOT EDIT MANUALLY!!! # DO NOT EDIT MANUALLY!!!

View file

@ -1,9 +1,9 @@
from __future__ import annotations from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from .base import TelegramObject from .base import TelegramObject
from .custom import DateTime
class VideoChatScheduled(TelegramObject): class VideoChatScheduled(TelegramObject):
@ -13,7 +13,7 @@ class VideoChatScheduled(TelegramObject):
Source: https://core.telegram.org/bots/api#videochatscheduled Source: https://core.telegram.org/bots/api#videochatscheduled
""" """
start_date: datetime.datetime start_date: DateTime
"""Point in time (Unix timestamp) when the video chat is supposed to be started by a chat administrator""" """Point in time (Unix timestamp) when the video chat is supposed to be started by a chat administrator"""
if TYPE_CHECKING: if TYPE_CHECKING:
@ -21,7 +21,7 @@ class VideoChatScheduled(TelegramObject):
# This section was auto-generated via `butcher` # This section was auto-generated via `butcher`
def __init__( def __init__(
__pydantic__self__, *, start_date: datetime.datetime, **__pydantic_kwargs: Any __pydantic__self__, *, start_date: DateTime, **__pydantic_kwargs: Any
) -> None: ) -> None:
# DO NOT EDIT MANUALLY!!! # DO NOT EDIT MANUALLY!!!
# This method was auto-generated via `butcher` # This method was auto-generated via `butcher`

View file

@ -1,9 +1,9 @@
from __future__ import annotations from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any, List, Optional from typing import TYPE_CHECKING, Any, List, Optional
from .base import TelegramObject from .base import TelegramObject
from .custom import DateTime
class WebhookInfo(TelegramObject): class WebhookInfo(TelegramObject):
@ -21,11 +21,11 @@ class WebhookInfo(TelegramObject):
"""Number of updates awaiting delivery""" """Number of updates awaiting delivery"""
ip_address: Optional[str] = None ip_address: Optional[str] = None
"""*Optional*. Currently used webhook IP address""" """*Optional*. Currently used webhook IP address"""
last_error_date: Optional[datetime.datetime] = None last_error_date: Optional[DateTime] = None
"""*Optional*. Unix time for the most recent error that happened when trying to deliver an update via webhook""" """*Optional*. Unix time for the most recent error that happened when trying to deliver an update via webhook"""
last_error_message: Optional[str] = None last_error_message: Optional[str] = None
"""*Optional*. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook""" """*Optional*. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook"""
last_synchronization_error_date: Optional[datetime.datetime] = None last_synchronization_error_date: Optional[DateTime] = None
"""*Optional*. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters""" """*Optional*. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters"""
max_connections: Optional[int] = None max_connections: Optional[int] = None
"""*Optional*. The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery""" """*Optional*. The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery"""
@ -43,9 +43,9 @@ class WebhookInfo(TelegramObject):
has_custom_certificate: bool, has_custom_certificate: bool,
pending_update_count: int, pending_update_count: int,
ip_address: Optional[str] = None, ip_address: Optional[str] = None,
last_error_date: Optional[datetime.datetime] = None, last_error_date: Optional[DateTime] = None,
last_error_message: Optional[str] = None, last_error_message: Optional[str] = None,
last_synchronization_error_date: Optional[datetime.datetime] = None, last_synchronization_error_date: Optional[DateTime] = None,
max_connections: Optional[int] = None, max_connections: Optional[int] = None,
allowed_updates: Optional[List[str]] = None, allowed_updates: Optional[List[str]] = None,
**__pydantic_kwargs: Any, **__pydantic_kwargs: Any,

View file

@ -45,6 +45,7 @@ dependencies = [
"pydantic>=2.1.1,<3", "pydantic>=2.1.1,<3",
"aiofiles~=23.1.0", "aiofiles~=23.1.0",
"certifi>=2023.7.22", "certifi>=2023.7.22",
"typing-extensions~=4.7.1",
] ]
dynamic = ["version"] dynamic = ["version"]
@ -102,7 +103,6 @@ dev = [
"pre-commit~=3.3.3", "pre-commit~=3.3.3",
"towncrier~=23.6.0", "towncrier~=23.6.0",
"packaging~=23.0", "packaging~=23.0",
"typing-extensions~=4.7.1",
] ]
[project.urls] [project.urls]