From 6eb5ef2606b4d139490ceb9bc358ab830632d71b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 27 Aug 2023 17:09:56 +0300 Subject: [PATCH] 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. --- .butcher/types/Chat/replace.yml | 2 +- .butcher/types/ChatInviteLink/replace.yml | 2 +- .butcher/types/ChatJoinRequest/replace.yml | 2 +- .butcher/types/ChatMember/replace.yml | 2 +- .butcher/types/ChatMemberBanned/replace.yml | 2 +- .butcher/types/ChatMemberRestricted/replace.yml | 2 +- .butcher/types/ChatMemberUpdated/replace.yml | 2 +- .butcher/types/Message/replace.yml | 4 ++-- .butcher/types/Poll/replace.yml | 2 +- .butcher/types/VideoChatScheduled/replace.yml | 2 +- .butcher/types/WebhookInfo/replace.yml | 2 +- CHANGES/1277.bugfix.rst | 2 ++ aiogram/types/__init__.py | 2 ++ aiogram/types/chat.py | 5 +++-- aiogram/types/chat_invite_link.py | 6 +++--- aiogram/types/chat_join_request.py | 5 +++-- aiogram/types/chat_member_banned.py | 6 +++--- aiogram/types/chat_member_restricted.py | 6 +++--- aiogram/types/chat_member_updated.py | 5 +++-- aiogram/types/custom.py | 14 ++++++++++++++ aiogram/types/message.py | 9 +++++---- aiogram/types/poll.py | 6 +++--- aiogram/types/video_chat_scheduled.py | 6 +++--- aiogram/types/webhook_info.py | 10 +++++----- pyproject.toml | 2 +- 25 files changed, 65 insertions(+), 43 deletions(-) create mode 100644 CHANGES/1277.bugfix.rst create mode 100644 aiogram/types/custom.py diff --git a/.butcher/types/Chat/replace.yml b/.butcher/types/Chat/replace.yml index 93d76533..60fe7d44 100644 --- a/.butcher/types/Chat/replace.yml +++ b/.butcher/types/Chat/replace.yml @@ -2,4 +2,4 @@ annotations: emoji_status_expiration_date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/ChatInviteLink/replace.yml b/.butcher/types/ChatInviteLink/replace.yml index 2577c954..21d6557f 100644 --- a/.butcher/types/ChatInviteLink/replace.yml +++ b/.butcher/types/ChatInviteLink/replace.yml @@ -2,4 +2,4 @@ annotations: expire_date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/ChatJoinRequest/replace.yml b/.butcher/types/ChatJoinRequest/replace.yml index 9a3a2842..80c48d76 100644 --- a/.butcher/types/ChatJoinRequest/replace.yml +++ b/.butcher/types/ChatJoinRequest/replace.yml @@ -2,4 +2,4 @@ annotations: date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/ChatMember/replace.yml b/.butcher/types/ChatMember/replace.yml index 0af85473..e264e991 100644 --- a/.butcher/types/ChatMember/replace.yml +++ b/.butcher/types/ChatMember/replace.yml @@ -2,4 +2,4 @@ annotations: until_date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/ChatMemberBanned/replace.yml b/.butcher/types/ChatMemberBanned/replace.yml index 0af85473..e264e991 100644 --- a/.butcher/types/ChatMemberBanned/replace.yml +++ b/.butcher/types/ChatMemberBanned/replace.yml @@ -2,4 +2,4 @@ annotations: until_date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/ChatMemberRestricted/replace.yml b/.butcher/types/ChatMemberRestricted/replace.yml index 0af85473..e264e991 100644 --- a/.butcher/types/ChatMemberRestricted/replace.yml +++ b/.butcher/types/ChatMemberRestricted/replace.yml @@ -2,4 +2,4 @@ annotations: until_date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/ChatMemberUpdated/replace.yml b/.butcher/types/ChatMemberUpdated/replace.yml index 9a3a2842..80c48d76 100644 --- a/.butcher/types/ChatMemberUpdated/replace.yml +++ b/.butcher/types/ChatMemberUpdated/replace.yml @@ -2,4 +2,4 @@ annotations: date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/Message/replace.yml b/.butcher/types/Message/replace.yml index 93a8f17e..5fa030bf 100644 --- a/.butcher/types/Message/replace.yml +++ b/.butcher/types/Message/replace.yml @@ -2,8 +2,8 @@ annotations: date: parsed_type: type: std - name: datetime.datetime + name: DateTime forward_date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/Poll/replace.yml b/.butcher/types/Poll/replace.yml index 6cf9fbff..fc4371b5 100644 --- a/.butcher/types/Poll/replace.yml +++ b/.butcher/types/Poll/replace.yml @@ -2,4 +2,4 @@ annotations: close_date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/VideoChatScheduled/replace.yml b/.butcher/types/VideoChatScheduled/replace.yml index 48d98bf6..8aa22091 100644 --- a/.butcher/types/VideoChatScheduled/replace.yml +++ b/.butcher/types/VideoChatScheduled/replace.yml @@ -2,4 +2,4 @@ annotations: start_date: parsed_type: type: std - name: datetime.datetime + name: DateTime diff --git a/.butcher/types/WebhookInfo/replace.yml b/.butcher/types/WebhookInfo/replace.yml index 4b1a71f0..5a784309 100644 --- a/.butcher/types/WebhookInfo/replace.yml +++ b/.butcher/types/WebhookInfo/replace.yml @@ -2,5 +2,5 @@ annotations: last_error_date: &date parsed_type: type: std - name: datetime.datetime + name: DateTime last_synchronization_error_date: *date diff --git a/CHANGES/1277.bugfix.rst b/CHANGES/1277.bugfix.rst new file mode 100644 index 00000000..6acfccf2 --- /dev/null +++ b/CHANGES/1277.bugfix.rst @@ -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. diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index bf9c02ee..8f4b6f34 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -37,6 +37,7 @@ from .chat_photo import ChatPhoto from .chat_shared import ChatShared from .chosen_inline_result import ChosenInlineResult from .contact import Contact +from .custom import DateTime from .dice import Dice from .document import Document from .downloadable import Downloadable @@ -197,6 +198,7 @@ __all__ = ( "ChosenInlineResult", "Contact", "ContentType", + "DateTime", "Dice", "Document", "Downloadable", diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 3e526bb4..921ed3e7 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -4,6 +4,7 @@ import datetime from typing import TYPE_CHECKING, Any, List, Optional, Union from .base import TelegramObject +from .custom import DateTime if TYPE_CHECKING: from ..methods import ( @@ -70,7 +71,7 @@ class Chat(TelegramObject): """*Optional*. If non-empty, the list of all `active chat usernames `_; for private chats, supergroups and channels. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" 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`.""" - 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`.""" bio: Optional[str] = None """*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, active_usernames: Optional[List[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, has_private_forwards: Optional[bool] = None, has_restricted_voice_and_video_messages: Optional[bool] = None, diff --git a/aiogram/types/chat_invite_link.py b/aiogram/types/chat_invite_link.py index ae947f36..7817c23d 100644 --- a/aiogram/types/chat_invite_link.py +++ b/aiogram/types/chat_invite_link.py @@ -1,9 +1,9 @@ from __future__ import annotations -import datetime from typing import TYPE_CHECKING, Any, Optional from .base import TelegramObject +from .custom import DateTime if TYPE_CHECKING: from .user import User @@ -28,7 +28,7 @@ class ChatInviteLink(TelegramObject): """:code:`True`, if the link is revoked""" name: Optional[str] = None """*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""" 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""" @@ -48,7 +48,7 @@ class ChatInviteLink(TelegramObject): is_primary: bool, is_revoked: bool, name: Optional[str] = None, - expire_date: Optional[datetime.datetime] = None, + expire_date: Optional[DateTime] = None, member_limit: Optional[int] = None, pending_join_request_count: Optional[int] = None, **__pydantic_kwargs: Any, diff --git a/aiogram/types/chat_join_request.py b/aiogram/types/chat_join_request.py index 6aeac238..9a9f73b9 100644 --- a/aiogram/types/chat_join_request.py +++ b/aiogram/types/chat_join_request.py @@ -11,6 +11,7 @@ from .base import ( UNSET_PROTECT_CONTENT, TelegramObject, ) +from .custom import DateTime if TYPE_CHECKING: from ..methods import ( @@ -63,7 +64,7 @@ class ChatJoinRequest(TelegramObject): """User that sent the join request""" 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.""" - date: datetime.datetime + date: DateTime """Date the request was sent in Unix time""" bio: Optional[str] = None """*Optional*. Bio of the user.""" @@ -80,7 +81,7 @@ class ChatJoinRequest(TelegramObject): chat: Chat, from_user: User, user_chat_id: int, - date: datetime.datetime, + date: DateTime, bio: Optional[str] = None, invite_link: Optional[ChatInviteLink] = None, **__pydantic_kwargs: Any, diff --git a/aiogram/types/chat_member_banned.py b/aiogram/types/chat_member_banned.py index ef8475df..88cfc75e 100644 --- a/aiogram/types/chat_member_banned.py +++ b/aiogram/types/chat_member_banned.py @@ -1,10 +1,10 @@ from __future__ import annotations -import datetime from typing import TYPE_CHECKING, Any, Literal from ..enums import ChatMemberStatus from .chat_member import ChatMember +from .custom import DateTime if TYPE_CHECKING: from .user import User @@ -21,7 +21,7 @@ class ChatMemberBanned(ChatMember): """The member's status in the chat, always 'kicked'""" user: 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""" if TYPE_CHECKING: @@ -33,7 +33,7 @@ class ChatMemberBanned(ChatMember): *, status: Literal[ChatMemberStatus.KICKED] = ChatMemberStatus.KICKED, user: User, - until_date: datetime.datetime, + until_date: DateTime, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! diff --git a/aiogram/types/chat_member_restricted.py b/aiogram/types/chat_member_restricted.py index 01971e76..32d4a0dc 100644 --- a/aiogram/types/chat_member_restricted.py +++ b/aiogram/types/chat_member_restricted.py @@ -1,10 +1,10 @@ from __future__ import annotations -import datetime from typing import TYPE_CHECKING, Any, Literal from ..enums import ChatMemberStatus from .chat_member import ChatMember +from .custom import DateTime if TYPE_CHECKING: from .user import User @@ -51,7 +51,7 @@ class ChatMemberRestricted(ChatMember): """:code:`True`, if the user is allowed to pin messages""" can_manage_topics: bool """: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""" if TYPE_CHECKING: @@ -78,7 +78,7 @@ class ChatMemberRestricted(ChatMember): can_invite_users: bool, can_pin_messages: bool, can_manage_topics: bool, - until_date: datetime.datetime, + until_date: DateTime, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! diff --git a/aiogram/types/chat_member_updated.py b/aiogram/types/chat_member_updated.py index 95db27de..46110092 100644 --- a/aiogram/types/chat_member_updated.py +++ b/aiogram/types/chat_member_updated.py @@ -11,6 +11,7 @@ from .base import ( UNSET_PROTECT_CONTENT, TelegramObject, ) +from .custom import DateTime if TYPE_CHECKING: from ..methods import ( @@ -65,7 +66,7 @@ class ChatMemberUpdated(TelegramObject): """Chat the user belongs to""" from_user: User = Field(..., alias="from") """Performer of the action, which resulted in the change""" - date: datetime.datetime + date: DateTime """Date the change was done in Unix time""" old_chat_member: Union[ ChatMemberOwner, @@ -99,7 +100,7 @@ class ChatMemberUpdated(TelegramObject): *, chat: Chat, from_user: User, - date: datetime.datetime, + date: DateTime, old_chat_member: Union[ ChatMemberOwner, ChatMemberAdministrator, diff --git a/aiogram/types/custom.py b/aiogram/types/custom.py new file mode 100644 index 00000000..5098caa6 --- /dev/null +++ b/aiogram/types/custom.py @@ -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", + ), +] diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 6652df7b..d1b68235 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -18,6 +18,7 @@ from .base import ( UNSET_PROTECT_CONTENT, TelegramObject, ) +from .custom import DateTime if TYPE_CHECKING: from ..methods import ( @@ -109,7 +110,7 @@ class Message(TelegramObject): message_id: int """Unique message identifier inside this chat""" - date: datetime.datetime + date: DateTime """Date the message was sent in Unix time""" chat: Chat """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""" 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""" - forward_date: Optional[datetime.datetime] = None + forward_date: Optional[DateTime] = None """*Optional*. For forwarded messages, date the original message was sent in Unix time""" is_topic_message: Optional[bool] = None """*Optional*. :code:`True`, if the message is sent to a forum topic""" @@ -260,7 +261,7 @@ class Message(TelegramObject): __pydantic__self__, *, message_id: int, - date: datetime.datetime, + date: DateTime, chat: Chat, message_thread_id: Optional[int] = None, from_user: Optional[User] = None, @@ -270,7 +271,7 @@ class Message(TelegramObject): forward_from_message_id: Optional[int] = None, forward_signature: 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_automatic_forward: Optional[bool] = None, reply_to_message: Optional[Message] = None, diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index 8b2c8524..ecf39f73 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -1,9 +1,9 @@ from __future__ import annotations -import datetime from typing import TYPE_CHECKING, Any, List, Optional from .base import TelegramObject +from .custom import DateTime if TYPE_CHECKING: 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*""" open_period: Optional[int] = None """*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""" if TYPE_CHECKING: @@ -63,7 +63,7 @@ class Poll(TelegramObject): explanation: Optional[str] = None, explanation_entities: Optional[List[MessageEntity]] = None, open_period: Optional[int] = None, - close_date: Optional[datetime.datetime] = None, + close_date: Optional[DateTime] = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! diff --git a/aiogram/types/video_chat_scheduled.py b/aiogram/types/video_chat_scheduled.py index c1563627..c82e4953 100644 --- a/aiogram/types/video_chat_scheduled.py +++ b/aiogram/types/video_chat_scheduled.py @@ -1,9 +1,9 @@ from __future__ import annotations -import datetime from typing import TYPE_CHECKING, Any from .base import TelegramObject +from .custom import DateTime class VideoChatScheduled(TelegramObject): @@ -13,7 +13,7 @@ class VideoChatScheduled(TelegramObject): 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""" if TYPE_CHECKING: @@ -21,7 +21,7 @@ class VideoChatScheduled(TelegramObject): # This section was auto-generated via `butcher` def __init__( - __pydantic__self__, *, start_date: datetime.datetime, **__pydantic_kwargs: Any + __pydantic__self__, *, start_date: DateTime, **__pydantic_kwargs: Any ) -> None: # DO NOT EDIT MANUALLY!!! # This method was auto-generated via `butcher` diff --git a/aiogram/types/webhook_info.py b/aiogram/types/webhook_info.py index ed19b342..1753df41 100644 --- a/aiogram/types/webhook_info.py +++ b/aiogram/types/webhook_info.py @@ -1,9 +1,9 @@ from __future__ import annotations -import datetime from typing import TYPE_CHECKING, Any, List, Optional from .base import TelegramObject +from .custom import DateTime class WebhookInfo(TelegramObject): @@ -21,11 +21,11 @@ class WebhookInfo(TelegramObject): """Number of updates awaiting delivery""" ip_address: Optional[str] = None """*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""" 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""" - 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""" max_connections: Optional[int] = None """*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, pending_update_count: int, 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_synchronization_error_date: Optional[datetime.datetime] = None, + last_synchronization_error_date: Optional[DateTime] = None, max_connections: Optional[int] = None, allowed_updates: Optional[List[str]] = None, **__pydantic_kwargs: Any, diff --git a/pyproject.toml b/pyproject.toml index 104a1582..a287076f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dependencies = [ "pydantic>=2.1.1,<3", "aiofiles~=23.1.0", "certifi>=2023.7.22", + "typing-extensions~=4.7.1", ] dynamic = ["version"] @@ -102,7 +103,6 @@ dev = [ "pre-commit~=3.3.3", "towncrier~=23.6.0", "packaging~=23.0", - "typing-extensions~=4.7.1", ] [project.urls]