Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2318dfd
First overall draft to PR and for feedback
ToothyDev Jan 2, 2026
32cebf2
Apply change request
ToothyDev Jan 2, 2026
cf54a23
Move generate_snowflake method over
ToothyDev Jan 2, 2026
93c16dd
Adjust changelog
ToothyDev Jan 2, 2026
57c2304
Apply change requests by @NeloBlivion
ToothyDev Jan 2, 2026
96fc400
Move snowflake_time (now from_snowflake) to DiscordTime
ToothyDev Jan 2, 2026
2c996bc
Remove extraneous import
ToothyDev Jan 2, 2026
0d71c45
Add missing return
ToothyDev Jan 2, 2026
408cdf5
🐛 Fix import errors
ToothyDev Jan 2, 2026
5fb5c49
Move format_dt to new subclass
ToothyDev Jan 2, 2026
d77f2b5
Address ruff warnings
ToothyDev Jan 2, 2026
b62d913
📝 Add to docs
ToothyDev Jan 2, 2026
571dc0f
Minor formatting fix
ToothyDev Jan 2, 2026
52a7a04
Add DiscordTime to a lot more places (not yet done)
ToothyDev Jan 2, 2026
865a2d1
Hopefully replacing the final outgoing usages
ToothyDev Jan 2, 2026
8e549ce
Final fixes so docs build and all
ToothyDev Jan 2, 2026
b355cec
Minor docs improvements
ToothyDev Jan 2, 2026
dc10f3b
Fix final ruff error, ruff ruff
ToothyDev Jan 2, 2026
b5db89a
Fix ruff for good and fix format_datetime
ToothyDev Jan 2, 2026
4132d73
Apply change requests
ToothyDev Jan 2, 2026
da1606c
Restore 3.10 compat
ToothyDev Jan 2, 2026
6ae7320
Update docs/api/data_classes.rst
ToothyDev Jan 2, 2026
ab7bb90
Apply change requests
ToothyDev Jan 3, 2026
83401c4
Simplify timestamp getter
ToothyDev Jan 3, 2026
c8b5ba7
Apply change requests
ToothyDev Jan 3, 2026
8ac36da
Apply change requests
ToothyDev Jan 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG-NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ release.

### Added

- `discord.DiscordTime`, a `datetime.datetime` subclass that offers additional
functionality for snowflakes as well as util methods.

### Fixed

### Changed
Expand All @@ -28,3 +31,7 @@ release.
- `AsyncIterator.get` use `AsyncIterator.find` with `lambda i: i.attr == val` instead
- `utils.as_chunks` use `itertools.batched` on Python 3.12+ or your own implementation
instead
- `utils.generate_snowflake`, moved to `discord.datetime.DiscordTime.generate_snowflake`
- `utils.utcnow`, moved to `discord.datetime.DiscordTime.utcnow`
- `utils.snowflake_time`, moved to `DiscordTime.from_snowflake`
- `utils.format_dt`, moved to `DiscordTime.format`
1 change: 1 addition & 0 deletions discord/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from .colour import *
from .commands import *
from .components import *
from .datetime import *
from .embeds import *
from .emoji import *
from .enums import *
Expand Down
35 changes: 19 additions & 16 deletions discord/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

from .asset import Asset
from .colour import Colour
from .datetime import DiscordTime
from .enums import ActivityType, try_enum
from .partial_emoji import PartialEmoji
from .utils.private import get_as_snowflake
Expand Down Expand Up @@ -123,13 +124,14 @@ def __init__(self, **kwargs):
self._created_at: float | None = kwargs.pop("created_at", None)

@property
def created_at(self) -> datetime.datetime | None:
def created_at(self) -> DiscordTime | None:
"""When the user started doing this activity in UTC.

.. versionadded:: 1.3
"""
if self._created_at is not None:
return datetime.datetime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
return DiscordTime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
return None

def to_dict(self) -> ActivityPayload:
raise NotImplementedError
Expand Down Expand Up @@ -273,24 +275,24 @@ def to_dict(self) -> dict[str, Any]:
return ret

@property
def start(self) -> datetime.datetime | None:
def start(self) -> DiscordTime | None:
"""When the user started doing this activity in UTC, if applicable."""
try:
timestamp = self.timestamps["start"] / 1000
except KeyError:
return None
else:
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
return DiscordTime.fromtimestamp(timestamp, tz=datetime.timezone.utc)

@property
def end(self) -> datetime.datetime | None:
def end(self) -> DiscordTime | None:
"""When the user will stop doing this activity in UTC, if applicable."""
try:
timestamp = self.timestamps["end"] / 1000
except KeyError:
return None
else:
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
return DiscordTime.fromtimestamp(timestamp, tz=datetime.timezone.utc)

@property
def large_image_url(self) -> str | None:
Expand Down Expand Up @@ -387,17 +389,17 @@ def type(self) -> ActivityType:
return ActivityType.playing

@property
def start(self) -> datetime.datetime | None:
def start(self) -> DiscordTime | None:
"""When the user started playing this game in UTC, if applicable."""
if self._start:
return datetime.datetime.fromtimestamp(self._start / 1000, tz=datetime.timezone.utc)
return DiscordTime.fromtimestamp(self._start / 1000, tz=datetime.timezone.utc)
return None

@property
def end(self) -> datetime.datetime | None:
def end(self) -> DiscordTime | None:
"""When the user will stop playing this game in UTC, if applicable."""
if self._end:
return datetime.datetime.fromtimestamp(self._end / 1000, tz=datetime.timezone.utc)
return DiscordTime.fromtimestamp(self._end / 1000, tz=datetime.timezone.utc)
return None

def __str__(self) -> str:
Expand Down Expand Up @@ -583,13 +585,14 @@ def type(self) -> ActivityType:
return ActivityType.listening

@property
def created_at(self) -> datetime.datetime | None:
def created_at(self) -> DiscordTime | None:
"""When the user started listening in UTC.

.. versionadded:: 1.3
"""
if self._created_at is not None:
return datetime.datetime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
return DiscordTime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
return None

@property
def colour(self) -> Colour:
Expand Down Expand Up @@ -689,14 +692,14 @@ def track_url(self) -> str:
return f"https://open.spotify.com/track/{self.track_id}"

@property
def start(self) -> datetime.datetime:
def start(self) -> DiscordTime:
"""When the user started playing this song in UTC."""
return datetime.datetime.fromtimestamp(self._timestamps["start"] / 1000, tz=datetime.timezone.utc)
return DiscordTime.fromtimestamp(self._timestamps["start"] / 1000, tz=datetime.timezone.utc)

@property
def end(self) -> datetime.datetime:
def end(self) -> DiscordTime:
"""When the user will stop playing this song in UTC."""
return datetime.datetime.fromtimestamp(self._timestamps["end"] / 1000, tz=datetime.timezone.utc)
return DiscordTime.fromtimestamp(self._timestamps["end"] / 1000, tz=datetime.timezone.utc)

@property
def duration(self) -> datetime.timedelta:
Expand Down
2 changes: 1 addition & 1 deletion discord/app/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
from ..ui.modal import Modal
from ..ui.view import View
from ..user import ClientUser, User
from ..utils.private import get_as_snowflake, parse_time, sane_wait_for
from ..utils.private import get_as_snowflake, sane_wait_for
from .cache import Cache
from .event_emitter import EventEmitter

Expand Down
7 changes: 4 additions & 3 deletions discord/audit_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@
from inspect import isawaitable
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generator, TypeVar

from . import enums, utils
from . import enums
from .asset import Asset
from .automod import AutoModAction, AutoModTriggerMetadata
from .colour import Colour
from .datetime import DiscordTime
from .invite import Invite
from .mixins import Hashable
from .object import Object
Expand Down Expand Up @@ -585,9 +586,9 @@ def __repr__(self) -> str:
return f"<AuditLogEntry id={self.id} action={self.action} user={self.user!r}>"

@cached_property
def created_at(self) -> datetime.datetime:
def created_at(self) -> DiscordTime:
"""Returns the entry's creation time in UTC."""
return utils.snowflake_time(self.id)
return DiscordTime.from_snowflake(self.id)

async def get_target(
self,
Expand Down
7 changes: 4 additions & 3 deletions discord/channel/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@
from typing_extensions import Self, TypeVar, override

from ..abc import Messageable, Snowflake, SnowflakeTime, User, _Overwrites, _purge_messages_helper
from ..datetime import DiscordTime
from ..emoji import GuildEmoji, PartialEmoji
from ..enums import ChannelType, InviteTarget, SortOrder, try_enum
from ..errors import ClientException
from ..flags import ChannelFlags, MessageFlags
from ..iterators import ArchivedThreadIterator
from ..mixins import Hashable
from ..utils import MISSING, Undefined, find, snowflake_time
from ..utils import MISSING, Undefined, find
from ..utils.private import SnowflakeList, bytes_to_base64_data, copy_doc, get_as_snowflake

if TYPE_CHECKING:
Expand Down Expand Up @@ -114,9 +115,9 @@ async def _get_channel(self) -> Self:
return self

@property
def created_at(self) -> datetime.datetime:
def created_at(self) -> DiscordTime:
"""The channel's creation time in UTC."""
return snowflake_time(self.id)
return DiscordTime.from_snowflake(self.id)

@abstractmethod
@override
Expand Down
15 changes: 8 additions & 7 deletions discord/channel/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from discord import utils

from ..abc import Messageable, _purge_messages_helper
from ..datetime import DiscordTime
from ..enums import (
ChannelType,
try_enum,
Expand All @@ -42,7 +43,7 @@
from ..mixins import Hashable
from ..types.threads import Thread as ThreadPayload
from ..utils import MISSING
from ..utils.private import get_as_snowflake, parse_time
from ..utils.private import get_as_snowflake
from .base import BaseChannel, GuildMessageableChannel

__all__ = (
Expand Down Expand Up @@ -128,9 +129,9 @@ class Thread(BaseChannel[ThreadPayload], GuildMessageableChannel):
auto_archive_duration: int
The duration in minutes until the thread is automatically archived due to inactivity.
Usually a value of 60, 1440, 4320 and 10080.
archive_timestamp: datetime.datetime
archive_timestamp: discord.DiscordTime
An aware timestamp of when the thread's archived status was last updated in UTC.
created_at: datetime.datetime | None
created_at: discord.DiscordTime | None
An aware timestamp of when the thread was created.
Only available for threads created after 2022-01-09.
flags: ChannelFlags
Expand Down Expand Up @@ -205,10 +206,10 @@ async def _update(self, data: ThreadPayload) -> None:
metadata = data["thread_metadata"]
self.archived: bool = metadata["archived"]
self.auto_archive_duration: int = metadata["auto_archive_duration"]
self.archive_timestamp = parse_time(metadata["archive_timestamp"])
self.archive_timestamp: DiscordTime = DiscordTime.parse_time(metadata["archive_timestamp"])
self.locked: bool = metadata["locked"]
self.invitable: bool = metadata.get("invitable", True)
self.created_at = parse_time(metadata.get("create_timestamp"))
self.created_at: DiscordTime | None = DiscordTime.parse_time(metadata.get("create_timestamp"))

# Handle thread member data
if "member" in data:
Expand Down Expand Up @@ -856,7 +857,7 @@ class ThreadMember(Hashable):
The thread member's ID.
thread_id: :class:`int`
The thread's ID.
joined_at: :class:`datetime.datetime`
joined_at: :class:`discord.DiscordTime`
The time the member joined the thread in UTC.
"""

Expand Down Expand Up @@ -889,7 +890,7 @@ def _from_data(self, data: ThreadMemberPayload):
except KeyError:
self.thread_id = self.parent.id

self.joined_at = parse_time(data["join_timestamp"])
self.joined_at: DiscordTime | None = DiscordTime.parse_time(data["join_timestamp"])
self.flags = data["flags"]

@property
Expand Down
7 changes: 4 additions & 3 deletions discord/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

from ..channel import PartialMessageable, _threaded_guild_channel_factory
from ..channel.thread import Thread
from ..datetime import DiscordTime
from ..enums import Enum as DiscordEnum
from ..enums import (
IntegrationType,
Expand All @@ -70,7 +71,7 @@
from ..object import Object
from ..role import Role
from ..user import User
from ..utils import MISSING, find, utcnow
from ..utils import MISSING, find
from ..utils.private import async_all, maybe_awaitable, warn_deprecated
from .context import ApplicationContext, AutocompleteContext
from .options import Option, OptionChoice
Expand Down Expand Up @@ -376,7 +377,7 @@ def is_on_cooldown(self, ctx: ApplicationContext) -> bool:
return False

bucket = self._buckets.get_bucket(ctx) # type: ignore
current = utcnow().timestamp()
current = DiscordTime.utcnow().timestamp()
return bucket.get_tokens(current) == 0

def reset_cooldown(self, ctx: ApplicationContext) -> None:
Expand Down Expand Up @@ -411,7 +412,7 @@ def get_cooldown_retry_after(self, ctx: ApplicationContext) -> float:
"""
if self._buckets.valid:
bucket = self._buckets.get_bucket(ctx) # type: ignore
current = utcnow().timestamp()
current = DiscordTime.utcnow().timestamp()
return bucket.get_retry_after(current)

return 0.0
Expand Down
Loading
Loading