From c95dfe520f06b3082b8893c6bc9403fe8078fad2 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 23 Mar 2025 19:52:07 +0100 Subject: [PATCH 1/9] feat: Add support for scheduled event recurrence --- discord/enums.py | 10 + discord/guild.py | 24 +- discord/scheduled_events.py | 360 +++++++++++++++++++++++++++++- discord/types/scheduled_events.py | 24 ++ docs/api/data_classes.rst | 8 + docs/api/enums.rst | 23 ++ 6 files changed, 437 insertions(+), 12 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 91425b8cf8..77fc831ff9 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -76,6 +76,7 @@ "EntitlementOwnerType", "IntegrationType", "InteractionContextType", + "ScheduledEventRecurrenceFrequency", ) @@ -1054,6 +1055,15 @@ class PollLayoutType(Enum): default = 1 +class ScheduledEventRecurrenceFrequency(Enum): + """A scheduled event recurrence rule's frequency.""" + + yearly = 0 + monthly = 1 + weekly = 2 + daily = 3 + + T = TypeVar("T") diff --git a/discord/guild.py b/discord/guild.py index b1e937d07b..830d38e3bd 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -80,7 +80,7 @@ from .onboarding import Onboarding from .permissions import PermissionOverwrite from .role import Role -from .scheduled_events import ScheduledEvent, ScheduledEventLocation +from .scheduled_events import ScheduledEvent, ScheduledEventLocation, ScheduledEventRecurrenceRule from .stage_instance import StageInstance from .sticker import GuildSticker from .threads import Thread, ThreadMember @@ -3770,14 +3770,16 @@ async def create_scheduled_event( *, name: str, description: str = MISSING, - start_time: datetime, - end_time: datetime = MISSING, + start_time: datetime.datetime, + end_time: datetime.datetime = MISSING, location: str | int | VoiceChannel | StageChannel | ScheduledEventLocation, privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, image: bytes = MISSING, - ) -> ScheduledEvent | None: + recurrence_rule: ScheduledEventRecurrenceRule | None = MISSING, + ) -> ScheduledEvent: """|coro| + Creates a scheduled event. Parameters @@ -3799,7 +3801,10 @@ async def create_scheduled_event( reason: Optional[:class:`str`] The reason to show in the audit log. image: Optional[:class:`bytes`] - The cover image of the scheduled event + The cover image of the scheduled event. + recurrence_rule: Optional[:class:`ScheduledEventRecurrenceRule`] + The recurrence rule this event will follow. If this is ``None`` then this is a + one-time event. Returns ------- @@ -3813,7 +3818,8 @@ async def create_scheduled_event( HTTPException The operation failed. """ - payload: dict[str, str | int] = { + + payload: dict[str, Any] = { "name": name, "scheduled_start_time": start_time.isoformat(), "privacy_level": int(privacy_level), @@ -3840,6 +3846,12 @@ async def create_scheduled_event( if image is not MISSING: payload["image"] = utils._bytes_to_base64_data(image) + if recurrence_rule is not MISSING: + if recurrence_rule is None: + payload['recurrence_rule'] = None + else: + payload['recurrence_rule'] = recurrence_rule._to_dict() + data = await self._state.http.create_scheduled_event( guild_id=self.id, reason=reason, **payload ) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 7e339dcee7..fc57decfdf 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -25,17 +25,18 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from . import utils from .asset import Asset from .enums import ( ScheduledEventLocationType, ScheduledEventPrivacyLevel, + ScheduledEventRecurrenceFrequency, ScheduledEventStatus, try_enum, ) -from .errors import InvalidArgument, ValidationError +from .errors import InvalidArgument, ValidationError, ClientException from .iterators import ScheduledEventSubscribersIterator from .mixins import Hashable from .object import Object @@ -44,16 +45,25 @@ __all__ = ( "ScheduledEvent", "ScheduledEventLocation", + "ScheduledEventRecurrenceRule", ) if TYPE_CHECKING: + from typing_extensions import Self + from .abc import Snowflake from .guild import Guild - from .iterators import AsyncIterator from .member import Member from .state import ConnectionState from .types.channel import StageChannel, VoiceChannel - from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload + from .types.scheduled_events import ( + ScheduledEvent as ScheduledEventPayload, + ScheduledEventRecurrenceRule as ScheduledEventRecurrenceRulePayload, + ) + + Week = Literal[1, 2, 3, 4, 5] + WeekDay = Literal[0, 1, 2, 3, 4, 5, 6] + NWeekDay = tuple[Week, WeekDay] MISSING = utils.MISSING @@ -115,6 +125,311 @@ def type(self) -> ScheduledEventLocationType: return ScheduledEventLocationType.voice +class ScheduledEventRecurrenceRule: + """Represents a :class:`ScheduledEvent`'s recurrence rule. + + .. versionadded:: 2.7 + + Parameters + ---------- + start_date: :class:`datetime.datetime` + When will this recurrence rule start. + frequency: :class:`ScheduledEventRecurrenceFrequency` + The frequency on which the event will recur. + interval: :class:`int` + The spacing between events, defined by ``frequency``. + Must be ``1`` except if ``frequency`` is :attr:`ScheduledEventRecurrenceFrequency.weekly`, + in which case it can also be ``2``. + weekdays: List[:class:`int`] + The days within a week the event will recur on. Must be between + 0 (Monday) and 6 (Sunday). + If ``frequency`` is ``2`` this can only have 1 item. + This is mutally exclusive with ``n_weekdays`` and ``month_days``. + n_weekdays: List[Tuple[:class:`int`, :class:`int`]] + A (week, weekday) pairs list that represent the specific day within a + specific week the event will recur on. + ``week`` must be between 1 and 5, representing the first and last week of a month + respectively. + ``weekday`` must be an integer between 0 (Monday) and 6 (Sunday). + This is mutually exclusive with ``weekdays`` and ``month_days``. + month_days: List[:class:`datetime.date`] + The specific days and months in which the event will recur on. The year will be ignored. + This is mutually exclusive with ``weekdays`` and ``n_weekdays``. + + Attributes + ---------- + end_date: Optional[:class:`datetime.datetime`] + The date on which this recurrence rule will stop. + count: Optional[:class:`int`] + The amount of times the event will recur before stopping. Will be ``None`` + if :attr:`ScheduledEventRecurrenceRule.end_date` is ``None``. + + + Examples + -------- + Creating a recurrence rule that repeats every weekday: :: + rrule = discord.ScheduledEventRecurrenceRule( + start_date=..., + frequency=discord.ScheduledEventRecurrenceFrequency.daily, + interval=1, + weekdays=[0, 1, 2, 3, 4], # from monday to friday + ) + Creating a recurrence rule that repeats every Wednesday: :: + rrule = discord.ScheduledEventRecurrenceRule( + start_date=..., + frequency=discord.ScheduledEventRecurrenceFrequency.weekly, + interval=1, # interval must be 1 for the rule to be "every Wednesday" + weekdays=[2], # wednesday + ) + Creating a recurrence rule that repeats every other Wednesday: :: + rrule = discord.ScheduledEventRecurrenceRule( + start_date=..., + frequency=discord.ScheduledEventRecurrenceFrequency.weekly, + interval=2, # interval CAN ONLY BE 2 in this context, and makes the rule be "every other Wednesday" + weekdays=[2], + ) + Creating a recurrence rule that repeats every monthly on the fourth Wednesday: :: + rrule = discord.ScheduledEventRecurrenceRule( + start_date=..., + frequency=discord.ScheduledEventRecurrenceFrequency.monthly, + interval=1, + n_weekdays=[ + ( + 4, # fourth week + 2, # wednesday + ), + ], + ) + Creating a recurrence rule that repeats anually on July 24: :: + rrule = discord.ScheduledEventRecurrenceRule( + start_date=..., + frequency=discord.ScheduledEventRecurrenceFrequency.yearly, + month_days=[ + datetime.date( + year=1900, # use a placeholder year, it is ignored anyways + month=7, # July + day=24, # 24th + ), + ], + ) + """ + + __slots__ = ( + 'start_date', + 'frequency', + 'interval', + 'count', + 'end_date', + '_weekdays', + '_n_weekdays', + '_month_days', + '_year_days', + '_state', + ) + + def __init__( + self, + start_date: datetime.datetime, + frequency: ScheduledEventRecurrenceFrequency, + interval: Literal[1, 2], + *, + weekdays: list[WeekDay] = MISSING, + n_weekdays: list[NWeekDay] = MISSING, + month_days: list[datetime.date] = MISSING, + ) -> None: + self.start_date: datetime.datetime = start_date + self.frequency: ScheduledEventRecurrenceFrequency = frequency + self.interval: Literal[1, 2] = interval + + self.count: int | None = None + self.end_date: datetime.datetime | None = None + + self._weekdays: list[WeekDay] = weekdays + self._n_weekdays: list[NWeekDay] = n_weekdays + self._month_days: list[datetime.date] = month_days + self._year_days: list[int] = MISSING + + self._state: ConnectionState | None = None + + def __repr__(self) -> str: + return f'' + + @property + def weekdays(self) -> list[WeekDay]: + """Returns a read-only list containing all the specific days + within a week on which the event will recur on. + """ + if self._weekdays is MISSING: + return [] + return self._weekdays.copy() + + @property + def n_weekdays(self) -> list[NWeekDay]: + """Returns a read-only list containing all the specific days + within a specific week on which the event will recur on. + """ + if self._n_weekdays is MISSING: + return [] + return self._n_weekdays.copy() + + @property + def month_days(self) -> list[datetime.date]: + """Returns a read-only list containing all the specific days + within a specific month on which the event will recur on. + """ + if self._month_days is MISSING: + return [] + return self._month_days.copy() + + @property + def year_days(self) -> list[int]: + """Returns a read-only list containing all the specific days + of the year on which the event will recur on. + """ + if self._year_days is MISSING: + return [] + return self._year_days.copy() + + def copy(self) -> ScheduledEventRecurrenceRule: + """Creates a stateless copy of this object that allows for + methods such as :meth:`.edit` to be used on. + + Returns + ------- + :class:`ScheduledEventRecurrenceRule` + The recurrence rule copy. + """ + + return ScheduledEventRecurrenceRule( + start_date=self.start_date, + frequency=self.frequency, + interval=self.interval, + weekdays=self._weekdays, + n_weekdays=self._n_weekdays, + month_days=self._month_days, + ) + + def edit( + self, + *, + weekdays: list[WeekDay] | None = MISSING, + n_weekdays: list[NWeekDay] | None = MISSING, + month_days: list[datetime.date] | None = MISSING, + ) -> Self: + """Edits this recurrence rule. + + If this recurrence rule was obtained from the API you will need to + :meth:`.copy` it in order to edit it. + + Parameters + ---------- + weekdays: List[:class:`int`] + The weekdays the event will recur on. Must be between 0 (Monday) and 6 (Sunday). + n_weekdays: List[Tuple[:class:`int`, :class:`int`]] + A (week, weekday) tuple pair list that represents the specific ``weekday``, from 0 (Monday) + to 6 (Sunday), on ``week`` on which this event will recur on. + month_days: List[:class:`datetime.date`] + A list containing the specific day on a month when the event will recur on. The year + is ignored. + + Raises + ------ + ClientException + You cannot edit this recurrence rule. + + Returns + ------- + :class:`ScheduledEventRecurrenceRule` + The updated recurrence rule. + """ + + if self._state is not None: + raise ClientException("You cannot edit this recurrence rule") + + for value, attr in ( + (weekdays, '_weekdays'), + (n_weekdays, '_n_weekdays'), + (month_days, '_month_days'), + ): + if value is None: + setattr(self, attr, MISSING) + elif value is not MISSING: + setattr(self, attr, value) + + return self + + def _get_month_days_payload(self) -> tuple[list[int], list[int]]: + months, days = map(list, zip(*((m.month, m.day) for m in self._month_days))) + return months, days + + def _parse_month_days_payload(self, months: list[int], days: list[int]) -> list[datetime.date]: + return [datetime.date(1900, month, day) for month, day in zip(months, days)] + + @classmethod + def _from_data(cls, data: ScheduledEventRecurrenceRulePayload | None, state: ConnectionState) -> Self | None: + if data is None: + return None + + start = utils.parse_time(data['start']) + end = utils.parse_time(data.get('end')) + + self = cls( + start_date=start, + frequency=try_enum(ScheduledEventRecurrenceFrequency, data['frequency']), + interval=int(data['interval']), # pyright: ignore[reportArgumentType] + ) + + self._state = state + self.end_date = end + self.count = data.get('count') + + weekdays = data.get('by_weekday', MISSING) or MISSING + self._weekdays = weekdays + + n_weekdays = data.get('by_n_weekday', MISSING) or MISSING: + if n_weekdays is not MISSING: + self._n_weekdays = [(n['n'], n['day']) for n in n_weekdays] + + months = data.get('by_month') + month_days = data.get('by_month_day') + + if months and month_days: + self._month_days = self._parse_month_days_payload(months, month_days) + + year_days = data.get('by_year_day') + if year_days is not None: + self._year_days = year_days + + return self + + def _to_dict(self) -> ScheduledEventRecurrenceRulePayload: + payload: ScheduledEventRecurrenceRulePayload = { + 'start': self.start_date.isoformat(), + 'frequency': self.frequency.value, + 'interval': self.interval, + 'by_weekday': None, + 'by_n_weekday': None, + 'by_month': None, + 'by_month_day': None, + } + + if self._weekdays is not MISSING: + payload["by_weekday"] = self._weekdays + if self._n_weekdays is not MISSING: + payload["by_n_weekday"] = list( + map( + lambda nw: {'n': nw[0], 'day': nw[1]}, + self._n_weekdays, + ), + ) + if self._month_days is not MISSING: + months, month_days = self._get_month_days_payload() + payload["by_month"] = months + payload["by_month_day"] = month_days + + return payload + + class ScheduledEvent(Hashable): """Represents a Discord Guild Scheduled Event. @@ -167,6 +482,15 @@ class ScheduledEvent(Hashable): The privacy level of the event. Currently, the only possible value is :attr:`ScheduledEventPrivacyLevel.guild_only`, which is default, so there is no need to use this attribute. + recurrence_rule: Optional[:class:`ScheduledEventRecurrenceRule`] + The recurrence rule this scheduled event follows. + + .. versionadded:: 2.7 + exceptions: List[:class:`Object`] + A list of objects that represents the events on the recurrence rule that were + cancelled or moved out of it. + + .. versionadded:: 2.7 """ __slots__ = ( @@ -183,6 +507,8 @@ class ScheduledEvent(Hashable): "_state", "_image", "subscriber_count", + "recurrence_rule", + 'exceptions', ) def __init__( @@ -222,6 +548,16 @@ def __init__( else: self.location = ScheduledEventLocation(state=state, value=int(channel_id)) + self.recurrence_rule: ScheduledEventRecurrenceRule | None = ScheduledEventRecurrenceRule._from_data( + data.get("recurrence_rule"), state, + ) + self.exceptions: list[Object] = list( + map( + Object, + data.get('guild_scheduled_events_exceptions', []) or [], + ), + ) + def __str__(self) -> str: return self.name @@ -290,6 +626,7 @@ async def edit( cover: bytes | None = MISSING, image: bytes | None = MISSING, privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, + recurrence_rule: ScheduledEventRecurrenceRule | None = MISSING, ) -> ScheduledEvent | None: """|coro| @@ -330,6 +667,11 @@ async def edit( .. deprecated:: 2.7 Use the `image` argument instead. + recurrence_rule: Optional[:class:`ScheduledEventRecurrenceRule`] + The recurrence rule this event will follow, or ``None`` to set it to a + one-time event. + + .. versionadded:: 2.7 Returns ------- @@ -402,6 +744,12 @@ async def edit( if end_time is not MISSING: payload["scheduled_end_time"] = end_time.isoformat() + if recurrence_rule is not MISSING: + if recurrence_rule is None: + payload["recurrence_rule"] = None + else: + payload["recurrence_rule"] = recurrence_rule._to_dict() + if payload != {}: data = await self._state.http.edit_scheduled_event( self.guild.id, self.id, **payload, reason=reason @@ -452,7 +800,7 @@ async def start(self, *, reason: str | None = None) -> None: """ return await self.edit(status=ScheduledEventStatus.active, reason=reason) - async def complete(self, *, reason: str | None = None) -> None: + async def complete(self, *, reason: str | None = None) -> ScheduledEvent | None: """|coro| Ends/completes the scheduled event. Shortcut from :meth:`.edit`. @@ -480,7 +828,7 @@ async def complete(self, *, reason: str | None = None) -> None: """ return await self.edit(status=ScheduledEventStatus.completed, reason=reason) - async def cancel(self, *, reason: str | None = None) -> None: + async def cancel(self, *, reason: str | None = None) -> ScheduledEvent | None: """|coro| Cancels the scheduled event. Shortcut from :meth:`.edit`. diff --git a/discord/types/scheduled_events.py b/discord/types/scheduled_events.py index 9bb4ad0328..93aae6b830 100644 --- a/discord/types/scheduled_events.py +++ b/discord/types/scheduled_events.py @@ -25,6 +25,7 @@ from __future__ import annotations from typing import Literal, TypedDict +from typing_extensions import NotRequired from .member import Member from .snowflake import Snowflake @@ -33,6 +34,8 @@ ScheduledEventStatus = Literal[1, 2, 3, 4] ScheduledEventLocationType = Literal[1, 2, 3] ScheduledEventPrivacyLevel = Literal[2] +ScheduledEventRecurrenceFrequency = Literal[0, 1, 2, 3] +ScheduledEventWeekdayRecurrence = Literal[0, 1, 2, 3, 4, 5, 6] class ScheduledEvent(TypedDict): @@ -52,6 +55,9 @@ class ScheduledEvent(TypedDict): entity_metadata: ScheduledEventEntityMetadata creator: User user_count: int | None + recurrence_rule: ScheduledEventRecurrenceRule | None + auto_start: bool + guild_scheduled_events_exceptions: list[Snowflake] class ScheduledEventEntityMetadata(TypedDict): @@ -63,3 +69,21 @@ class ScheduledEventSubscriber(TypedDict): user_id: Snowflake user: User member: Member | None + + +class ScheduledEventRecurrenceRule(TypedDict): + start: str + end: NotRequired[str | None] + frequency: ScheduledEventRecurrenceFrequency + interval: int + by_weekday: list[ScheduledEventWeekdayRecurrence] | None + by_n_weekday: list[ScheduledEventNWeekdayRecurrence] | None + by_month: list[int] | None + by_month_day: list[int] | None + by_year_day: NotRequired[list[int] | None] + count: NotRequired[int | None] + + +class ScheduledEventNWeekdayRecurrence(TypedDict): + n: Literal[1, 2, 3, 4, 5] + day: ScheduledEventWeekdayRecurrence diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index 1d891b90cd..ba1f965c53 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -126,6 +126,14 @@ Poll .. autoclass:: PollResults :members: +Scheduled Event Recurrence rule +------------------------------- + +.. attributetable:: ScheduledEventRecurrenceRule + +.. autoclass:: ScheduledEventRecurrenceRule + :members: + Flags diff --git a/docs/api/enums.rst b/docs/api/enums.rst index cd48a85cf5..f71ed4161a 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2500,3 +2500,26 @@ of :class:`enum.Enum`. .. attribute:: private_channel The interaction is in a private DM or group DM channel. + + +.. class:: ScheduledEventRecurrenceRuleFrequency + + A scheduled event recurrence rule's frequency. + + .. versionadded:: 2.7 + + .. attribute:: yearly + + The event will repeat on a yearly basis. + + .. attribute:: monthly + + The event will repeat on a monthly basis. + + .. attribute:: weekly + + The event will repeat on a weekly basis. + + .. attribute:: daily + + The event will repeat on a daily basis. From e87b93d3cbf7dea57a9f9d0784879969958cd006 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 18:57:00 +0000 Subject: [PATCH 2/9] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/guild.py | 10 +++++++--- discord/scheduled_events.py | 15 +++++++-------- discord/types/scheduled_events.py | 1 + 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 4149fbc17f..74e3d1395c 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -80,7 +80,11 @@ from .onboarding import Onboarding from .permissions import PermissionOverwrite from .role import Role -from .scheduled_events import ScheduledEvent, ScheduledEventLocation, ScheduledEventRecurrenceRule +from .scheduled_events import ( + ScheduledEvent, + ScheduledEventLocation, + ScheduledEventRecurrenceRule, +) from .stage_instance import StageInstance from .sticker import GuildSticker from .threads import Thread, ThreadMember @@ -3848,9 +3852,9 @@ async def create_scheduled_event( if recurrence_rule is not MISSING: if recurrence_rule is None: - payload['recurrence_rule'] = None + payload["recurrence_rule"] = None else: - payload['recurrence_rule'] = recurrence_rule._to_dict() + payload["recurrence_rule"] = recurrence_rule._to_dict() data = await self._state.http.create_scheduled_event( guild_id=self.id, reason=reason, **payload diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index fc57decfdf..6be69bff74 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -36,7 +36,7 @@ ScheduledEventStatus, try_enum, ) -from .errors import InvalidArgument, ValidationError, ClientException +from .errors import ClientException, InvalidArgument, ValidationError from .iterators import ScheduledEventSubscribersIterator from .mixins import Hashable from .object import Object @@ -56,8 +56,8 @@ from .member import Member from .state import ConnectionState from .types.channel import StageChannel, VoiceChannel + from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload from .types.scheduled_events import ( - ScheduledEvent as ScheduledEventPayload, ScheduledEventRecurrenceRule as ScheduledEventRecurrenceRulePayload, ) @@ -164,7 +164,6 @@ class ScheduledEventRecurrenceRule: The amount of times the event will recur before stopping. Will be ``None`` if :attr:`ScheduledEventRecurrenceRule.end_date` is ``None``. - Examples -------- Creating a recurrence rule that repeats every weekday: :: @@ -332,15 +331,15 @@ def edit( A list containing the specific day on a month when the event will recur on. The year is ignored. + Returns + ------- + :class:`ScheduledEventRecurrenceRule` + The updated recurrence rule. + Raises ------ ClientException You cannot edit this recurrence rule. - - Returns - ------- - :class:`ScheduledEventRecurrenceRule` - The updated recurrence rule. """ if self._state is not None: diff --git a/discord/types/scheduled_events.py b/discord/types/scheduled_events.py index 93aae6b830..09d9943d69 100644 --- a/discord/types/scheduled_events.py +++ b/discord/types/scheduled_events.py @@ -25,6 +25,7 @@ from __future__ import annotations from typing import Literal, TypedDict + from typing_extensions import NotRequired from .member import Member From 476d1069b8f382ab5a33cca0ddbc4381c1bda10e Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 23 Mar 2025 19:58:26 +0100 Subject: [PATCH 3/9] oops --- discord/scheduled_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 6be69bff74..e550aa90bd 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -385,7 +385,7 @@ def _from_data(cls, data: ScheduledEventRecurrenceRulePayload | None, state: Con weekdays = data.get('by_weekday', MISSING) or MISSING self._weekdays = weekdays - n_weekdays = data.get('by_n_weekday', MISSING) or MISSING: + n_weekdays = data.get('by_n_weekday', MISSING) or MISSING if n_weekdays is not MISSING: self._n_weekdays = [(n['n'], n['day']) for n in n_weekdays] From 14ebe2009c18c7249194b1d9eea27fd2c8250926 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 18:58:56 +0000 Subject: [PATCH 4/9] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/scheduled_events.py | 85 ++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index e550aa90bd..1e96a019af 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -214,16 +214,16 @@ class ScheduledEventRecurrenceRule: """ __slots__ = ( - 'start_date', - 'frequency', - 'interval', - 'count', - 'end_date', - '_weekdays', - '_n_weekdays', - '_month_days', - '_year_days', - '_state', + "start_date", + "frequency", + "interval", + "count", + "end_date", + "_weekdays", + "_n_weekdays", + "_month_days", + "_year_days", + "_state", ) def __init__( @@ -251,7 +251,7 @@ def __init__( self._state: ConnectionState | None = None def __repr__(self) -> str: - return f'' + return f"" @property def weekdays(self) -> list[WeekDay]: @@ -346,9 +346,9 @@ def edit( raise ClientException("You cannot edit this recurrence rule") for value, attr in ( - (weekdays, '_weekdays'), - (n_weekdays, '_n_weekdays'), - (month_days, '_month_days'), + (weekdays, "_weekdays"), + (n_weekdays, "_n_weekdays"), + (month_days, "_month_days"), ): if value is None: setattr(self, attr, MISSING) @@ -361,41 +361,45 @@ def _get_month_days_payload(self) -> tuple[list[int], list[int]]: months, days = map(list, zip(*((m.month, m.day) for m in self._month_days))) return months, days - def _parse_month_days_payload(self, months: list[int], days: list[int]) -> list[datetime.date]: + def _parse_month_days_payload( + self, months: list[int], days: list[int] + ) -> list[datetime.date]: return [datetime.date(1900, month, day) for month, day in zip(months, days)] @classmethod - def _from_data(cls, data: ScheduledEventRecurrenceRulePayload | None, state: ConnectionState) -> Self | None: + def _from_data( + cls, data: ScheduledEventRecurrenceRulePayload | None, state: ConnectionState + ) -> Self | None: if data is None: return None - start = utils.parse_time(data['start']) - end = utils.parse_time(data.get('end')) + start = utils.parse_time(data["start"]) + end = utils.parse_time(data.get("end")) self = cls( start_date=start, - frequency=try_enum(ScheduledEventRecurrenceFrequency, data['frequency']), - interval=int(data['interval']), # pyright: ignore[reportArgumentType] + frequency=try_enum(ScheduledEventRecurrenceFrequency, data["frequency"]), + interval=int(data["interval"]), # pyright: ignore[reportArgumentType] ) self._state = state self.end_date = end - self.count = data.get('count') + self.count = data.get("count") - weekdays = data.get('by_weekday', MISSING) or MISSING + weekdays = data.get("by_weekday", MISSING) or MISSING self._weekdays = weekdays - n_weekdays = data.get('by_n_weekday', MISSING) or MISSING + n_weekdays = data.get("by_n_weekday", MISSING) or MISSING if n_weekdays is not MISSING: - self._n_weekdays = [(n['n'], n['day']) for n in n_weekdays] + self._n_weekdays = [(n["n"], n["day"]) for n in n_weekdays] - months = data.get('by_month') - month_days = data.get('by_month_day') + months = data.get("by_month") + month_days = data.get("by_month_day") if months and month_days: self._month_days = self._parse_month_days_payload(months, month_days) - year_days = data.get('by_year_day') + year_days = data.get("by_year_day") if year_days is not None: self._year_days = year_days @@ -403,13 +407,13 @@ def _from_data(cls, data: ScheduledEventRecurrenceRulePayload | None, state: Con def _to_dict(self) -> ScheduledEventRecurrenceRulePayload: payload: ScheduledEventRecurrenceRulePayload = { - 'start': self.start_date.isoformat(), - 'frequency': self.frequency.value, - 'interval': self.interval, - 'by_weekday': None, - 'by_n_weekday': None, - 'by_month': None, - 'by_month_day': None, + "start": self.start_date.isoformat(), + "frequency": self.frequency.value, + "interval": self.interval, + "by_weekday": None, + "by_n_weekday": None, + "by_month": None, + "by_month_day": None, } if self._weekdays is not MISSING: @@ -417,7 +421,7 @@ def _to_dict(self) -> ScheduledEventRecurrenceRulePayload: if self._n_weekdays is not MISSING: payload["by_n_weekday"] = list( map( - lambda nw: {'n': nw[0], 'day': nw[1]}, + lambda nw: {"n": nw[0], "day": nw[1]}, self._n_weekdays, ), ) @@ -507,7 +511,7 @@ class ScheduledEvent(Hashable): "_image", "subscriber_count", "recurrence_rule", - 'exceptions', + "exceptions", ) def __init__( @@ -547,13 +551,16 @@ def __init__( else: self.location = ScheduledEventLocation(state=state, value=int(channel_id)) - self.recurrence_rule: ScheduledEventRecurrenceRule | None = ScheduledEventRecurrenceRule._from_data( - data.get("recurrence_rule"), state, + self.recurrence_rule: ScheduledEventRecurrenceRule | None = ( + ScheduledEventRecurrenceRule._from_data( + data.get("recurrence_rule"), + state, + ) ) self.exceptions: list[Object] = list( map( Object, - data.get('guild_scheduled_events_exceptions', []) or [], + data.get("guild_scheduled_events_exceptions", []) or [], ), ) From 2fed715f131731f320856571e7271268ef1d063f Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 1 Apr 2025 09:53:36 +0200 Subject: [PATCH 5/9] update scheduled_events.py Co-authored-by: plun1331 Signed-off-by: DA344 <108473820+DA-344@users.noreply.github.com> --- discord/scheduled_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 1e96a019af..7823ab081b 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -187,7 +187,7 @@ class ScheduledEventRecurrenceRule: interval=2, # interval CAN ONLY BE 2 in this context, and makes the rule be "every other Wednesday" weekdays=[2], ) - Creating a recurrence rule that repeats every monthly on the fourth Wednesday: :: + Creating a recurrence rule that repeats every month on the fourth Wednesday: :: rrule = discord.ScheduledEventRecurrenceRule( start_date=..., frequency=discord.ScheduledEventRecurrenceFrequency.monthly, From 2e6cfa92c846a52ec02d6863c31c12cf41231d83 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:06:34 +0200 Subject: [PATCH 6/9] chore: do some things i dont remember --- discord/enums.py | 13 +++++++++++++ discord/scheduled_events.py | 33 ++++++++++++++++++++++----------- docs/api/enums.rst | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index c625b56e79..7e52673dfe 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -77,6 +77,7 @@ "IntegrationType", "InteractionContextType", "ScheduledEventRecurrenceFrequency", + "ScheduledEventWeekday" ) @@ -1073,6 +1074,18 @@ class ScheduledEventRecurrenceFrequency(Enum): daily = 3 +class ScheduledEventWeekday(Enum): + """A scheduled event week day.""" + + monday = 0 + tuesday = 1 + wednesday = 2 + thursday = 3 + friday = 4 + saturday = 5 + sunday = 6 + + T = TypeVar("T") diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 7823ab081b..bd5a5d2e0f 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -34,6 +34,7 @@ ScheduledEventPrivacyLevel, ScheduledEventRecurrenceFrequency, ScheduledEventStatus, + ScheduledEventWeekday, try_enum, ) from .errors import ClientException, InvalidArgument, ValidationError @@ -140,7 +141,7 @@ class ScheduledEventRecurrenceRule: The spacing between events, defined by ``frequency``. Must be ``1`` except if ``frequency`` is :attr:`ScheduledEventRecurrenceFrequency.weekly`, in which case it can also be ``2``. - weekdays: List[:class:`int`] + weekdays: List[Union[:class:`int`, :class:`ScheduledEventWeekday`]] The days within a week the event will recur on. Must be between 0 (Monday) and 6 (Sunday). If ``frequency`` is ``2`` this can only have 1 item. @@ -207,7 +208,7 @@ class ScheduledEventRecurrenceRule: datetime.date( year=1900, # use a placeholder year, it is ignored anyways month=7, # July - day=24, # 24th + day=4, # 4th ), ], ) @@ -232,7 +233,7 @@ def __init__( frequency: ScheduledEventRecurrenceFrequency, interval: Literal[1, 2], *, - weekdays: list[WeekDay] = MISSING, + weekdays: list[WeekDay | ScheduledEventWeekday] = MISSING, n_weekdays: list[NWeekDay] = MISSING, month_days: list[datetime.date] = MISSING, ) -> None: @@ -243,7 +244,7 @@ def __init__( self.count: int | None = None self.end_date: datetime.datetime | None = None - self._weekdays: list[WeekDay] = weekdays + self._weekdays: list[ScheduledEventWeekday] = self._parse_weekdays(weekdays) self._n_weekdays: list[NWeekDay] = n_weekdays self._month_days: list[datetime.date] = month_days self._year_days: list[int] = MISSING @@ -254,7 +255,7 @@ def __repr__(self) -> str: return f"" @property - def weekdays(self) -> list[WeekDay]: + def weekdays(self) -> list[ScheduledEventWeekday]: """Returns a read-only list containing all the specific days within a week on which the event will recur on. """ @@ -303,7 +304,7 @@ def copy(self) -> ScheduledEventRecurrenceRule: start_date=self.start_date, frequency=self.frequency, interval=self.interval, - weekdays=self._weekdays, + weekdays=self._weekdays, # pyright: ignore[reportArgumentType] n_weekdays=self._n_weekdays, month_days=self._month_days, ) @@ -311,7 +312,7 @@ def copy(self) -> ScheduledEventRecurrenceRule: def edit( self, *, - weekdays: list[WeekDay] | None = MISSING, + weekdays: list[WeekDay | ScheduledEventWeekday] | None = MISSING, n_weekdays: list[NWeekDay] | None = MISSING, month_days: list[datetime.date] | None = MISSING, ) -> Self: @@ -322,7 +323,7 @@ def edit( Parameters ---------- - weekdays: List[:class:`int`] + weekdays: List[Union[:class:`int`, :class:`ScheduledEventWeekday`]] The weekdays the event will recur on. Must be between 0 (Monday) and 6 (Sunday). n_weekdays: List[Tuple[:class:`int`, :class:`int`]] A (week, weekday) tuple pair list that represents the specific ``weekday``, from 0 (Monday) @@ -353,6 +354,8 @@ def edit( if value is None: setattr(self, attr, MISSING) elif value is not MISSING: + if attr == "_weekdays": + value = self._parse_weekdays(weekdays) # pyright: ignore[reportArgumentType] setattr(self, attr, value) return self @@ -364,7 +367,15 @@ def _get_month_days_payload(self) -> tuple[list[int], list[int]]: def _parse_month_days_payload( self, months: list[int], days: list[int] ) -> list[datetime.date]: - return [datetime.date(1900, month, day) for month, day in zip(months, days)] + return [datetime.date(1, month, day) for month, day in zip(months, days)] + + def _parse_weekdays( + self, weekdays: list[WeekDay | ScheduledEventWeekday] + ) -> list[ScheduledEventWeekday]: + return [w if w is ScheduledEventWeekday else try_enum(ScheduledEventWeekday, w) for w in weekdays] + + def _get_weekdays(self) -> list[WeekDay]: + return [w.value for w in self._weekdays] @classmethod def _from_data( @@ -387,7 +398,7 @@ def _from_data( self.count = data.get("count") weekdays = data.get("by_weekday", MISSING) or MISSING - self._weekdays = weekdays + self._weekdays = self._parse_weekdays(weekdays) # pyright: ignore[reportArgumentType] n_weekdays = data.get("by_n_weekday", MISSING) or MISSING if n_weekdays is not MISSING: @@ -417,7 +428,7 @@ def _to_dict(self) -> ScheduledEventRecurrenceRulePayload: } if self._weekdays is not MISSING: - payload["by_weekday"] = self._weekdays + payload["by_weekday"] = self._get_weekdays() if self._n_weekdays is not MISSING: payload["by_n_weekday"] = list( map( diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 24831cb4c7..2c3ad89cf2 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2542,3 +2542,38 @@ of :class:`enum.Enum`. .. attribute:: daily The event will repeat on a daily basis. + + +.. class:: ScheduledEventWeekday + + Represents a scheduled event weekday. + + .. versionadded:: 2.7 + + .. attribute:: monday + + Monday, the first day of the week. Index of 0. + + .. attribute:: tuesday + + Tuesday, the second day of the week. Index of 1. + + .. attribute:: wednesday + + Wednesday, the third day of the week. Index of 2. + + .. attribute:: thursday + + Thrusday, the fourth day of the week. Index of 3. + + .. attribute:: friday + + Friday, the fifth day of the week. Index of 4. + + .. attribute:: saturday + + Saturday, the sixth day of the week. Index of 5. + + .. attribute:: sunday + + Sunday, the seventh day of the week. Index of 6. From 548304a8d068a0ebd2f9857e397a4317b688857c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 17:07:12 +0000 Subject: [PATCH 7/9] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/enums.py | 2 +- discord/scheduled_events.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 7e52673dfe..3c6867b14c 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -77,7 +77,7 @@ "IntegrationType", "InteractionContextType", "ScheduledEventRecurrenceFrequency", - "ScheduledEventWeekday" + "ScheduledEventWeekday", ) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index bd5a5d2e0f..5335c9bdb6 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -355,7 +355,9 @@ def edit( setattr(self, attr, MISSING) elif value is not MISSING: if attr == "_weekdays": - value = self._parse_weekdays(weekdays) # pyright: ignore[reportArgumentType] + value = self._parse_weekdays( + weekdays + ) # pyright: ignore[reportArgumentType] setattr(self, attr, value) return self @@ -372,7 +374,10 @@ def _parse_month_days_payload( def _parse_weekdays( self, weekdays: list[WeekDay | ScheduledEventWeekday] ) -> list[ScheduledEventWeekday]: - return [w if w is ScheduledEventWeekday else try_enum(ScheduledEventWeekday, w) for w in weekdays] + return [ + w if w is ScheduledEventWeekday else try_enum(ScheduledEventWeekday, w) + for w in weekdays + ] def _get_weekdays(self) -> list[WeekDay]: return [w.value for w in self._weekdays] @@ -398,7 +403,9 @@ def _from_data( self.count = data.get("count") weekdays = data.get("by_weekday", MISSING) or MISSING - self._weekdays = self._parse_weekdays(weekdays) # pyright: ignore[reportArgumentType] + self._weekdays = self._parse_weekdays( + weekdays + ) # pyright: ignore[reportArgumentType] n_weekdays = data.get("by_n_weekday", MISSING) or MISSING if n_weekdays is not MISSING: From dca155783b8bc8737d344ea5ecafc0192bb5d516 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:41:26 +0200 Subject: [PATCH 8/9] chore: Replace 24 to 4 --- discord/scheduled_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index bd5a5d2e0f..0f18e74591 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -200,7 +200,7 @@ class ScheduledEventRecurrenceRule: ), ], ) - Creating a recurrence rule that repeats anually on July 24: :: + Creating a recurrence rule that repeats anually on July 4: :: rrule = discord.ScheduledEventRecurrenceRule( start_date=..., frequency=discord.ScheduledEventRecurrenceFrequency.yearly, From 0389bc148fa05b0a6af82d0b0f0121bfaa351d86 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:47:40 +0200 Subject: [PATCH 9/9] chore: Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38725e4141..4a0a8170e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2714](https://github.com/Pycord-Development/pycord/pull/2714)) - Added the ability to pass a `datetime.time` object to `format_dt` ([#2747](https://github.com/Pycord-Development/pycord/pull/2747)) +- Added support getting and setting recurrence rules on `ScheduledEvent`s. + ([#2749](https://github.com/Pycord-Development/pycord/pull/2749)) ### Fixed