From 9a7f49e92d993a262ef366fe5f711aff8b56831d Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 28 Aug 2025 09:49:39 +0200 Subject: [PATCH 01/35] fix: :bug: Fix `AttributeError` when accessing `AuditLogEntry.changes` more than once (#2882) * :bug: Fix `AttributeError` when accessing `AuditLogEntry.changes` more than once * :memo: CHANGELOG (cherry picked from commit 54ca771006aa150a1e4b5c45e71f797d25fd58d6) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d76398448..83807d1dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2843](https://github.com/Pycord-Development/pycord/pull/2843)) - Fixed `TypeError` when using `@option` with certain annotations and along with `channel_types`. ([#2835](https://github.com/Pycord-Development/pycord/pull/2835)) +- Fixed `AttributeError` when accessing `AuditLogEntry.changes` more than once. + ([#2882])(https://github.com/Pycord-Development/pycord/pull/2882)) ### Changed From 6085463b47ff209e4320aa06cb55f5b6dac85045 Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 28 Aug 2025 10:01:54 +0200 Subject: [PATCH 02/35] feat: :sparkles: Implement and document `AuditLogDiff.communication_disabled_until` (#2883) * :sparkles: Implement and document `AuditLogDiff.communication_disabled_until` * :memo: CHANGELOG.md --------- Signed-off-by: Lala Sabathil Co-authored-by: Lala Sabathil (cherry picked from commit 59d3f38662acd1eb92c95c6bce43e5e85e113cb8) --- CHANGELOG.md | 2 ++ discord/audit_logs.py | 12 ++++++++++-- docs/api/audit_logs.rst | 12 +++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83807d1dce..c6f705f561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2818](https://github.com/Pycord-Development/pycord/pull/2818)) - Added `Interaction.attachment_size_limit`. ([#2854](https://github.com/Pycord-Development/pycord/pull/2854)) +- Added `AuditLogDiff.communication_disabled_until`. + ([#2883](https://github.com/Pycord-Development/pycord/pull/2883)) ### Fixed diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 5078cacbdd..b5ff2efba4 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -25,6 +25,7 @@ from __future__ import annotations +import datetime from functools import cached_property from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generator, TypeVar @@ -46,8 +47,6 @@ if TYPE_CHECKING: - import datetime - from . import abc from .emoji import GuildEmoji from .guild import Guild @@ -197,6 +196,14 @@ def _transform_trigger_metadata( return AutoModTriggerMetadata.from_dict(data) +def _transform_communication_disabled_until( + entry: AuditLogEntry, data: str +) -> datetime.datetime | None: + if data: + return datetime.datetime.fromisoformat(data) + return None + + class AuditLogDiff: def __len__(self) -> int: return len(self.__dict__) @@ -269,6 +276,7 @@ class AuditLogChanges: "trigger_metadata": (None, _transform_trigger_metadata), "exempt_roles": (None, _transform_roles), "exempt_channels": (None, _transform_channels), + "communication_disabled_until": (None, _transform_communication_disabled_until), } def __init__( diff --git a/docs/api/audit_logs.rst b/docs/api/audit_logs.rst index c8ac5a877a..af90f95c18 100644 --- a/docs/api/audit_logs.rst +++ b/docs/api/audit_logs.rst @@ -524,11 +524,17 @@ this goal, it must make use of a couple of data classes that aid in this goal. :type: :class:`str` - .. attribute:: image + .. attribute:: image - The cover image of a :class:`ScheduledEvent`. + The cover image of a :class:`ScheduledEvent`. - :type: :class:`str` + :type: :class:`str` + + .. attribute:: communication_disabled_until + + Until when a :class:`Member` will be timed out + + :type: :class:`datetime.datetime` | :data:`None` .. this is currently missing the following keys: reason and application_id I'm not sure how to about porting these From 69c81c38853321bff44d42e46c9e2ad028135738 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Thu, 28 Aug 2025 11:50:38 +0200 Subject: [PATCH 03/35] feat: Add support for user.primary_guild (#2876) Signed-off-by: Lala Sabathil Signed-off-by: plun1331 <49261529+plun1331@users.noreply.github.com> Signed-off-by: UK <41271523+neloblivion@users.noreply.github.com> Signed-off-by: Lala Sabathil Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: Paillat (cherry picked from commit 8c00456f045175e7bb04cab00c78c3239efb5164) Signed-off-by: Paillat-dev --- CHANGELOG.md | 2 + discord/asset.py | 29 +++++++++++++ discord/primary_guild.py | 74 ++++++++++++++++++++++++++++++++++ discord/types/primary_guild.py | 36 +++++++++++++++++ discord/user.py | 11 +++++ 5 files changed, 152 insertions(+) create mode 100644 discord/primary_guild.py create mode 100644 discord/types/primary_guild.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f705f561..494e99a514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2854](https://github.com/Pycord-Development/pycord/pull/2854)) - Added `AuditLogDiff.communication_disabled_until`. ([#2883](https://github.com/Pycord-Development/pycord/pull/2883)) +- Added `discord.User.primary_guild` and the `PrimaryGuild` class. + ([#2876](https://github.com/Pycord-Development/pycord/pull/2876)) ### Fixed diff --git a/discord/asset.py b/discord/asset.py index c54162de83..b1e889fa9d 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -39,6 +39,8 @@ if TYPE_CHECKING: ValidStaticFormatTypes = Literal["webp", "jpeg", "jpg", "png"] ValidAssetFormatTypes = Literal["webp", "jpeg", "jpg", "png", "gif"] + from .state import ConnectionState + VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"}) VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"} @@ -204,6 +206,33 @@ def _from_avatar_decoration(cls, state, user_id: int, avatar_decoration: str) -> animated=animated, ) + @classmethod + def _from_user_primary_guild_tag( + cls, state: ConnectionState, identity_guild_id: int, badge_id: str + ) -> Asset: + """Creates an Asset for a user's primary guild (tag) badge. + + Parameters + ---------- + state: ConnectionState + The connection state. + identity_guild_id: int + The ID of the guild. + badge_id: str + The badge hash/id. + + Returns + ------- + :class:`Asset` + The primary guild badge asset. + """ + return cls( + state, + url=f"{Asset.BASE}/guild-tag-badges/{identity_guild_id}/{badge_id}.png?size=256", + key=badge_id, + animated=False, + ) + @classmethod def _from_guild_avatar(cls, state, guild_id: int, member_id: int, avatar: str) -> Asset: animated = avatar.startswith("a_") diff --git a/discord/primary_guild.py b/discord/primary_guild.py new file mode 100644 index 0000000000..13e6ae035f --- /dev/null +++ b/discord/primary_guild.py @@ -0,0 +1,74 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .state import ConnectionState + +from .asset import Asset +from .types.primary_guild import PrimaryGuild as PrimaryGuildPayload + +__all__ = ("PrimaryGuild",) + + +class PrimaryGuild: + """ + Represents a Discord Primary Guild. + + .. versionadded:: 2.7 + + Attributes + ---------- + identity_guild_id: int + The ID of the guild. + identity_enabled: :class:`bool` + Whether the primary guild is enabled. + tag: str + The tag of the primary guild. + """ + + def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: + self.identity_guild_id: int | None = ( + int(data.get("identity_guild_id") or 0) or None + ) + self.identity_enabled: bool | None = data.get("identity_enabled", None) + self.tag: str | None = data.get("tag", None) + self._badge: str | None = data.get("badge", None) + self._state: "ConnectionState" = state + + def __repr__(self) -> str: + return f"" + + @property + def badge(self) -> Asset | None: + """Returns the badge asset, if available. + + .. versionadded:: 2.7 + """ + if self._badge is None: + return None + return Asset._from_user_primary_guild_tag( + self._state, self.identity_guild_id, self._badge + ) diff --git a/discord/types/primary_guild.py b/discord/types/primary_guild.py new file mode 100644 index 0000000000..5ee6386cc3 --- /dev/null +++ b/discord/types/primary_guild.py @@ -0,0 +1,36 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import TypedDict + +from .snowflake import Snowflake + + +class PrimaryGuild(TypedDict): + identity_guild_id: Snowflake + identity_enabled: bool | None + tag: str + badge: str diff --git a/discord/user.py b/discord/user.py index 68209d33df..4ba25810ca 100644 --- a/discord/user.py +++ b/discord/user.py @@ -35,6 +35,7 @@ from .flags import PublicUserFlags from .iterators import EntitlementIterator from .monetization import Entitlement +from .primary_guild import PrimaryGuild from .utils import MISSING, Undefined, snowflake_time from .utils.private import bytes_to_base64_data @@ -79,6 +80,7 @@ class BaseUser(_UserTag): "_avatar_decoration", "_state", "nameplate", + "primary_guild", ) if TYPE_CHECKING: @@ -95,6 +97,7 @@ class BaseUser(_UserTag): _avatar_decoration: dict | None _public_flags: int nameplate: Nameplate | None + primary_guild: PrimaryGuild | None def __init__(self, *, state: ConnectionState, data: UserPayload | PartialUserPayload) -> None: self._state = state @@ -142,6 +145,13 @@ def _update(self, data: UserPayload) -> None: self.nameplate = Nameplate(data=nameplate, state=self._state) else: self.nameplate = None + primary_guild_payload = data.get("primary_guild", None) + if primary_guild_payload and primary_guild_payload.get("identity_enabled"): + self.primary_guild = PrimaryGuild( + data=primary_guild_payload, state=self._state + ) + else: + self.primary_guild = None self._public_flags = data.get("public_flags", 0) self.bot = data.get("bot", False) self.system = data.get("system", False) @@ -161,6 +171,7 @@ def _copy(cls: type[BU], user: BU) -> BU: self.bot = user.bot self._state = user._state self._public_flags = user._public_flags + self.primary_guild = user.primary_guild return self From 921260d859349350822d059d5efa4e48f06c6cbc Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 28 Aug 2025 11:58:10 +0200 Subject: [PATCH 04/35] docs: :memo: Fix docstring of `Nameplate` (#2884) :memo: Fix docstring of `Nameplate` (cherry picked from commit a383590f7256d0d920ece1b339ca465e85a72a54) --- discord/collectibles.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/collectibles.py b/discord/collectibles.py index 5c5ccc291c..ff7f62c4fe 100644 --- a/discord/collectibles.py +++ b/discord/collectibles.py @@ -39,10 +39,10 @@ class Nameplate: Attributes ---------- - sku_id: int - The SKU ID of the nameplate. - palette: str - The color palette of the nameplate. + sku_id: :class:`int` + The SKU ID of the nameplate. + palette: :class:`str` + The color palette of the nameplate. """ def __init__(self, data: NameplatePayload, state: "ConnectionState") -> None: From 5f5b0fd8455e438568fe0ab83889ecd0d48ae5c3 Mon Sep 17 00:00:00 2001 From: UK <41271523+neloblivion@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:00:24 +0200 Subject: [PATCH 05/35] feat: Message pin updates (#2872) Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> Co-authored-by: plun1331 (cherry picked from commit e038fc2fc36a66d0cd6b15389ce1c36f95cc1833) Signed-off-by: Paillat-dev --- CHANGELOG.md | 5 +++ discord/abc.py | 68 ++++++++++++++++++++++-------- discord/http.py | 23 +++++++++-- discord/iterators.py | 89 +++++++++++++++++++++++++++++++++++++++- discord/message.py | 37 ++++++++++++++++- discord/permissions.py | 17 ++++++-- discord/types/message.py | 10 +++++ 7 files changed, 220 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 494e99a514..a147d812c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2797](https://github.com/Pycord-Development/pycord/pull/2797)) - Upgraded voice websocket version to v8. ([#2812](https://github.com/Pycord-Development/pycord/pull/2812)) +- `Messageable.pins()` now returns a `MessagePinIterator` and has new arguments. + ([#2872](https://github.com/Pycord-Development/pycord/pull/2872)) ### Deprecated @@ -176,6 +178,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) - Deprecated `Interaction.cached_channel` in favor of `Interaction.channel`. ([#2658](https://github.com/Pycord-Development/pycord/pull/2658)) +- Deprecated `Messageable.pins()` returning a list of `Message`; it should be used as an + iterator of `MessagePin` instead. + ([#2872](https://github.com/Pycord-Development/pycord/pull/2872)) ### Removed diff --git a/discord/abc.py b/discord/abc.py index 3c1a1dfeb4..3a40dbf4ea 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -48,7 +48,7 @@ from .file import File, VoiceMessage from .flags import ChannelFlags, MessageFlags from .invite import Invite -from .iterators import HistoryIterator +from .iterators import HistoryIterator, MessagePinIterator from .mentions import AllowedMentions from .partial_emoji import PartialEmoji, _EmojiTag from .permissions import PermissionOverwrite, Permissions @@ -1679,32 +1679,64 @@ async def fetch_message(self, id: int, /) -> Message: data = await self._state.http.get_message(channel.id, id) return self._state.create_message(channel=channel, data=data) - async def pins(self) -> list[Message]: - """|coro| + def pins( + self, + *, + limit: int | None = 50, + before: SnowflakeTime | None = None, + ) -> MessagePinIterator: + """Returns a :class:`~discord.MessagePinIterator` that enables receiving the destination's pinned messages. - Retrieves all messages that are currently pinned in the channel. + You must have :attr:`~discord.Permissions.read_message_history` permissions to use this. - .. note:: + .. warning:: - Due to a limitation with the Discord API, the :class:`.Message` - objects returned by this method do not contain complete - :attr:`.Message.reactions` data. + Starting from version 3.0, `await channel.pins()` will no longer return a list of :class:`Message`. See examples below for new usage instead. - Returns - ------- - List[:class:`~discord.Message`] - The messages that are currently pinned. + Parameters + ---------- + limit: Optional[:class:`int`] + The number of pinned messages to retrieve. + If ``None``, retrieves every pinned message in the channel. + before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages pinned before this datetime. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + + Yields + ------ + :class:`~discord.MessagePin` + The pinned message. Raises ------ + ~discord.Forbidden + You do not have permissions to get pinned messages. ~discord.HTTPException - Retrieving the pinned messages failed. - """ + The request to get pinned messages failed. - channel = await self._get_channel() - state = self._state - data = await state.http.pins_from(channel.id) - return [state.create_message(channel=channel, data=m) for m in data] + Examples + -------- + + Usage :: + + counter = 0 + async for pin in channel.pins(limit=250): + if pin.message.author == client.user: + counter += 1 + + Flattening into a list: :: + + pins = await channel.pins(limit=None).flatten() + # pins is now a list of MessagePin... + + All parameters are optional. + """ + return MessagePinIterator( + self, + limit=limit, + before=before, + ) def can_send(self, *objects) -> bool: """Returns a :class:`bool` indicating whether you have the permissions to send the object(s). diff --git a/discord/http.py b/discord/http.py index 22cddfe684..23d7e77e88 100644 --- a/discord/http.py +++ b/discord/http.py @@ -848,7 +848,7 @@ def publish_message(self, channel_id: Snowflake, message_id: Snowflake) -> Respo def pin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: str | None = None) -> Response[None]: r = Route( "PUT", - "/channels/{channel_id}/pins/{message_id}", + "/channels/{channel_id}/messages/pins/{message_id}", channel_id=channel_id, message_id=message_id, ) @@ -857,13 +857,30 @@ def pin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: str def unpin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: str | None = None) -> Response[None]: r = Route( "DELETE", - "/channels/{channel_id}/pins/{message_id}", + "/channels/{channel_id}/messages/pins/{message_id}", channel_id=channel_id, message_id=message_id, ) return self.request(r, reason=reason) - def pins_from(self, channel_id: Snowflake) -> Response[list[message.Message]]: + def pins_from( + self, + channel_id: Snowflake, + limit: int | None = None, + before: str | None = None, + ) -> Response[message.MessagePinPagination]: + r = Route("GET", "/channels/{channel_id}/messages/pins", channel_id=channel_id) + params = {} + if limit: + params["limit"] = limit + if before: + params["before"] = before + + return self.request(r, params=params) + + def legacy_pins_from( + self, channel_id: Snowflake + ) -> Response[list[message.Message]]: return self.request(Route("GET", "/channels/{channel_id}/pins", channel_id=channel_id)) # Member management diff --git a/discord/iterators.py b/discord/iterators.py index 0185cb683f..069fc7598c 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -33,6 +33,7 @@ AsyncIterator, Awaitable, Callable, + Generator, List, TypeVar, Union, @@ -42,7 +43,7 @@ from .errors import NoMoreItems from .object import Object from .utils import generate_snowflake, snowflake_time -from .utils.private import maybe_awaitable +from .utils.private import maybe_awaitable, warn_deprecated __all__ = ( "ReactionIterator", @@ -57,15 +58,17 @@ if TYPE_CHECKING: from .abc import Snowflake + from .channel import MessageableChannel from .guild import BanEntry, Guild from .member import Member - from .message import Message + from .message import Message, MessagePin from .monetization import Entitlement, Subscription from .scheduled_events import ScheduledEvent from .threads import Thread from .types.audit_log import AuditLog as AuditLogPayload from .types.guild import Guild as GuildPayload from .types.message import Message as MessagePayload + from .types.message import MessagePin as MessagePinPayload from .types.monetization import Entitlement as EntitlementPayload from .types.monetization import Subscription as SubscriptionPayload from .types.threads import Thread as ThreadPayload @@ -1157,3 +1160,85 @@ async def _retrieve_subscriptions_after_strategy(self, retrieve): self.limit -= retrieve self.after = Object(id=int(data[0]["id"])) return data + + +class MessagePinIterator(_AsyncIterator["MessagePin"]): + def __init__( + self, + channel: MessageableChannel, + limit: int | None, + before: Snowflake | datetime.datetime | None = None, + ): + self._channel = channel + self.limit = limit + self.http = channel._state.http + + self.before: str | None + if before is None: + self.before = None + elif isinstance(before, datetime.datetime): + self.before = before.isoformat() + else: + self.before = snowflake_time(before.id).isoformat() + + self.update_before: Callable[[MessagePinPayload], str] = self.get_last_pinned + + self.endpoint = self.http.pins_from + + self.queue: asyncio.Queue[MessagePin] = asyncio.Queue() + self.has_more: bool = True + + async def next(self) -> MessagePin: + if self.queue.empty(): + await self.fill_queue() + + try: + return self.queue.get_nowait() + except asyncio.QueueEmpty: + raise NoMoreItems() + + @staticmethod + def get_last_pinned(data: MessagePinPayload) -> str: + return data["pinned_at"] + + async def fill_queue(self) -> None: + if not self.has_more: + raise NoMoreItems() + + if not hasattr(self, "channel"): + channel = await self._channel._get_channel() + self.channel = channel + + limit = 50 if self.limit is None else min(self.limit, 50) + data = await self.endpoint(self.channel.id, before=self.before, limit=limit) + + pins: list[MessagePinPayload] = data.get("items", []) + for d in pins: + self.queue.put_nowait(self.create_pin(d)) + + self.has_more = data.get("has_more", False) + if self.limit is not None: + self.limit -= len(pins) + if self.limit <= 0: + self.has_more = False + + if self.has_more: + self.before = self.update_before(pins[-1]) + + def create_pin(self, data: MessagePinPayload) -> MessagePin: + from .message import MessagePin + + return MessagePin(state=self.channel._state, channel=self.channel, data=data) + + async def retrieve_inner(self) -> list[Message]: + pins = await self.flatten() + return [p.message for p in pins] + + def __await__(self) -> Generator[Any, Any, MessagePin]: + warn_deprecated( + f"Messageable.pins() returning a list of Message", + since="2.7", + removed="3.0", + reference="The documentation of pins()", + ) + return self.retrieve_inner().__await__() diff --git a/discord/message.py b/discord/message.py index 4a148e562f..cf9899e767 100644 --- a/discord/message.py +++ b/discord/message.py @@ -85,6 +85,7 @@ from .types.message import MessageActivity as MessageActivityPayload from .types.message import MessageApplication as MessageApplicationPayload from .types.message import MessageCall as MessageCallPayload + from .types.message import MessagePin as MessagePinPayload from .types.message import MessageReference as MessageReferencePayload from .types.message import MessageSnapshot as MessageSnapshotPayload from .types.message import Reaction as ReactionPayload @@ -769,6 +770,38 @@ def flatten_handlers(cls): return cls +class MessagePin: + """Represents information about a pinned message. + + .. versionadded:: 2.7 + """ + + def __init__( + self, + state: ConnectionState, + channel: MessageableChannel, + data: MessagePinPayload, + ): + self._state: ConnectionState = state + self._pinned_at: datetime.datetime = utils.parse_time(data["pinned_at"]) + self._message: Message = state.create_message( + channel=channel, data=data["message"] + ) + + @property + def message(self) -> Message: + """The pinned message.""" + return self._message + + @property + def pinned_at(self) -> datetime.datetime: + """An aware timestamp of when the message was pinned.""" + return self._pinned_at + + def __repr__(self) -> str: + return f"" + + @flatten_handlers class Message(Hashable): r"""Represents a message from Discord. @@ -1743,7 +1776,7 @@ async def pin(self, *, reason: str | None = None) -> None: Pins the message. - You must have the :attr:`~Permissions.manage_messages` permission to do + You must have the :attr:`~Permissions.pin_messages` permission to do this in a non-private channel context. Parameters @@ -1772,7 +1805,7 @@ async def unpin(self, *, reason: str | None = None) -> None: Unpins the message. - You must have the :attr:`~Permissions.manage_messages` permission to do + You must have the :attr:`~Permissions.pin_messages` permission to do this in a non-private channel context. Parameters diff --git a/discord/permissions.py b/discord/permissions.py index cfd4677375..142e94cc22 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -171,7 +171,7 @@ def all(cls: type[P]) -> P: """A factory method that creates a :class:`Permissions` with all permissions set to ``True``. """ - return cls(0b1111111111111111111111111111111111111111111111111) + return cls(~(~1 << 51)) @classmethod def all_channel(cls: type[P]) -> P: @@ -385,11 +385,11 @@ def send_tts_messages(self) -> int: @flag_value def manage_messages(self) -> int: - """:class:`bool`: Returns ``True`` if a user can delete or pin messages in a text channel. + """:class:`bool`: Returns ``True`` if a user can delete messages in a text channel. - .. note:: + .. warning:: - Note that there are currently no ways to edit other people's messages. + Starting from January 12th 2026, this will no longer grant the ability to pin/unpin messages. Use :attr:`pin_messages` instead. """ return 1 << 13 @@ -663,6 +663,14 @@ def use_external_apps(self) -> int: """ return 1 << 50 + @flag_value + def pin_messages(self) -> int: + """:class:`bool`: Returns ``True`` if a member can pin/unpin messages. + + .. versionadded:: 2.7 + """ + return 1 << 51 + PO = TypeVar("PO", bound="PermissionOverwrite") @@ -786,6 +794,7 @@ class PermissionOverwrite: set_voice_channel_status: bool | None send_polls: bool | None use_external_apps: bool | None + pin_messages: bool | None def __init__(self, **kwargs: bool | None): self._values: dict[str, bool | None] = {} diff --git a/discord/types/message.py b/discord/types/message.py index cbed1629b1..82488745d2 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -173,6 +173,16 @@ class Message(TypedDict): message_snapshots: NotRequired[list[MessageSnapshot]] +class MessagePin(TypedDict): + pinned_at: str + message: Message + + +class MessagePinPagination(TypedDict): + items: list[MessagePin] + has_more: bool + + AllowedMentionType = Literal["roles", "users", "everyone"] From dd6688db3930e21ccd52d96d0d7063a2eab4e7fc Mon Sep 17 00:00:00 2001 From: JL710 Date: Sat, 30 Aug 2025 15:43:10 +0200 Subject: [PATCH 06/35] fix: fix typing for PermissionOverwrite.update (#2878) * fix: fix typing for PermissionOverwrite.update * Update CHANGELOG.md Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lala Sabathil --------- Signed-off-by: Paillat Signed-off-by: Lala Sabathil Co-authored-by: Paillat Co-authored-by: Lala Sabathil Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> (cherry picked from commit 890153a15959655251dd88dd590ec7d4869a3ad0) --- CHANGELOG.md | 2 ++ discord/permissions.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a147d812c5..ca4561986c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,6 +144,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2843](https://github.com/Pycord-Development/pycord/pull/2843)) - Fixed `TypeError` when using `@option` with certain annotations and along with `channel_types`. ([#2835](https://github.com/Pycord-Development/pycord/pull/2835)) +- Fixed type-hinting for `PermissionOverwrite.update`. + ([#2878](https://github.com/Pycord-Development/pycord/pull/2878)) - Fixed `AttributeError` when accessing `AuditLogEntry.changes` more than once. ([#2882])(https://github.com/Pycord-Development/pycord/pull/2882)) diff --git a/discord/permissions.py b/discord/permissions.py index 142e94cc22..2449737920 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -858,7 +858,7 @@ def is_empty(self) -> bool: """ return len(self._values) == 0 - def update(self, **kwargs: bool) -> None: + def update(self, **kwargs: bool | None) -> None: r"""Bulk updates this permission overwrite object. Allows you to set multiple attributes by using keyword From 90ba570a518661b1eaed5382e3ba2a5f1851f702 Mon Sep 17 00:00:00 2001 From: BOXER <130167557+boxerrmd@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:58:10 +0200 Subject: [PATCH 07/35] feat: add ThreadAutoArchiveDuration enum (#2826) Signed-off-by: BOXER <130167557+BOXERRMD@users.noreply.github.com> Signed-off-by: Lala Sabathil Signed-off-by: Lala Sabathil Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Paillat Co-authored-by: Lala Sabathil Co-authored-by: plun1331 Co-authored-by: Lala Sabathil Co-authored-by: Lumouille <144063653+Lumabots@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> (cherry picked from commit 54884c3a2c7d0361df6735c1449e859d7fcc0428) Signed-off-by: Paillat-dev --- CHANGELOG.md | 2 ++ discord/channel.py | 8 +++++++- discord/enums.py | 11 +++++++++++ discord/threads.py | 11 +++++++++-- docs/api/enums.rst | 24 ++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4561986c..d0b4a11b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2817](https://github.com/Pycord-Development/pycord/pull/2817)) - Added role gradients support with `Role.colours` and the `RoleColours` class. ([#2818](https://github.com/Pycord-Development/pycord/pull/2818)) +- Added `ThreadArchiveDuration` enum to improve clarity of thread archive durations. + ([#2826](https://github.com/Pycord-Development/pycord/pull/2826)) - Added `Interaction.attachment_size_limit`. ([#2854](https://github.com/Pycord-Development/pycord/pull/2854)) - Added `AuditLogDiff.communication_disabled_until`. diff --git a/discord/channel.py b/discord/channel.py index f30ff8ed39..8d654be8a0 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -48,6 +48,9 @@ InviteTarget, SortOrder, StagePrivacyLevel, +) +from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum +from .enums import ( VideoQualityMode, VoiceRegion, try_enum, @@ -1052,7 +1055,9 @@ async def edit( sync_permissions: bool = ..., category: CategoryChannel | None = ..., slowmode_delay: int = ..., - default_auto_archive_duration: ThreadArchiveDuration = ..., + default_auto_archive_duration: ( + ThreadArchiveDuration | ThreadArchiveDurationEnum + ) = ..., default_thread_slowmode_delay: int = ..., default_sort_order: SortOrder = ..., default_reaction_emoji: GuildEmoji | int | str | None = ..., @@ -1098,6 +1103,7 @@ async def edit(self, *, reason=None, **options): default_auto_archive_duration: :class:`int` The new default auto archive duration in minutes for threads created in this channel. Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + :class:`ThreadArchiveDuration` can be used alternatively. default_thread_slowmode_delay: :class:`int` The new default slowmode delay in seconds for threads created in this channel. diff --git a/discord/enums.py b/discord/enums.py index 75b73fb14f..dbed0cf59e 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -27,6 +27,7 @@ import types from enum import Enum as EnumBase +from enum import IntEnum from typing import Any, Self, TypeVar, Union E = TypeVar("E", bound="Enum") @@ -80,6 +81,7 @@ "InteractionContextType", "PollLayoutType", "MessageReferenceType", + "ThreadArchiveDuration", "SubscriptionStatus", "SeparatorSpacingSize", ) @@ -1009,6 +1011,15 @@ class SubscriptionStatus(Enum): inactive = 2 +class ThreadArchiveDuration(IntEnum): + """The time set until a thread is automatically archived.""" + + one_hour = 60 + one_day = 1440 + three_days = 4320 + one_week = 10080 + + class SeparatorSpacingSize(Enum): """A separator component's spacing size.""" diff --git a/discord/threads.py b/discord/threads.py index a8ad1100ec..7b22fa2743 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -30,7 +30,13 @@ from discord import utils from .abc import Messageable, _purge_messages_helper -from .enums import ChannelType, try_enum +from .enums import ( + ChannelType, +) +from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum +from .enums import ( + try_enum, +) from .errors import ClientException from .flags import ChannelFlags from .mixins import Hashable @@ -575,7 +581,7 @@ async def edit( locked: bool | utils.Undefined = MISSING, invitable: bool | utils.Undefined = MISSING, slowmode_delay: int | utils.Undefined = MISSING, - auto_archive_duration: ThreadArchiveDuration | utils.Undefined = MISSING, + auto_archive_duration: ThreadArchiveDuration | ThreadArchiveDurationEnum | utils.Undefined = MISSING, pinned: bool | utils.Undefined = MISSING, applied_tags: list[ForumTag] | utils.Undefined = MISSING, reason: str | None = None, @@ -605,6 +611,7 @@ async def edit( auto_archive_duration: :class:`int` The new duration in minutes before a thread is automatically archived for inactivity. Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + :class:`ThreadArchiveDuration` can be used alternatively. slowmode_delay: :class:`int` Specifies the slowmode rate limit for user in this thread, in seconds. A value of ``0`` disables slowmode. The maximum value possible is ``21600``. diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 1210990717..34ac6ed5b9 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2521,6 +2521,30 @@ of :class:`enum.Enum`. The subscription is inactive and the subscription owner is not being charged. + +.. class:: ThreadArchiveDuration + + Represents the time before a thread is archived. + + .. versionadded:: 2.7 + + .. attribute:: one_hour + + Indicates that the thread will be archived after 1 hour of inactivity. + + .. attribute:: one_day + + Indicates that the thread will be archived after 1 day of inactivity. + + .. attribute:: three_days + + Indicates that the thread will be archived after 3 days of inactivity. + + .. attribute:: one_week + + Indicates that the thread will be archived after 1 week of inactivity. + + .. class:: SeparatorSpacingSize Represents the padding size around a separator component. From b0e0840f5ee6ff7e4553832f6232540cfa5eaaa1 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+lumabots@users.noreply.github.com> Date: Sat, 30 Aug 2025 16:04:00 +0200 Subject: [PATCH 08/35] feat: adding missing parameters and attribute documentation (#2772) * Update guild.py Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * Update channel.py add missings docs Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * Update guild.py Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * Update channel.py Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * Update guild.py Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * Update guild.py Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * Update guild.py Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * Update channel.py removal of all the nsfw mention which should not exist for category Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * Update channel.py Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * fix typo Co-authored-by: plun1331 Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * Update discord/channel.py Co-authored-by: plun1331 Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * Update discord/channel.py Co-authored-by: plun1331 Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * Update discord/channel.py Co-authored-by: plun1331 Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * feat(channel): deprecate is_nsfw method in CategoryChannel * fix(channel): correct deprecated decorator arguments in CategoryChannel * style(pre-commit): auto fixes from pre-commit.com hooks * fix: improve clarity of NSFW parameter documentation and slowmode for continiuty * fix: improve clarity of NSFW parameter documentation * fix: add missing parameters to channel creation methods and deprecate unsupported nsfw for categories * Update CHANGELOG.md Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * fix: add missing parameters for channel creation and allow default_reaction_emoji to be None * Update CHANGELOG.md Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * added reference * deprecated nsfw using property * Update channel.py * Update channel.py * Update discord/guild.py Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * Update discord/guild.py Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * Update discord/guild.py Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> * Update discord/channel.py Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --------- Signed-off-by: Lumabots <144063653+Lumabots@users.noreply.github.com> Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> (cherry picked from commit 9fd9b3e1edfbda437d63d1c21f5d11c0a6900eb0) Signed-off-by: Paillat-dev --- CHANGELOG.md | 4 ++ discord/channel.py | 81 ++++++++++++++++++------ discord/guild.py | 149 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 210 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b4a11b4d..63355e05e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,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 various missing channel parameters and allow `default_reaction_emoji` to be + `None`. ([#2772](https://github.com/Pycord-Development/pycord/pull/2772)) - Added support for type hinting slash command options with `typing.Annotated`. ([#2782](https://github.com/Pycord-Development/pycord/pull/2782)) - Added conversion to `Member` in `MentionableConverter`. @@ -182,6 +184,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) - Deprecated `Interaction.cached_channel` in favor of `Interaction.channel`. ([#2658](https://github.com/Pycord-Development/pycord/pull/2658)) +- Deprecated `is_nsfw` for categories since it was never supported by the API. + ([#2772](https://github.com/Pycord-Development/pycord/pull/2772)) - Deprecated `Messageable.pins()` returning a list of `Message`; it should be used as an iterator of `MessagePin` instead. ([#2872](https://github.com/Pycord-Development/pycord/pull/2872)) diff --git a/discord/channel.py b/discord/channel.py index 8d654be8a0..d96a3c72d4 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -225,7 +225,7 @@ def __init__( @property def _repr_attrs(self) -> tuple[str, ...]: - return "id", "name", "position", "nsfw", "category_id" + return "id", "name", "position", "category_id" def __repr__(self) -> str: attrs = [(val, getattr(self, val)) for val in self._repr_attrs] @@ -790,7 +790,7 @@ async def edit(self, *, reason=None, **options): position: :class:`int` The new channel's position. nsfw: :class:`bool` - To mark the channel as NSFW or not. + Whether the channel is marked as NSFW. sync_permissions: :class:`bool` Whether to sync permissions with the channel's new or pre-existing category. Defaults to ``False``. @@ -1012,6 +1012,8 @@ def _update(self, guild: Guild, data: ForumChannelPayload) -> None: if self.default_sort_order is not None: self.default_sort_order = try_enum(SortOrder, self.default_sort_order) + self.default_reaction_emoji = None + reaction_emoji_ctx: dict = data.get("default_reaction_emoji") if reaction_emoji_ctx is not None: emoji_name = reaction_emoji_ctx.get("emoji_name") @@ -1086,7 +1088,7 @@ async def edit(self, *, reason=None, **options): position: :class:`int` The new channel's position. nsfw: :class:`bool` - To mark the channel as NSFW or not. + Whether the channel is marked as NSFW. sync_permissions: :class:`bool` Whether to sync permissions with the channel's new or pre-existing category. Defaults to ``False``. @@ -1451,7 +1453,7 @@ async def edit(self, *, reason=None, **options): position: :class:`int` The new channel's position. nsfw: :class:`bool` - To mark the channel as NSFW or not. + Whether the channel is marked as NSFW. sync_permissions: :class:`bool` Whether to sync permissions with the channel's new or pre-existing category. Defaults to ``False``. @@ -1691,6 +1693,11 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): Extra features of the channel. .. versionadded:: 2.0 + + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + + .. versionadded:: 2.7 """ def __init__( @@ -1998,6 +2005,7 @@ async def edit( rtc_region: VoiceRegion | None = ..., video_quality_mode: VideoQualityMode = ..., slowmode_delay: int = ..., + nsfw: bool = ..., reason: str | None = ..., ) -> VoiceChannel | None: ... @@ -2048,6 +2056,15 @@ async def edit(self, *, reason=None, **options): .. versionadded:: 2.0 + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + + .. versionadded:: 2.7 + Returns ------- Optional[:class:`.VoiceChannel`] @@ -2202,6 +2219,15 @@ class StageChannel(discord.abc.Messageable, VocalGuildChannel): last_message_id: Optional[:class:`int`] The ID of the last message sent to this channel. It may not always point to an existing or valid message. .. versionadded:: 2.5 + + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + The maximum value possible is `21600`. + + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + + .. versionadded:: 2.7 """ __slots__ = ("topic",) @@ -2661,6 +2687,16 @@ async def edit(self, *, reason=None, **options): .. versionadded:: 2.0 + bitrate: :class:`int` + The channel's preferred audio bitrate in bits per second. + + user_limit: :class:`int` + The channel's limit for number of members that can be in a voice channel. + + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + Returns ------- Optional[:class:`.StageChannel`] @@ -2717,12 +2753,9 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): position: Optional[:class:`int`] The position in the category list. This is a number that starts at 0. e.g. the top category is position 0. Can be ``None`` if the channel was received in an interaction. - nsfw: :class:`bool` - If the channel is marked as "not safe for work". .. note:: - To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. flags: :class:`ChannelFlags` Extra features of the channel. @@ -2733,7 +2766,6 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): "name", "id", "guild", - "nsfw", "_state", "position", "_overwrites", @@ -2747,7 +2779,9 @@ def __init__(self, *, state: ConnectionState, guild: Guild, data: CategoryChanne self._update(guild, data) def __repr__(self) -> str: - return f"" + return ( + f" None: # This data will always exist @@ -2757,7 +2791,6 @@ def _update(self, guild: Guild, data: CategoryChannelPayload) -> None: # This data may be missing depending on how this object is being created/updated if not data.pop("_invoke_flag", False): - self.nsfw: bool = data.get("nsfw", False) self.position: int = data.get("position") self.flags: ChannelFlags = ChannelFlags._from_value(data.get("flags", 0)) self._fill_overwrites(data) @@ -2771,13 +2804,9 @@ def type(self) -> ChannelType: """The channel's Discord type.""" return ChannelType.category - def is_nsfw(self) -> bool: - """Checks if the category is NSFW.""" - return self.nsfw - @copy_doc(discord.abc.GuildChannel.clone) async def clone(self, *, name: str | None = None, reason: str | None = None) -> CategoryChannel: - return await self._clone_impl({"nsfw": self.nsfw}, name=name, reason=reason) + return await self._clone_impl({}, name=name, reason=reason) @overload async def edit( @@ -2785,7 +2814,6 @@ async def edit( *, name: str = ..., position: int = ..., - nsfw: bool = ..., overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., reason: str | None = ..., ) -> CategoryChannel | None: ... @@ -2813,8 +2841,6 @@ async def edit(self, *, reason=None, **options): The new category's name. position: :class:`int` The new category's position. - nsfw: :class:`bool` - To mark the category as NSFW or not. reason: Optional[:class:`str`] The reason for editing this category. Shows up on the audit log. overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] @@ -2946,6 +2972,25 @@ async def create_forum_channel(self, name: str, **options: Any) -> ForumChannel: """ return await self.guild.create_forum_channel(name, category=self, **options) + @utils.deprecated( + since="2.7", + removed="3.0", + reference="NSFW categories are not available in the Discord API.", + ) + def is_nsfw(self) -> bool: + return False + + # TODO: Remove in 3.0 + + @property + @utils.deprecated( + since="2.7", + removed="3.0", + reference="NSFW categories are not available in the Discord API.", + ) + def nsfw(self) -> bool: + return False + DMC = TypeVar("DMC", bound="DMChannel") diff --git a/discord/guild.py b/discord/guild.py index 56250ecbc1..a0f727efcf 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1078,6 +1078,8 @@ async def create_text_channel( slowmode_delay: int | utils.Undefined = MISSING, nsfw: bool | utils.Undefined = MISSING, overwrites: dict[Role | Member, PermissionOverwrite] | utils.Undefined = MISSING, + default_thread_slowmode_delay: int | None = MISSING, + default_auto_archive_duration: int = MISSING, ) -> TextChannel: """|coro| @@ -1114,12 +1116,22 @@ async def create_text_channel( The new channel's topic. slowmode_delay: :class:`int` Specifies the slowmode rate limit for user in this channel, in seconds. - The maximum value possible is `21600`. + A value of `0` disables slowmode. The maximum value possible is `21600`. nsfw: :class:`bool` - To mark the channel as NSFW or not. + Whether the channel is marked as NSFW. reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. + default_thread_slowmode_delay: Optional[:class:`int`] + The initial slowmode delay to set on newly created threads in this channel. + + .. versionadded:: 2.7 + + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + + .. versionadded:: 2.7 + Returns ------- :class:`TextChannel` @@ -1168,6 +1180,12 @@ async def create_text_channel( if nsfw is not MISSING: options["nsfw"] = nsfw + if default_thread_slowmode_delay is not MISSING: + options["default_thread_slowmode_delay"] = default_thread_slowmode_delay + + if default_auto_archive_duration is not MISSING: + options["default_auto_archive_duration"] = default_auto_archive_duration + data = await self._create_channel( name, overwrites=overwrites, @@ -1194,6 +1212,8 @@ async def create_voice_channel( rtc_region: VoiceRegion | None | utils.Undefined = MISSING, video_quality_mode: VideoQualityMode | utils.Undefined = MISSING, overwrites: dict[Role | Member, PermissionOverwrite] | utils.Undefined = MISSING, + slowmode_delay: int = MISSING, + nsfw: bool = MISSING, ) -> VoiceChannel: """|coro| @@ -1228,6 +1248,17 @@ async def create_voice_channel( reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + + .. versionadded:: 2.7 + + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + + .. versionadded:: 2.7 + Returns ------- :class:`VoiceChannel` @@ -1258,6 +1289,12 @@ async def create_voice_channel( if video_quality_mode is not MISSING: options["video_quality_mode"] = video_quality_mode.value + if slowmode_delay is not MISSING: + options["rate_limit_per_user"] = slowmode_delay + + if nsfw is not MISSING: + options["nsfw"] = nsfw + data = await self._create_channel( name, overwrites=overwrites, @@ -1281,6 +1318,12 @@ async def create_stage_channel( overwrites: dict[Role | Member, PermissionOverwrite] | utils.Undefined = MISSING, category: CategoryChannel | None = None, reason: str | None = None, + bitrate: int = MISSING, + user_limit: int = MISSING, + rtc_region: VoiceRegion | None = MISSING, + video_quality_mode: VideoQualityMode = MISSING, + slowmode_delay: int = MISSING, + nsfw: bool = MISSING, ) -> StageChannel: """|coro| @@ -1306,6 +1349,38 @@ async def create_stage_channel( reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. + bitrate: :class:`int` + The channel's preferred audio bitrate in bits per second. + + .. versionadded:: 2.7 + + user_limit: :class:`int` + The channel's limit for number of members that can be in a voice channel. + + .. versionadded:: 2.7 + + rtc_region: Optional[:class:`VoiceRegion`] + The region for the voice channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + + .. versionadded:: 2.7 + + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the voice channel's participants. + + .. versionadded:: 2.7 + + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + + .. versionadded:: 2.7 + + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + + .. versionadded:: 2.7 + Returns ------- :class:`StageChannel` @@ -1327,6 +1402,24 @@ async def create_stage_channel( if position is not MISSING: options["position"] = position + if bitrate is not MISSING: + options["bitrate"] = bitrate + + if user_limit is not MISSING: + options["user_limit"] = user_limit + + if rtc_region is not MISSING: + options["rtc_region"] = None if rtc_region is None else str(rtc_region) + + if video_quality_mode is not MISSING: + options["video_quality_mode"] = video_quality_mode.value + + if slowmode_delay is not MISSING: + options["rate_limit_per_user"] = slowmode_delay + + if nsfw is not MISSING: + options["nsfw"] = nsfw + data = await self._create_channel( name, overwrites=overwrites, @@ -1353,6 +1446,10 @@ async def create_forum_channel( nsfw: bool | utils.Undefined = MISSING, overwrites: dict[Role | Member, PermissionOverwrite] | utils.Undefined = MISSING, default_reaction_emoji: GuildEmoji | int | str | utils.Undefined = MISSING, + available_tags: list[ForumTag] = MISSING, + default_sort_order: SortOrder | None = MISSING, + default_thread_slowmode_delay: int | None = MISSING, + default_auto_archive_duration: int = MISSING, ) -> ForumChannel: """|coro| @@ -1389,9 +1486,9 @@ async def create_forum_channel( The new channel's topic. slowmode_delay: :class:`int` Specifies the slowmode rate limit for user in this channel, in seconds. - The maximum value possible is `21600`. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. nsfw: :class:`bool` - To mark the channel as NSFW or not. + Whether the channel is marked as NSFW. reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. default_reaction_emoji: Optional[:class:`GuildEmoji` | :class:`int` | :class:`str`] @@ -1401,6 +1498,26 @@ async def create_forum_channel( .. versionadded:: v2.5 + available_tags: List[:class:`ForumTag`] + The set of tags that can be used in a forum channel. + + .. versionadded:: 2.7 + + default_sort_order: Optional[:class:`SortOrder`] + The default sort order type used to order posts in this channel. + + .. versionadded:: 2.7 + + default_thread_slowmode_delay: Optional[:class:`int`] + The initial slowmode delay to set on newly created threads in this channel. + + .. versionadded:: 2.7 + + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + + .. versionadded:: 2.7 + Returns ------- :class:`ForumChannel` @@ -1449,6 +1566,20 @@ async def create_forum_channel( if nsfw is not MISSING: options["nsfw"] = nsfw + if available_tags is not MISSING: + options["available_tags"] = [tag.to_dict() for tag in available_tags] + + if default_sort_order is not MISSING: + options["default_sort_order"] = ( + default_sort_order.value if default_sort_order else None + ) + + if default_thread_slowmode_delay is not MISSING: + options["default_thread_slowmode_delay"] = default_thread_slowmode_delay + + if default_auto_archive_duration is not MISSING: + options["default_auto_archive_duration"] = default_auto_archive_duration + if default_reaction_emoji is not MISSING: if isinstance(default_reaction_emoji, _EmojiTag): # GuildEmoji, PartialEmoji default_reaction_emoji = default_reaction_emoji._to_partial() @@ -1456,10 +1587,16 @@ async def create_forum_channel( default_reaction_emoji = PartialEmoji(name=None, id=default_reaction_emoji) elif isinstance(default_reaction_emoji, str): default_reaction_emoji = PartialEmoji.from_str(default_reaction_emoji) + elif default_reaction_emoji is None: + pass else: - raise InvalidArgument("default_reaction_emoji must be of type: GuildEmoji | int | str") + raise InvalidArgument("default_reaction_emoji must be of type: GuildEmoji | int | str | None") - options["default_reaction_emoji"] = default_reaction_emoji._to_forum_reaction_payload() + options["default_reaction_emoji"] = ( + default_reaction_emoji._to_forum_reaction_payload() + if default_reaction_emoji + else None + ) data = await self._create_channel( name, From a1e8949568b1340145d026346af927cb98d13bad Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 30 Aug 2025 16:57:25 +0200 Subject: [PATCH 09/35] feat: :sparkles: Soundboard (#2623) Signed-off-by: Paillat Signed-off-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: Dasupergrasskakjd Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> (cherry picked from commit 5ef8a52b2a13a8ff056100ba0332e8e662b7888f) Signed-off-by: Paillat-dev --- CHANGELOG.md | 9 ++ discord/__init__.py | 1 + discord/asset.py | 8 ++ discord/channel.py | 102 ++++++++++++++ discord/client.py | 45 ++++++ discord/enums.py | 11 ++ discord/gateway.py | 10 ++ discord/guild.py | 190 ++++++++++++++++++++++++- discord/http.py | 101 +++++++++++++- discord/raw_models.py | 27 +++- discord/soundboard.py | 270 ++++++++++++++++++++++++++++++++++++ discord/state.py | 80 +++++++++++ discord/types/channel.py | 12 ++ discord/types/soundboard.py | 42 ++++++ discord/utils/private.py | 10 +- docs/api/enums.rst | 14 ++ docs/api/events.rst | 82 +++++++++++ docs/api/models.rst | 19 +++ examples/soundboard.py | 170 +++++++++++++++++++++++ 19 files changed, 1191 insertions(+), 12 deletions(-) create mode 100644 discord/soundboard.py create mode 100644 discord/types/soundboard.py create mode 100644 examples/soundboard.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 63355e05e6..88210abca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,15 @@ These changes are available on the `master` branch, but have not yet been releas ([#2659](https://github.com/Pycord-Development/pycord/pull/2659)) - Added `VoiceMessage` subclass of `File` to allow voice messages to be sent. ([#2579](https://github.com/Pycord-Development/pycord/pull/2579)) +- Added the following soundboard-related features: + - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, + `SoundboardSound.edit()`, and `SoundboardSound.delete()`. + - Access Discord default sounds with `Client.fetch_default_sounds()`. + - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. + - New `on_voice_channel_effect_send` event for sound and emoji effects. + - Soundboard limits based on guild premium tier (8-48 slots) in + `Guild.soundboard_limit`. + ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) - Added new `Subscription` object and related methods/events. ([#2564](https://github.com/Pycord-Development/pycord/pull/2564)) - Added `Message.forward_to`, `Message.snapshots`, and other related attributes. diff --git a/discord/__init__.py b/discord/__init__.py index 2d17e2dc2a..8a613ad558 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -65,6 +65,7 @@ from .role import * from .scheduled_events import * from .shard import * +from .soundboard import * from .stage_instance import * from .sticker import * from .team import * diff --git a/discord/asset.py b/discord/asset.py index b1e889fa9d..6a700adb4e 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -328,6 +328,14 @@ def _from_scheduled_event_image(cls, state, event_id: int, cover_hash: str) -> A animated=False, ) + @classmethod + def _from_soundboard_sound(cls, state, sound_id: int) -> Asset: + return cls( + state, + url=f"{cls.BASE}/soundboard-sounds/{sound_id}", + key=str(sound_id), + ) + def __str__(self) -> str: return self._url diff --git a/discord/channel.py b/discord/channel.py index d96a3c72d4..c099c66a0d 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -32,6 +32,7 @@ Callable, Iterable, Mapping, + NamedTuple, Sequence, TypeVar, overload, @@ -52,6 +53,7 @@ from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum from .enums import ( VideoQualityMode, + VoiceChannelEffectAnimationType, VoiceRegion, try_enum, ) @@ -64,6 +66,7 @@ from .object import Object from .partial_emoji import PartialEmoji, _EmojiTag from .permissions import PermissionOverwrite, Permissions +from .soundboard import PartialSoundboardSound, SoundboardSound from .stage_instance import StageInstance from .threads import Thread from .utils import MISSING @@ -80,6 +83,7 @@ "ForumChannel", "MediaChannel", "ForumTag", + "VoiceChannelEffectSendEvent", ) if TYPE_CHECKING: @@ -101,6 +105,7 @@ from .types.channel import StageChannel as StageChannelPayload from .types.channel import TextChannel as TextChannelPayload from .types.channel import VoiceChannel as VoiceChannelPayload + from .types.channel import VoiceChannelEffectSendEvent as VoiceChannelEffectSend from .types.snowflake import SnowflakeList from .types.threads import ThreadArchiveDuration from .ui.view import View @@ -2162,6 +2167,25 @@ async def set_status(self, status: str | None, *, reason: str | None = None) -> """ await self._state.http.set_voice_channel_status(self.id, status, reason=reason) + async def send_soundboard_sound(self, sound: PartialSoundboardSound) -> None: + """|coro| + + Sends a soundboard sound to the voice channel. + + Parameters + ---------- + sound: :class:`PartialSoundboardSound` + The soundboard sound to send. + + Raises + ------ + Forbidden + You do not have proper permissions to send the soundboard sound. + HTTPException + Sending the soundboard sound failed. + """ + await self._state.http.send_soundboard_sound(self.id, sound) + class StageChannel(discord.abc.Messageable, VocalGuildChannel): """Represents a Discord guild stage channel. @@ -3357,6 +3381,84 @@ def __repr__(self) -> str: return f"" +class VoiceChannelEffectAnimation(NamedTuple): + """Represents an animation that can be sent to a voice channel. + + .. versionadded:: 2.7 + """ + + id: int + type: VoiceChannelEffectAnimationType + + +class VoiceChannelSoundEffect(PartialSoundboardSound): ... + + +class VoiceChannelEffectSendEvent: + """Represents the payload for an :func:`on_voice_channel_effect_send`. + + .. versionadded:: 2.7 + + Attributes + ---------- + animation_type: :class:`int` + The type of animation that is being sent. + animation_id: :class:`int` + The ID of the animation that is being sent. + sound: Optional[:class:`SoundboardSound`] + The sound that is being sent, could be ``None`` if the effect is not a sound effect. + guild: :class:`Guild` + The guild in which the sound is being sent. + user: :class:`Member` + The member that sent the sound. + channel: :class:`VoiceChannel` + The voice channel in which the sound is being sent. + data: :class:`dict` + The raw data sent by the gateway. + """ + + __slots__ = ( + "_state", + "animation_type", + "animation_id", + "sound", + "guild", + "user", + "channel", + "data", + "emoji", + ) + + def __init__( + self, + data: VoiceChannelEffectSend, + state: ConnectionState, + sound: SoundboardSound | PartialSoundboardSound | None = None, + ) -> None: + self._state = state + channel_id = int(data["channel_id"]) + user_id = int(data["user_id"]) + guild_id = int(data["guild_id"]) + self.animation_type: VoiceChannelEffectAnimationType = try_enum( + VoiceChannelEffectAnimationType, data["animation_type"] + ) + self.animation_id = int(data["animation_id"]) + self.sound = sound + self.guild = state._get_guild(guild_id) + self.user = self.guild.get_member(user_id) + self.channel = self.guild.get_channel(channel_id) + self.emoji = ( + PartialEmoji( + name=data["emoji"]["name"], + animated=data["emoji"]["animated"], + id=data["emoji"]["id"], + ) + if data.get("emoji", None) + else None + ) + self.data = data + + def _guild_channel_factory(channel_type: int): value = try_enum(ChannelType, channel_type) if value is ChannelType.text: diff --git a/discord/client.py b/discord/client.py index 71aed9351a..4658f2563a 100644 --- a/discord/client.py +++ b/discord/client.py @@ -55,6 +55,7 @@ from .mentions import AllowedMentions from .monetization import SKU, Entitlement from .object import Object +from .soundboard import SoundboardSound from .stage_instance import StageInstance from .state import ConnectionState from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory @@ -80,6 +81,7 @@ from .member import Member from .message import Message from .poll import Poll + from .soundboard import SoundboardSound from .ui.item import Item from .voice_client import VoiceProtocol @@ -2268,3 +2270,46 @@ async def delete_emoji(self, emoji: Snowflake) -> None: await self._connection.http.delete_application_emoji(self.application_id, emoji.id) if self._connection.cache_app_emojis and self._connection.get_emoji(emoji.id): self._connection.remove_emoji(emoji) + + def get_sound(self, sound_id: int) -> SoundboardSound | None: + """Gets a :class:`.Sound` from the bot's sound cache. + + .. versionadded:: 2.7 + + Parameters + ---------- + sound_id: :class:`int` + The ID of the sound to get. + + Returns + ------- + Optional[:class:`.SoundboardSound`] + The sound with the given ID. + """ + return self._connection._get_sound(sound_id) + + @property + def sounds(self) -> list[SoundboardSound]: + """A list of all the sounds the bot can see. + + .. versionadded:: 2.7 + """ + return self._connection.sounds + + async def fetch_default_sounds(self) -> list[SoundboardSound]: + """|coro| + + Fetches the bot's default sounds. + + .. versionadded:: 2.7 + + Returns + ------- + List[:class:`.SoundboardSound`] + The bot's default sounds. + """ + data = await self._connection.http.get_default_sounds() + return [ + SoundboardSound(http=self.http, state=self._connection, data=s) + for s in data + ] diff --git a/discord/enums.py b/discord/enums.py index dbed0cf59e..a213ba7c77 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -74,6 +74,7 @@ "PromptType", "OnboardingMode", "ReactionType", + "VoiceChannelEffectAnimationType", "SKUType", "EntitlementType", "EntitlementOwnerType", @@ -996,6 +997,16 @@ class PollLayoutType(Enum): default = 1 +class VoiceChannelEffectAnimationType(Enum): + """Voice channel effect animation type. + + .. versionadded:: 2.7 + """ + + premium = 0 + basic = 1 + + class MessageReferenceType(Enum): """The type of the message reference object""" diff --git a/discord/gateway.py b/discord/gateway.py index c829186137..de25237c71 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -284,6 +284,7 @@ class DiscordWebSocket: HELLO = 10 HEARTBEAT_ACK = 11 GUILD_SYNC = 12 + REQUEST_SOUNDBOARD_SOUNDS = 31 def __init__(self, socket, *, loop): self.socket = socket @@ -714,6 +715,15 @@ async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=Fal _log.debug("Updating our voice state to %s.", payload) await self.send_as_json(payload) + async def request_soundboard_sounds(self, guild_ids): + payload = { + "op": self.REQUEST_SOUNDBOARD_SOUNDS, + "d": {"guild_ids": guild_ids}, + } + + _log.debug("Requesting soundboard sounds for guilds %s.", guild_ids) + await self.send_as_json(payload) + async def close(self, code=4000): if self._keep_alive: self._keep_alive.stop() diff --git a/discord/guild.py b/discord/guild.py index a0f727efcf..b40af647e2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -81,6 +81,7 @@ from .permissions import PermissionOverwrite from .role import Role, RoleColours from .scheduled_events import ScheduledEvent, ScheduledEventLocation +from .soundboard import SoundboardSound from .stage_instance import StageInstance from .sticker import GuildSticker from .threads import Thread, ThreadMember @@ -129,6 +130,7 @@ class BanEntry(NamedTuple): class _GuildLimit(NamedTuple): emoji: int stickers: int + soundboard: int bitrate: float filesize: int @@ -285,14 +287,25 @@ class Guild(Hashable): "_threads", "approximate_member_count", "approximate_presence_count", + "_sounds", ) _PREMIUM_GUILD_LIMITS: ClassVar[dict[int | None, _GuildLimit]] = { - None: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=10_485_760), - 0: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=10_485_760), - 1: _GuildLimit(emoji=100, stickers=15, bitrate=128e3, filesize=10_485_760), - 2: _GuildLimit(emoji=150, stickers=30, bitrate=256e3, filesize=52_428_800), - 3: _GuildLimit(emoji=250, stickers=60, bitrate=384e3, filesize=104_857_600), + None: _GuildLimit( + emoji=50, stickers=5, soundboard=8, bitrate=96e3, filesize=10_485_760 + ), + 0: _GuildLimit( + emoji=50, stickers=5, soundboard=8, bitrate=96e3, filesize=10_485_760 + ), + 1: _GuildLimit( + emoji=100, stickers=15, soundboard=24, bitrate=128e3, filesize=10_485_760 + ), + 2: _GuildLimit( + emoji=150, stickers=30, soundboard=36, bitrate=256e3, filesize=52_428_800 + ), + 3: _GuildLimit( + emoji=250, stickers=60, soundboard=48, bitrate=384e3, filesize=104_857_600 + ), } def __init__(self, *, data: GuildPayload, state: ConnectionState): @@ -307,6 +320,7 @@ def __init__(self, *, data: GuildPayload, state: ConnectionState): self._voice_states: dict[int, VoiceState] = {} self._threads: dict[int, Thread] = {} self._state: ConnectionState = state + self._sounds: dict[int, SoundboardSound] = {} self._from_data(data) def _add_channel(self, channel: GuildChannel, /) -> None: @@ -517,6 +531,133 @@ def _from_data(self, guild: GuildPayload) -> None: for obj in guild.get("voice_states", []): self._update_voice_state(obj, int(obj["channel_id"])) + for sound in guild.get("soundboard_sounds", []): + sound = SoundboardSound(state=state, http=state.http, data=sound) + self._add_sound(sound) + + def _add_sound(self, sound: SoundboardSound) -> None: + self._sounds[sound.id] = sound + self._state._add_sound(sound) + + def _remove_sound(self, sound_id: int) -> None: + self._sounds.pop(sound_id, None) + + async def fetch_sounds(self) -> list[SoundboardSound]: + """|coro| + Fetches all the soundboard sounds in the guild. + + .. versionadded:: 2.7 + + Returns + ------- + List[:class:`SoundboardSound`] + The sounds in the guild. + """ + data = await self._state.http.get_all_guild_sounds(self.id) + return [ + SoundboardSound( + state=self._state, + http=self._state.http, + data=sound, + ) + for sound in data["items"] + ] + + async def fetch_sound(self, sound_id: int) -> SoundboardSound: + """|coro| + Fetches a soundboard sound in the guild. + + .. versionadded:: 2.7 + + Parameters + ---------- + sound_id: :class:`int` + The ID of the sound. + + Returns + ------- + :class:`SoundboardSound` + The sound. + """ + data = await self._state.http.get_guild_sound(self.id, sound_id) + return SoundboardSound( + state=self._state, + http=self._state.http, + data=data, + ) + + async def create_sound( + self, + name: str, + sound: bytes, + volume: float = 1.0, + emoji: PartialEmoji | GuildEmoji | str | None = None, + reason: str | None = None, + ) -> SoundboardSound: + """|coro| + Creates a :class:`SoundboardSound` in the guild. + You must have :attr:`Permissions.manage_expressions` permission to use this. + + .. versionadded:: 2.7 + + Parameters + ---------- + name: :class:`str` + The name of the sound. + sound: :class:`bytes` + The :term:`py:bytes-like object` representing the sound data. + Only MP3 sound files that are less than 5.2 seconds long are supported. + volume: :class:`float` + The volume of the sound. Defaults to 1.0. + emoji: Optional[Union[:class:`PartialEmoji`, :class:`GuildEmoji`, :class:`str`]] + The emoji of the sound. + reason: Optional[:class:`str`] + The reason for creating this sound. Shows up on the audit log. + + Returns + ------- + :class:`SoundboardSound` + The created sound. + + Raises + ------ + :exc:`HTTPException` + Creating the sound failed. + :exc:`Forbidden` + You do not have permissions to create sounds. + """ + + payload: dict[str, Any] = { + "name": name, + "sound": bytes_to_base64_data(sound), + "volume": volume, + "emoji_id": None, + "emoji_name": None, + } + + if emoji is not None: + if isinstance(emoji, _EmojiTag): + partial_emoji = emoji._to_partial() + elif isinstance(emoji, str): + partial_emoji = PartialEmoji.from_str(emoji) + else: + partial_emoji = None + + if partial_emoji is not None: + if partial_emoji.id is None: + payload["emoji_name"] = partial_emoji.name + else: + payload["emoji_id"] = partial_emoji.id + + data = await self._state.http.create_guild_sound( + self.id, reason=reason, **payload + ) + return SoundboardSound( + state=self._state, + http=self._state.http, + data=data, + ) + # TODO: refactor/remove? def _sync(self, data: GuildPayload) -> None: try: @@ -643,6 +784,17 @@ def categories(self) -> list[CategoryChannel]: r.sort(key=lambda c: (c.position or -1, c.id)) return r + @property + def sounds(self) -> list[SoundboardSound]: + """A list of soundboard sounds that belong to this guild. + + .. versionadded:: 2.7 + + This is sorted by the position and are in UI order from top to bottom. + """ + r = list(self._sounds.values()) + return r + def by_category(self) -> list[ByCategoryItem]: """Returns every :class:`CategoryChannel` and their associated channels. @@ -791,6 +943,17 @@ def sticker_limit(self) -> int: more_stickers = 60 if "MORE_STICKERS" in self.features else 0 return max(more_stickers, self._PREMIUM_GUILD_LIMITS[self.premium_tier].stickers) + @property + def soundboard_limit(self) -> int: + """The maximum number of soundboard slots this guild has. + + .. versionadded:: 2.7 + """ + more_soundboard = 48 if "MORE_SOUNDBOARD" in self.features else 0 + return max( + more_soundboard, self._PREMIUM_GUILD_LIMITS[self.premium_tier].soundboard + ) + @property def bitrate_limit(self) -> int: """The maximum bitrate for voice channels this guild can have.""" @@ -4188,3 +4351,20 @@ def entitlements( guild_id=self.id, exclude_ended=exclude_ended, ) + + def get_sound(self, sound_id: int) -> Soundboard | None: + """Returns a sound with the given ID. + + .. versionadded :: 2.7 + + Parameters + ---------- + sound_id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`SoundboardSound`] + The sound or ``None`` if not found. + """ + return self._sounds.get(sound_id) diff --git a/discord/http.py b/discord/http.py index 23d7e77e88..8a4867934e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -46,8 +46,9 @@ ) from .file import VoiceMessage from .gateway import DiscordClientWebSocketResponse +from .soundboard import PartialSoundboardSound, SoundboardSound from .utils import MISSING -from .utils.private import from_json, get_mime_type_for_image, to_json, warn_deprecated +from .utils.private import from_json, get_mime_type_for_file, to_json, warn_deprecated _log = logging.getLogger(__name__) @@ -85,6 +86,7 @@ widget, ) from .types.snowflake import Snowflake, SnowflakeList + from .types.soundboard import SoundboardSound as SoundboardSoundPayload T = TypeVar("T") BE = TypeVar("BE", bound=BaseException) @@ -1671,7 +1673,7 @@ def create_guild_sticker( initial_bytes = file.fp.read(16) try: - mime_type = get_mime_type_for_image(initial_bytes) + mime_type = get_mime_type_for_file(initial_bytes) except InvalidArgument: if initial_bytes.startswith(b"{"): mime_type = "application/json" @@ -3057,3 +3059,98 @@ async def get_bot_gateway(self, *, encoding: str = "json", zlib: bool = True) -> def get_user(self, user_id: Snowflake) -> Response[user.User]: return self.request(Route("GET", "/users/{user_id}", user_id=user_id)) + + def delete_sound( + self, sound: SoundboardSound, *, reason: str | None + ) -> Response[None]: + return self.request( + Route( + "DELETE", + "/guilds/{guild_id}/soundboard-sounds/{sound_id}", + guild_id=sound.guild.id, + sound_id=sound.id, + ), + reason=reason, + ) + + def get_default_sounds(self) -> Response[list[SoundboardSoundPayload]]: + return self.request(Route("GET", "/soundboard-default-sounds")) + + def create_guild_sound( + self, guild_id: Snowflake, reason: str | None, **payload + ) -> Response[SoundboardSoundPayload]: + keys = ( + "name", + "sound", + "volume", + "emoji_id", + "emoji_name", + ) + + payload = {k: v for k, v in payload.items() if k in keys and v is not None} + + return self.request( + Route("POST", "/guilds/{guild_id}/soundboard-sounds", guild_id=guild_id), + json=payload, + reason=reason, + ) + + def get_all_guild_sounds( + self, guild_id: Snowflake + ) -> Response[list[SoundboardSoundPayload]]: + return self.request( + Route("GET", "/guilds/{guild_id}/soundboard-sounds", guild_id=guild_id) + ) + + def get_guild_sound( + self, guild_id: Snowflake, sound_id: Snowflake + ) -> Response[SoundboardSoundPayload]: + return self.request( + Route( + "GET", + "/guilds/{guild_id}/soundboard-sounds/{sound_id}", + guild_id=guild_id, + sound_id=sound_id, + ) + ) + + def edit_guild_sound( + self, guild_id: Snowflake, sound_id: Snowflake, *, reason: str | None, **payload + ) -> Response[SoundboardSoundPayload]: + keys = ( + "name", + "volume", + "emoji_id", + "emoji_name", + ) + + payload = {k: v for k, v in payload.items() if k in keys and v is not None} + + return self.request( + Route( + "PATCH", + "/guilds/{guild_id}/soundboard-sounds/{sound_id}", + guild_id=guild_id, + sound_id=sound_id, + ), + json=payload, + reason=reason, + ) + + def send_soundboard_sound( + self, channel_id: int, sound: PartialSoundboardSound + ) -> Response[None]: + payload = { + "sound_id": sound.id, + } + if isinstance(sound, SoundboardSound) and not sound.is_default_sound: + payload["source_guild_id"] = sound.guild_id + + return self.request( + Route( + "POST", + "/channels/{channel_id}/send-soundboard-sound", + channel_id=channel_id, + ), + json=payload, + ) diff --git a/discord/raw_models.py b/discord/raw_models.py index 702b992e53..fa543dfb0b 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -29,7 +29,13 @@ from typing import TYPE_CHECKING from .automod import AutoModAction, AutoModTriggerType -from .enums import AuditLogAction, ChannelType, ReactionType, try_enum +from .enums import ( + AuditLogAction, + ChannelType, + ReactionType, + VoiceChannelEffectAnimationType, + try_enum, +) if TYPE_CHECKING: from .abc import MessageableChannel @@ -37,6 +43,7 @@ from .member import Member from .message import Message from .partial_emoji import PartialEmoji + from .soundboard import PartialSoundboardSound, SoundboardSound from .state import ConnectionState from .threads import Thread from .types.raw_models import ( @@ -55,6 +62,9 @@ ThreadMembersUpdateEvent, ThreadUpdateEvent, TypingEvent, + ) + from .types.raw_models import VoiceChannelEffectSendEvent as VoiceChannelEffectSend + from .types.raw_models import ( VoiceChannelStatusUpdateEvent, ) from .types.raw_models import AutoModActionExecutionEvent as AutoModActionExecution @@ -79,6 +89,7 @@ "RawAuditLogEntryEvent", "RawVoiceChannelStatusUpdateEvent", "RawMessagePollVoteEvent", + "RawSoundboardSoundDeleteEvent", ) @@ -827,3 +838,17 @@ def __init__(self, data: MessagePollVoteEvent, added: bool) -> None: self.guild_id: int | None = int(data["guild_id"]) except KeyError: self.guild_id: int | None = None + + +class RawSoundboardSoundDeleteEvent(_RawReprMixin): + """Represents the payload for an :func:`on_raw_soundboard_sound_delete`. + + .. versionadded 2.7 + """ + + __slots__ = ("sound_id", "guild_id", "data") + + def __init__(self, data: PartialSoundboardSound) -> None: + self.sound_id: int = int(data["sound_id"]) + self.guild_id: int = int(data["guild_id"]) + self.data: PartialSoundboardSound = data diff --git a/discord/soundboard.py b/discord/soundboard.py new file mode 100644 index 0000000000..6fae037090 --- /dev/null +++ b/discord/soundboard.py @@ -0,0 +1,270 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Coroutine + +from typing_extensions import override + +from .asset import Asset +from .emoji import PartialEmoji, _EmojiTag +from .mixins import Hashable +from .types.channel import ( + VoiceChannelEffectSendEvent as VoiceChannelEffectSendEventPayload, +) +from .types.soundboard import SoundboardSound as SoundboardSoundPayload +from .utils import cached_slot_property + +if TYPE_CHECKING: + from .guild import Guild + from .http import HTTPClient + from .state import ConnectionState + + +__all__ = ( + "PartialSoundboardSound", + "SoundboardSound", +) + + +class PartialSoundboardSound(Hashable): + """A partial soundboard sound. + + .. versionadded:: 2.7 + + Attributes + ---------- + id: :class:`int` + The sound's ID. + volume: :class:`float` + The sound's volume. + emoji: :class:`PartialEmoji` | :class:`None` + The sound's emoji. Could be ``None`` if the sound has no emoji. + """ + + __slots__ = ("id", "volume", "emoji", "_http", "_state") + + def __init__( + self, + data: SoundboardSoundPayload | VoiceChannelEffectSendEventPayload, + state: ConnectionState, + http: HTTPClient, + ): + self._http = http + self._state = state + self._from_data(data) + + def _from_data( + self, data: SoundboardSoundPayload | VoiceChannelEffectSendEventPayload + ) -> None: + self.id = int(data.get("sound_id", 0)) + self.volume = ( + float(data.get("volume", 0) or data.get("sound_volume", 0)) or None + ) + self.emoji = None + if raw_emoji := data.get( + "emoji" + ): # From gateway event (VoiceChannelEffectSendEventPayload) + self.emoji = PartialEmoji.from_dict(raw_emoji) + elif data.get("emoji_name") or data.get( + "emoji_id" + ): # From HTTP response (SoundboardSoundPayload) + self.emoji = PartialEmoji( + name=data.get("emoji_name"), + id=int(data.get("emoji_id", 0) or 0) or None, + ) + + @override + def __eq__( + self, other: PartialSoundboardSound + ) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] + if isinstance(other, self, __class__): + return self.id == other.id + return NotImplemented + + @override + def __ne__( + self, other: PartialSoundboardSound + ) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] + return not self.__eq__(other) + + @property + def file(self) -> Asset: + """:class:`Asset`: Returns the sound's file.""" + return Asset._from_soundboard_sound(self._state, sound_id=self.id) + + def __repr__(self) -> str: + return f"" + + +class SoundboardSound(PartialSoundboardSound): + """Represents a soundboard sound. + + .. versionadded:: 2.7 + + Attributes + ---------- + id: :class:`int` + The sound's ID. + volume: :class:`float` + The sound's volume. + emoji: :class:`PartialEmoji` | :class:`None` + The sound's emoji. Could be ``None`` if the sound has no emoji. + name: :class:`str` + The sound's name. + available: :class:`bool` + Whether the sound is available. Could be ``False`` if the sound is not available. + This is the case, for example, when the guild loses the boost level required to use the sound. + guild_id: :class:`int` | :class:`None` + The ID of the guild to which the sound belongs. Could be :class:`None` if the sound is a default sound. + user: :class:`User` | :class:`None` + The sound's owner. Could be ``None`` if the sound is a default sound. + """ + + __slots__ = ( + "name", + "available", + "guild_id", + "user", + "_cs_guild", + "_state", + ) + + def __init__( + self, + *, + state: ConnectionState, + http: HTTPClient, + data: SoundboardSoundPayload, + ) -> None: + super().__init__(data, state, http) + + @override + def _from_data( + self, data: SoundboardSoundPayload + ) -> None: # pyright: ignore[reportIncompatibleMethodOverride] + super()._from_data(data) + self.name = data["name"] + self.available: bool = data["available"] + self.guild_id = int(data.get("guild_id", 0) or 0) or None + user = data.get("user") + self.user = self._state.store_user(user) if user else None + + @cached_slot_property("_cs_guild") + def guild(self) -> Guild | None: + """:class:`Guild` | :class:`None` The guild the sound belongs to. Could be :class:`None` if the sound is a default sound.""" + return self._state._get_guild(self.guild_id) if self.guild_id else None + + @override + def __eq__( + self, other: SoundboardSound + ) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] + return isinstance(other, SoundboardSound) and self.__dict__ == other.__dict__ + + @property + def is_default_sound(self) -> bool: + """:class:`bool`: Whether the sound is a default sound.""" + return self.guild_id is None + + def edit( + self, + *, + name: str | None = None, + volume: float | None = None, + emoji: PartialEmoji | str | None = None, + reason: str | None = None, + ) -> Coroutine[Any, Any, SoundboardSound]: + """Edits the sound. + + .. versionadded:: 2.7 + + Parameters + ---------- + name: Optional[:class:`str`] + The new name of the sound. + volume: Optional[:class:`float`] + The new volume of the sound. + emoji: Optional[Union[:class:`PartialEmoji`, :class:`str`]] + The new emoji of the sound. + reason: Optional[:class:`str`] + The reason for editing the sound. Shows up in the audit log. + + Returns + ------- + :class:`SoundboardSound` + The edited sound. + + Raises + ------ + :exc:`ValueError` + Editing a default sound is not allowed. + """ + if self.is_default_sound: + raise ValueError("Cannot edit a default sound.") + payload: dict[str, Any] = { + "name": name, + "volume": volume, + "emoji_id": None, + "emoji_name": None, + } + partial_emoji = None + if emoji is not None: + if isinstance(emoji, _EmojiTag): + partial_emoji = emoji._to_partial() + elif isinstance(emoji, str): + partial_emoji = PartialEmoji.from_str(emoji) + + if partial_emoji is not None: + if partial_emoji.id is None: + payload["emoji_name"] = partial_emoji.name + else: + payload["emoji_id"] = partial_emoji.id + + return self._http.edit_guild_sound( + self.guild_id, self.id, reason=reason, **payload + ) + + def delete(self, *, reason: str | None = None) -> Coroutine[Any, Any, None]: + """Deletes the sound. + + .. versionadded:: 2.7 + + Parameters + ---------- + reason: Optional[:class:`str`] + The reason for deleting the sound. Shows up in the audit log. + + Raises + ------ + :exc:`ValueError` + Deleting a default sound is not allowed. + """ + if self.is_default_sound: + raise ValueError("Cannot delete a default sound.") + return self._http.delete_sound(self, reason=reason) + + @override + def __repr__(self) -> str: + return f"" diff --git a/discord/state.py b/discord/state.py index a6925df145..fd4934eacf 100644 --- a/discord/state.py +++ b/discord/state.py @@ -66,6 +66,7 @@ from .raw_models import * from .role import Role from .scheduled_events import ScheduledEvent +from .soundboard import PartialSoundboardSound, SoundboardSound from .stage_instance import StageInstance from .sticker import GuildSticker from .threads import Thread, ThreadMember @@ -270,6 +271,7 @@ def clear(self, *, views: bool = True) -> None: self._view_store: ViewStore = ViewStore(self) self._modal_store: ModalStore = ModalStore(self) self._voice_clients: dict[int, VoiceClient] = {} + self._sounds: dict[int, SoundboardSound] = {} # LRU of max size 128 self._private_channels: OrderedDict[int, PrivateChannel] = OrderedDict() @@ -616,6 +618,7 @@ async def _delay_ready(self) -> None: except asyncio.CancelledError: pass else: + await self._add_default_sounds() # dispatch the event self.call_handlers("ready") self.dispatch("ready") @@ -1883,6 +1886,83 @@ def create_message( ) -> Message: return Message(state=self, channel=channel, data=data) + def parse_voice_channel_effect_send(self, data) -> None: + if sound_id := int(data.get("sound_id", 0)): + sound = self._get_sound(sound_id) + if sound is None: + sound = PartialSoundboardSound(data, self, self.http) + raw = VoiceChannelEffectSendEvent(data, self, sound) + else: + raw = VoiceChannelEffectSendEvent(data, self, None) + + self.dispatch("voice_channel_effect_send", raw) + + def _get_sound(self, sound_id: int) -> SoundboardSound | None: + return self._sounds.get(sound_id) + + def _update_sound(self, sound: SoundboardSound) -> SoundboardSound | None: + before = self._sounds.get(sound.id) + self._sounds[sound.id] = sound + return before + + def parse_soundboard_sounds(self, data) -> None: + guild_id = int(data["guild_id"]) + for sound_data in data["soundboard_sounds"]: + self._add_sound( + SoundboardSound( + state=self, http=self.http, data=sound_data, guild_id=guild_id + ) + ) + + def parse_guild_soundboard_sounds_update(self, data): + before_sounds = [] + after_sounds = [] + for sound_data in data["soundboard_sounds"]: + after = SoundboardSound(state=self, http=self.http, data=sound_data) + if before := self._update_sound(after): + before_sounds.append(before) + after_sounds.append(after) + if len(before_sounds) == len(after_sounds): + self.dispatch("soundboard_sounds_update", before_sounds, after_sounds) + self.dispatch("raw_soundboard_sounds_update", after_sounds) + + def parse_guild_soundboard_sound_update(self, data): + after = SoundboardSound(state=self, http=self.http, data=data) + if before := self._update_sound(after): + self.dispatch("soundboard_sound_update", before, after) + self.dispatch("raw_soundboard_sound_update", after) + + def parse_guild_soundboard_sound_create(self, data): + sound = SoundboardSound(state=self, http=self.http, data=data) + self._add_sound(sound) + self.dispatch("soundboard_sound_create", sound) + + def parse_guild_soundboard_sound_delete(self, data): + sound_id = int(data["sound_id"]) + sound = self._get_sound(sound_id) + if sound is not None: + self._remove_sound(sound) + self.dispatch("soundboard_sound_delete", sound) + self.dispatch( + "raw_soundboard_sound_delete", RawSoundboardSoundDeleteEvent(data) + ) + + async def _add_default_sounds(self) -> None: + default_sounds = await self.http.get_default_sounds() + for default_sound in default_sounds: + sound = SoundboardSound(state=self, http=self.http, data=default_sound) + self._add_sound(sound) + + def _add_sound(self, sound: SoundboardSound) -> None: + self._sounds[sound.id] = sound + + def _remove_sound(self, sound: SoundboardSound) -> None: + self._sounds.pop(sound.id, None) + + @property + def sounds(self) -> list[SoundboardSound]: + return list(self._sounds.values()) + class AutoShardedConnectionState(ConnectionState): def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/discord/types/channel.py b/discord/types/channel.py index d215e20254..72096e7a73 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -31,6 +31,7 @@ from ..enums import SortOrder from ..flags import ChannelFlags +from .emoji import PartialEmoji from .snowflake import Snowflake from .threads import ThreadArchiveDuration, ThreadMember, ThreadMetadata from .user import User @@ -174,3 +175,14 @@ class StageInstance(TypedDict): privacy_level: PrivacyLevel discoverable_disabled: bool guild_scheduled_event_id: Snowflake + + +class VoiceChannelEffectSendEvent(TypedDict): + channel_id: Snowflake + guild_id: Snowflake + user_id: Snowflake + emoji: NotRequired[PartialEmoji | None] + animation_type: NotRequired[int | None] + animation_id: NotRequired[int] + sound_id: NotRequired[Snowflake | int] + sound_volume: NotRequired[float] diff --git a/discord/types/soundboard.py b/discord/types/soundboard.py new file mode 100644 index 0000000000..9a4c19b0ed --- /dev/null +++ b/discord/types/soundboard.py @@ -0,0 +1,42 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing_extensions import NotRequired, TypedDict + +from discord.types.user import User + +from .snowflake import Snowflake + + +class SoundboardSound(TypedDict): + name: str + sound_id: Snowflake | int + volume: float + emoji_name: str | None + emoji_id: Snowflake | None + guild_id: NotRequired[Snowflake] + user: NotRequired[User] + available: bool diff --git a/discord/utils/private.py b/discord/utils/private.py index 7ed3612f9e..78fb7a80e4 100644 --- a/discord/utils/private.py +++ b/discord/utils/private.py @@ -79,7 +79,7 @@ def get_as_snowflake(data: Any, key: str) -> int | None: return value and int(value) -def get_mime_type_for_image(data: bytes): +def get_mime_type_for_file(data: bytes): if data.startswith(b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"): return "image/png" elif data[0:3] == b"\xff\xd8\xff" or data[6:10] in (b"JFIF", b"Exif"): @@ -88,13 +88,15 @@ def get_mime_type_for_image(data: bytes): return "image/gif" elif data.startswith(b"RIFF") and data[8:12] == b"WEBP": return "image/webp" + elif data.startswith(b"\x49\x44\x33") or data.startswith(b"\xff\xfb"): + return "audio/mpeg" else: - raise InvalidArgument("Unsupported image type given") + raise InvalidArgument("Unsupported file type given") def bytes_to_base64_data(data: bytes) -> str: fmt = "data:{mime};base64,{data}" - mime = get_mime_type_for_image(data) + mime = get_mime_type_for_file(data) b64 = b64encode(data).decode("ascii") return fmt.format(mime=mime, data=b64) @@ -150,7 +152,7 @@ def resolve_template(code: Template | str) -> str: __all__ = ( "resolve_invite", "get_as_snowflake", - "get_mime_type_for_image", + "get_mime_type_for_file", "bytes_to_base64_data", "parse_ratelimit_header", "string_width", diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 34ac6ed5b9..61bedcc8dd 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2501,6 +2501,20 @@ of :class:`enum.Enum`. The interaction is in a private DM or group DM channel. +.. class:: VoiceChannelEffectAnimationType + + Represents the type of animation for a voice channel effect. + + .. versionadded:: 2.7 + + .. attribute:: premium + + The animation is a premium effect. + + .. attribute:: basic + + The animation is a basic effect. + .. class:: SubscriptionStatus diff --git a/docs/api/events.rst b/docs/api/events.rst index e9bd0a4c0d..b1f08b89e7 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -1413,3 +1413,85 @@ Voice Channel Status Update :param payload: The raw voice channel status update payload. :type payload: :class:`RawVoiceChannelStatusUpdateEvent` + +Voice Channel Effects +--------------------- +.. function:: on_voice_channel_effect_send(event) + + Called when a voice channel effect is sent. + + .. versionadded:: 2.7 + + :param event: The voice channel effect event. + :type event: :class:`VoiceChannelEffectSendEvent` + +Soundboard Sound +---------------- +.. function:: on_soundboard_sounds_update(before, after) + + Called when multiple guild soundboard sounds are updated at once and they were all already in the cache. + This is called, for example, when a guild loses a boost level and some sounds become unavailable. + + .. versionadded:: 2.7 + + :param before: The soundboard sounds prior to being updated. + :type before: List[:class:`SoundboardSound`] + :param after: The soundboard sounds after being updated. + :type after: List[:class:`SoundboardSound`] + +.. function:: on_raw_soundboard_sounds_update(after) + + Called when multiple guild soundboard sounds are updated at once. + This is called, for example, when a guild loses a boost level and some sounds become unavailable. + + .. versionadded:: 2.7 + + :param after: The soundboard sounds after being updated. + :type after: List[:class:`SoundboardSound`] + +.. function:: on_soundboard_sound_update(before, after) + + Called when a soundboard sound is updated and it was already in the cache. + + .. versionadded:: 2.7 + + :param before: The soundboard sound prior to being updated. + :type before: :class:`Soundboard + :param after: The soundboard sound after being updated. + :type after: :class:`Soundboard + +.. function:: on_raw_soundboard_sound_update(after) + + Called when a soundboard sound is updated. + + .. versionadded:: 2.7 + + :param after: The soundboard sound after being updated. + :type after: :class:`SoundboardSound` + +.. function:: on_soundboard_sound_delete(sound) + + Called when a soundboard sound is deleted. + + .. versionadded:: 2.7 + + :param sound: The soundboard sound that was deleted. + :type sound: :class:`SoundboardSound` + +.. function:: on_raw_soundboard_sound_delete(payload) + + Called when a soundboard sound is deleted. + + .. versionadded:: 2.7 + + :param payload: The raw event payload data. + :type payload: :class:`RawSoundboardSoundDeleteEvent` + +.. function:: on_soundboard_sound_create(sound) + + Called when a soundboard sound is created. + + .. versionadded:: 2.7 + + :param sound: The soundboard sound that was created. + :type sound: :class:`SoundboardSound` diff --git a/docs/api/models.rst b/docs/api/models.rst index 3188ccde1d..7ee6d0f7ef 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -555,6 +555,20 @@ Stickers .. autoclass:: GuildSticker() :members: +Soundboard +---------- + +.. attributetable:: PartialSoundboardSound + +.. autoclass:: PartialSoundboardSound() + :members: + +.. attributetable:: SoundboardSound + +.. autoclass:: SoundboardSound() + :members: + :inherited-members: + Events ------ @@ -638,6 +652,11 @@ Events .. autoclass:: RawVoiceChannelStatusUpdateEvent() :members: +.. attributetable:: VoiceChannelEffectSendEvent + +.. autoclass:: VoiceChannelEffectSendEvent() + :members: + Webhooks diff --git a/examples/soundboard.py b/examples/soundboard.py new file mode 100644 index 0000000000..d19faa666d --- /dev/null +++ b/examples/soundboard.py @@ -0,0 +1,170 @@ +import asyncio +import logging +import os + +from dotenv import load_dotenv + +import discord + +logging.basicConfig(level=logging.INFO) + +load_dotenv() +TOKEN = os.getenv("TOKEN") + +bot = discord.Bot() + + +class SoundboardCog(discord.Cog): + """A cog demonstrating Discord's soundboard features.""" + + def __init__(self, bot: discord.Bot): + self.bot = bot + + @discord.Cog.listener() + async def on_voice_channel_effect_send( + self, event: discord.VoiceChannelEffectSendEvent + ): + """Called when someone uses a soundboard effect in a voice channel.""" + if event.sound: + print(f"{event.user} played sound '{event.sound.name}' in {event.channel}") + elif event.emoji: + print(f"{event.user} sent emoji effect {event.emoji} in {event.channel}") + + @discord.slash_command() + async def list_sounds(self, ctx: discord.ApplicationContext): + """Lists all the available sounds in the guild.""" + await ctx.defer() + + # Fetch both default and guild-specific sounds + default_sounds = await self.bot.fetch_default_sounds() + guild_sounds = await ctx.guild.fetch_sounds() + + embed = discord.Embed(title="Available Sounds") + + # List default sounds + if default_sounds: + default_list = "\n".join( + f"{s.emoji} {s.name} (Volume: {s.volume})" for s in default_sounds + ) + embed.add_field( + name="Default Sounds", value=default_list or "None", inline=False + ) + + # List guild sounds + if guild_sounds: + guild_list = "\n".join( + f"{s.emoji} {s.name} (Volume: {s.volume})" for s in guild_sounds + ) + embed.add_field( + name="Guild Sounds", value=guild_list or "None", inline=False + ) + + await ctx.respond(embed=embed) + + @discord.slash_command() + @discord.default_permissions(manage_guild=True) + async def add_sound( + self, + ctx: discord.ApplicationContext, + name: str, + emoji: str, + attachment: discord.Attachment, + ): + """Adds a new sound to the guild's soundboard. Currently only supports mp3 files.""" + await ctx.defer() + + if not attachment.content_type.startswith("audio/"): + return await ctx.respond("Please upload an audio file!") + + try: + sound_bytes = await attachment.read() + emoji = discord.PartialEmoji.from_str(emoji) + + new_sound = await ctx.guild.create_sound( + name=name, sound=sound_bytes, volume=1.0, emoji=emoji + ) + + await ctx.respons(f"Added new sound: {new_sound.emoji} {new_sound.name}") + except Exception as e: + await ctx.respond(f"Failed to add sound: {str(e)}") + + @discord.slash_command() + @discord.default_permissions(manage_guild=True) + async def edit_sound( + self, + ctx: discord.ApplicationContext, + sound_name: str, + new_name: str | None = None, + new_emoji: str | None = None, + new_volume: float | None = None, + ): + """Edits an existing sound in the guild's soundboard.""" + await ctx.defer() + + # Find the sound by name + sounds = await ctx.guild.fetch_sounds() + sound = discord.utils.get(sounds, name=sound_name) + + if not sound: + return await ctx.respond(f"Sound '{sound_name}' not found!") + + try: + await sound.edit( + name=new_name or sound.name, + emoji=( + discord.PartialEmoji.from_str(new_emoji) + if new_emoji + else sound.emoji + ), + volume=new_volume or sound.volume, + ) + await ctx.respond(f"Updated sound: {sound.emoji} {sound.name}") + except Exception as e: + await ctx.respond(f"Failed to edit sound: {str(e)}") + + @discord.slash_command() + async def play_sound( + self, + ctx: discord.ApplicationContext, + sound_name: str, + channel: discord.VoiceChannel | None = None, + ): + """Plays a sound in a voice channel.""" + await ctx.defer() + + # Use author's voice channel if none specified + if not channel and ctx.author.voice: + channel = ctx.author.voice.channel + if not channel: + return await ctx.respond("Please specify a voice channel or join one!") + + try: + # Find the sound + sounds = await ctx.guild.fetch_sounds() + sound = discord.utils.get(sounds, name=sound_name) + if not sound: + # Check default sounds if not found in guild sounds + defaults = await self.bot.fetch_default_sounds() + sound = discord.utils.get(defaults, name=sound_name) + + if not sound: + return await ctx.respond(f"Sound '{sound_name}' not found!") + + # Connect to voice channel if not already connected + voice_client = await channel.connect() + + # Play the sound + await channel.send_soundboard_sound(sound) + await ctx.respond(f"Playing sound: {sound.emoji} {sound.name}") + + await asyncio.sleep(6) + if voice_client.is_connected(): + await voice_client.disconnect() + + except Exception as e: + await ctx.respond(f"Failed to play sound: {str(e)}") + + +bot.add_cog(SoundboardCog(bot)) + +bot.run(TOKEN) From 2abb5ddd28c421457d7e17c84fb73787d7193f5b Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 30 Aug 2025 17:11:13 +0200 Subject: [PATCH 10/35] feat: :recycle: Better nameplates assets api (#2890) :recycle: Better nameplates assets api Co-authored-by: Soheab <33902984+Soheab@users.noreply.github.com> (cherry picked from commit 3f2fb348cd4d2ea2db508d1e009465a94dbf695e) --- discord/asset.py | 12 ++++++++++++ discord/collectibles.py | 25 ++++++++++--------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index 6a700adb4e..6387c23e61 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -273,6 +273,18 @@ def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asse animated=False, ) + @classmethod + def _from_collectible( + cls, state: ConnectionState, asset: str, animated: bool = False + ) -> Asset: + name = "static.png" if not animated else "asset.webm" + return cls( + state, + url=f"{cls.BASE}/assets/collectibles/{asset}{name}", + key=asset, + animated=animated, + ) + @classmethod def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset: animated = False diff --git a/discord/collectibles.py b/discord/collectibles.py index ff7f62c4fe..ce8d489530 100644 --- a/discord/collectibles.py +++ b/discord/collectibles.py @@ -22,6 +22,7 @@ DEALINGS IN THE SOFTWARE. """ +from functools import cached_property from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -55,21 +56,15 @@ def __init__(self, data: NameplatePayload, state: "ConnectionState") -> None: def __repr__(self) -> str: return f"" - def get_asset(self, animated: bool = False) -> Asset: - """Returns the asset of the nameplate. - - Parameters - ---------- - animated: :class:`bool` - Whether to return the animated version of the asset, in webm version. Defaults to ``False``. - """ - fn = "static.png" if not animated else "asset.webm" - return Asset( - state=self._state, - url=f"{Asset.BASE}/assets/collectibles/{self._asset}{fn}", - key=self._asset.split("/")[-1], - animated=animated, - ) + @cached_property + def static_asset(self) -> Asset: + """The static :class:`Asset` of this nameplate.""" + return Asset._from_collectible(self._state, self._asset) + + @cached_property + def animated_asset(self) -> Asset: + """The animated :class:`Asset` of this nameplate.""" + return Asset._from_collectible(self._state, self._asset, animated=True) __all__ = ("Nameplate",) From cf7dc165ef7ff56175f7f701105839e6f16fe67e Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Sat, 30 Aug 2025 17:11:39 +0200 Subject: [PATCH 11/35] fix: handling of Optional[...] in command option type resolution (#2852) Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil (cherry picked from commit 2cf71dffb68de8673d35e6b7a2e79a9c44028dca) --- CHANGELOG.md | 2 ++ discord/commands/options.py | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88210abca1..5a12ce8276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2843](https://github.com/Pycord-Development/pycord/pull/2843)) - Fixed `TypeError` when using `@option` with certain annotations and along with `channel_types`. ([#2835](https://github.com/Pycord-Development/pycord/pull/2835)) +- Fixed `TypeError` when using `Optional[...]` or `... | None` in command option type. + ([#2852](https://github.com/Pycord-Development/pycord/pull/2852)) - Fixed type-hinting for `PermissionOverwrite.update`. ([#2878](https://github.com/Pycord-Development/pycord/pull/2878)) - Fixed `AttributeError` when accessing `AuditLogEntry.changes` more than once. diff --git a/discord/commands/options.py b/discord/commands/options.py index 835276673a..f4b4ad078e 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -26,8 +26,9 @@ import inspect import logging +import types from enum import Enum -from typing import TYPE_CHECKING, Literal, Optional, Type, Union +from typing import TYPE_CHECKING, Literal, Optional, Type, Union, get_args from ..abc import GuildChannel, Mentionable from ..channel import ( @@ -193,6 +194,7 @@ def __init__(self, input_type: InputType = str, /, description: str | None = Non if self.name is not None: self.name = str(self.name) self._parameter_name = self.name # default + input_type = self._strip_none_type(input_type) self._raw_type: InputType | tuple = input_type enum_choices = [] @@ -329,6 +331,26 @@ def __init__(self, input_type: InputType = str, /, description: str | None = Non if input_type is None: raise TypeError("input_type cannot be NoneType.") + @staticmethod + def _strip_none_type(input_type): + if input_type is type(None): + raise TypeError("Option type cannot be only NoneType") + + if isinstance(input_type, (types.UnionType, tuple)): + args = ( + get_args(input_type) + if isinstance(input_type, types.UnionType) + else input_type + ) + filtered = tuple(t for t in args if t is not type(None)) + if not filtered: + raise TypeError("Option type cannot be only NoneType") + if len(filtered) == 1: + return filtered[0] + return filtered + + return input_type + def to_dict(self) -> dict: as_dict = { "name": self.name, From f6f6f4e7f8915587281216165af8c1a9c0b177bc Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 30 Aug 2025 17:13:02 +0200 Subject: [PATCH 12/35] fix: change badge property to cached_property (#2891) Signed-off-by: Lala Sabathil (cherry picked from commit 2b5746179d31f552db4b2ac0de14ac621b601f66) --- discord/primary_guild.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 13e6ae035f..27edba7e97 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -22,6 +22,7 @@ DEALINGS IN THE SOFTWARE. """ +from functools import cached_property from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -61,7 +62,7 @@ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: def __repr__(self) -> str: return f"" - @property + @cached_property def badge(self) -> Asset | None: """Returns the badge asset, if available. From c7fdd01098b972534b7ec2ea205423d126980cbc Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 30 Aug 2025 17:16:25 +0200 Subject: [PATCH 13/35] docs: document version addition for asset properties Signed-off-by: Lala Sabathil (cherry picked from commit 27ece8688a587a06f9f53d5536fd059bc247b6c3) --- discord/collectibles.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/discord/collectibles.py b/discord/collectibles.py index ce8d489530..849cc3413e 100644 --- a/discord/collectibles.py +++ b/discord/collectibles.py @@ -58,12 +58,20 @@ def __repr__(self) -> str: @cached_property def static_asset(self) -> Asset: - """The static :class:`Asset` of this nameplate.""" + """ + The static :class:`Asset` of this nameplate. + + .. versionadded:: 2.7 + """ return Asset._from_collectible(self._state, self._asset) @cached_property def animated_asset(self) -> Asset: - """The animated :class:`Asset` of this nameplate.""" + """ + The animated :class:`Asset` of this nameplate. + + .. versionadded:: 2.7 + """ return Asset._from_collectible(self._state, self._asset, animated=True) From 329ae2fbc714e056bf719da3a2a4f166fc0dc401 Mon Sep 17 00:00:00 2001 From: JL710 Date: Sat, 30 Aug 2025 17:37:25 +0200 Subject: [PATCH 14/35] fix: datetime type for create_scheduled_event (#2879) Signed-off-by: Lala Sabathil Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil (cherry picked from commit bd771d9f736cba32399d0b9e79e5bdf2ff39c1d3) Signed-off-by: Paillat-dev --- CHANGELOG.md | 3 +++ discord/guild.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a12ce8276..c592ebb98c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2878](https://github.com/Pycord-Development/pycord/pull/2878)) - Fixed `AttributeError` when accessing `AuditLogEntry.changes` more than once. ([#2882])(https://github.com/Pycord-Development/pycord/pull/2882)) +- Fixed type hint for argument `start_time` and `end_time` of + `Guild.create_scheduled_event` + ([#2879](https://github.com/Pycord-Development/pycord/pull/2879)) ### Changed diff --git a/discord/guild.py b/discord/guild.py index b40af647e2..c6f3aa4688 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3980,8 +3980,8 @@ async def create_scheduled_event( *, name: str, description: str | utils.Undefined = MISSING, - start_time: datetime, - end_time: datetime | utils.Undefined = MISSING, + start_time: datetime.datetime, + end_time: datetime.datetime | utils.Undefined = MISSING, location: str | int | VoiceChannel | StageChannel | ScheduledEventLocation, privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, From 5051530b664a2d3f5049153f63a68642422fc6d2 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 30 Aug 2025 20:11:53 +0200 Subject: [PATCH 15/35] =?UTF-8?q?docs:=20=F0=9F=93=9D=20Fixes=20(#2892)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paillat Co-authored-by: Lala Sabathil Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> (cherry picked from commit bc5b9a3570e172d8fd37f9da512c61006f1ac6ed) Signed-off-by: Paillat-dev --- discord/channel.py | 2 -- discord/client.py | 22 +++++++++++++++++++--- discord/components.py | 2 -- discord/ext/bridge/core.py | 8 ++++++-- discord/ext/commands/core.py | 2 +- discord/guild.py | 9 +++++---- discord/raw_models.py | 4 +--- docs/api/data_classes.rst | 2 +- docs/api/enums.rst | 14 +++++++++++++- docs/api/events.rst | 4 ++-- docs/api/models.rst | 2 +- docs/conf.py | 2 +- docs/ext/bridge/api.rst | 2 +- 13 files changed, 51 insertions(+), 24 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index c099c66a0d..44a2117a43 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2778,8 +2778,6 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): The position in the category list. This is a number that starts at 0. e.g. the top category is position 0. Can be ``None`` if the channel was received in an interaction. - .. note:: - flags: :class:`ChannelFlags` Extra features of the channel. diff --git a/discord/client.py b/discord/client.py index 4658f2563a..c5de967f6e 100644 --- a/discord/client.py +++ b/discord/client.py @@ -77,7 +77,7 @@ if TYPE_CHECKING: from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime from .channel import DMChannel - from .interaction import Interaction + from .interactions import Interaction from .member import Member from .message import Message from .poll import Poll @@ -554,6 +554,15 @@ async def on_view_error(self, error: Exception, item: Item, interaction: Interac The default view error handler provided by the client. This only fires for a view if you did not define its :func:`~discord.ui.View.on_error`. + + Parameters + ---------- + error: :class:`Exception` + The exception that was raised. + item: :class:`Item` + The item that the user interacted with. + interaction: :class:`Interaction` + The interaction that was received. """ print( @@ -569,6 +578,13 @@ async def on_modal_error(self, error: Exception, interaction: Interaction) -> No The default implementation prints the traceback to stderr. This only fires for a modal if you did not define its :func:`~discord.ui.Modal.on_error`. + + Parameters + ---------- + error: :class:`Exception` + The exception that was raised. + interaction: :class:`Interaction` + The interaction that was received. """ print(f"Ignoring exception in modal {interaction.modal}:", file=sys.stderr) @@ -1270,7 +1286,7 @@ def add_listener(self, func: Coro, name: str | utils.Undefined = MISSING) -> Non TypeError The ``func`` parameter is not a coroutine function. ValueError - The ``name`` (event name) does not start with 'on_' + The ``name`` (event name) does not start with ``on_``. Example ------- @@ -1339,7 +1355,7 @@ def listen(self, name: str | utils.Undefined = MISSING, once: bool = False) -> C TypeError The function being listened to is not a coroutine. ValueError - The ``name`` (event name) does not start with 'on_' + The ``name`` (event name) does not start with ``on_``. Example ------- diff --git a/discord/components.py b/discord/components.py index 4cd8c0e1c0..a6d525cf79 100644 --- a/discord/components.py +++ b/discord/components.py @@ -676,7 +676,6 @@ def __str__(self) -> str: @property def url(self) -> str: - """Returns this media item's url.""" return self._url @url.setter @@ -778,7 +777,6 @@ def __init__(self, url, *, description=None, spoiler=False): @property def url(self) -> str: - """Returns the URL of this gallery's underlying media item.""" return self.media.url def is_dispatchable(self) -> bool: diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index 9d43be7817..25f13fb196 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -178,8 +178,10 @@ def __init__(self, callback, **kwargs): @property def name_localizations(self) -> dict[str, str] | None: """Returns name_localizations from :attr:`slash_variant` - You can edit/set name_localizations directly with + You can edit/set name_localizations directly with + .. code-block:: python3 + bridge_command.name_localizations["en-UK"] = ... # or any other locale # or bridge_command.name_localizations = {"en-UK": ..., "fr-FR": ...} @@ -193,8 +195,10 @@ def name_localizations(self, value): @property def description_localizations(self) -> dict[str, str] | None: """Returns description_localizations from :attr:`slash_variant` - You can edit/set description_localizations directly with + You can edit/set description_localizations directly with + .. code-block:: python3 + bridge_command.description_localizations["en-UK"] = ... # or any other locale # or bridge_command.description_localizations = {"en-UK": ..., "fr-FR": ...} diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 36d5eafea0..afcb9c4f97 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -1783,7 +1783,7 @@ def check_any(*checks: Check) -> Callable[[T], T]: the :func:`check` decorator. Raises - ------- + ------ TypeError A check passed has not been decorated with the :func:`check` decorator. diff --git a/discord/guild.py b/discord/guild.py index c6f3aa4688..1e46e39ea2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -56,8 +56,10 @@ EntitlementOwnerType, NotificationLevel, NSFWLevel, + OnboardingMode, ScheduledEventLocationType, ScheduledEventPrivacyLevel, + SortOrder, VerificationLevel, VideoQualityMode, VoiceRegion, @@ -105,6 +107,7 @@ TextChannel, VoiceChannel, ) + from .onboarding import OnboardingPrompt from .permissions import Permissions from .state import ConnectionState from .template import Template @@ -174,8 +177,6 @@ class Guild(Hashable): The channel that denotes the AFK channel. ``None`` if it doesn't exist. id: :class:`int` The guild's ID. - invites_disabled: :class:`bool` - Indicates if the guild invites are disabled. owner_id: :class:`int` The guild owner's ID. Use :attr:`Guild.owner` instead. unavailable: :class:`bool` @@ -1143,7 +1144,7 @@ def created_at(self) -> datetime.datetime: @property def invites_disabled(self) -> bool: - """Returns a boolean indicating if the guild invites are disabled.""" + """A boolean indicating whether the guild invites are disabled.""" return "INVITES_DISABLED" in self.features def get_member_named(self, name: str, /) -> Member | None: @@ -4352,7 +4353,7 @@ def entitlements( exclude_ended=exclude_ended, ) - def get_sound(self, sound_id: int) -> Soundboard | None: + def get_sound(self, sound_id: int) -> SoundboardSound | None: """Returns a sound with the given ID. .. versionadded :: 2.7 diff --git a/discord/raw_models.py b/discord/raw_models.py index fa543dfb0b..6611953575 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -46,6 +46,7 @@ from .soundboard import PartialSoundboardSound, SoundboardSound from .state import ConnectionState from .threads import Thread + from .types.channel import VoiceChannelEffectSendEvent as VoiceChannelEffectSend from .types.raw_models import ( AuditLogEntryEvent, BulkMessageDeleteEvent, @@ -62,9 +63,6 @@ ThreadMembersUpdateEvent, ThreadUpdateEvent, TypingEvent, - ) - from .types.raw_models import VoiceChannelEffectSendEvent as VoiceChannelEffectSend - from .types.raw_models import ( VoiceChannelStatusUpdateEvent, ) from .types.raw_models import AutoModActionExecutionEvent as AutoModActionExecution diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index 2e2e814289..ad71d7deee 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -28,7 +28,7 @@ dynamic attributes in mind. .. attributetable:: MediaGalleryItem -.. autoclass:: SelectOption +.. autoclass:: MediaGalleryItem :members: .. attributetable:: UnfurledMediaItem diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 61bedcc8dd..7ceb0db38a 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2362,7 +2362,7 @@ of :class:`enum.Enum`. .. attribute:: advanced - Both default channels and questions (``OnboardingPrompt``s) will count towards the Onboarding requirements. + Both default channels and questions (``OnboardingPrompt``\s) will count towards the Onboarding requirements. .. class:: ReactionType @@ -2572,3 +2572,15 @@ of :class:`enum.Enum`. .. attribute:: large The separator uses large padding. + +.. class:: SortOrder + + Used to represent the default sort order for posts in :class:`ForumChannel` and :class:`MediaChannel`. + + .. attribute:: latest_activity + + Sort by latest activity. + + .. attribute:: creation_date + + Sort by post creation date. diff --git a/docs/api/events.rst b/docs/api/events.rst index b1f08b89e7..45d8c1e279 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -1456,9 +1456,9 @@ Soundboard Sound .. versionadded:: 2.7 :param before: The soundboard sound prior to being updated. - :type before: :class:`Soundboard + :type before: :class:`Soundboard` :param after: The soundboard sound after being updated. - :type after: :class:`Soundboard + :type after: :class:`Soundboard` .. function:: on_raw_soundboard_sound_update(after) diff --git a/docs/api/models.rst b/docs/api/models.rst index 7ee6d0f7ef..ef5aaabdbe 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -374,7 +374,7 @@ Interactions :members: Message Components ------------- +------------------ .. attributetable:: Component diff --git a/docs/conf.py b/docs/conf.py index e31db91bed..b5d973b0c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -495,7 +495,7 @@ def write_new(): linkcheck_anchors_ignore_for_url = [r"https://github.com/Delitefully/DiscordLists"] modindex_common_prefix = ["discord."] -# suppress_warnings = ['autosectionlabel.*'] +suppress_warnings = ["autosectionlabel.*"] myst_enable_extensions = [ "amsmath", "attrs_inline", diff --git a/docs/ext/bridge/api.rst b/docs/ext/bridge/api.rst index 370b2f3c5d..63fe6d18c0 100644 --- a/docs/ext/bridge/api.rst +++ b/docs/ext/bridge/api.rst @@ -159,7 +159,7 @@ BridgeContext Subclasses Alias of :data:`typing.Union` [ :class:`.BridgeExtContext`, :class:`.BridgeApplicationContext` ] for typing convenience. Options ------- +------- Shortcut Decorators ~~~~~~~~~~~~~~~~~~~ From a69e3540e198d3fcd1bc36971c5f90c49f2bbd25 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Sat, 30 Aug 2025 21:22:43 +0200 Subject: [PATCH 16/35] fix: improve handling of types in option input_type validation (#2893) * fix: improve handling of Union and tuple types in option input_type validation * fix: Option type handling for multiple Literals Updates Option type resolution to return a Union of Literals when all filtered types are Literal, ensuring correct type annotation for command options. --------- Co-authored-by: Lala Sabathil (cherry picked from commit d0937bef8f19d557d124149571a7e46dad37b1eb) --- discord/commands/options.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/discord/commands/options.py b/discord/commands/options.py index f4b4ad078e..0e93ee7343 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -336,19 +336,23 @@ def _strip_none_type(input_type): if input_type is type(None): raise TypeError("Option type cannot be only NoneType") - if isinstance(input_type, (types.UnionType, tuple)): - args = ( - get_args(input_type) - if isinstance(input_type, types.UnionType) - else input_type - ) + args = () + if isinstance(input_type, types.UnionType): + args = get_args(input_type) + elif getattr(input_type, "__origin__", None) is Union: + args = get_args(input_type) + elif isinstance(input_type, tuple): + args = input_type + + if args: filtered = tuple(t for t in args if t is not type(None)) if not filtered: raise TypeError("Option type cannot be only NoneType") if len(filtered) == 1: return filtered[0] - return filtered - + if all(getattr(t, "__origin__", None) is Literal for t in filtered): + return Union[filtered] + return Union[filtered] return input_type def to_dict(self) -> dict: From 78ae3d46eacb5de28a2fe1f19dc7bae165297b2a Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Sun, 31 Aug 2025 11:58:06 +0200 Subject: [PATCH 17/35] fix: lala (#2896) Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> (cherry picked from commit 8f903aa6819d45810ff17901c185aacb66be229f) --- discord/commands/core.py | 1 + discord/commands/options.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index c4c5099e5b..23d40fcea6 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -780,6 +780,7 @@ def _parse_options(self, params, *, check_params: bool = True) -> list[Option]: if option == inspect.Parameter.empty: option = str + option = Option._strip_none_type(option) if self._is_typing_literal(option): literal_values = get_args(option) if not all(isinstance(v, (str, int, float)) for v in literal_values): diff --git a/discord/commands/options.py b/discord/commands/options.py index 0e93ee7343..70092c616e 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -350,9 +350,9 @@ def _strip_none_type(input_type): raise TypeError("Option type cannot be only NoneType") if len(filtered) == 1: return filtered[0] - if all(getattr(t, "__origin__", None) is Literal for t in filtered): - return Union[filtered] - return Union[filtered] + + return filtered + return input_type def to_dict(self) -> dict: From b907c05e330932e14371bcea5c9248a0baaea6da Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sun, 31 Aug 2025 16:58:08 +0200 Subject: [PATCH 18/35] chore: update badges Signed-off-by: Lala Sabathil (cherry picked from commit 5f33ac83947d0f309dec102c9b36f90d0a0597a5) --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1c198ec2e7..76e9041605 100644 --- a/README.rst +++ b/README.rst @@ -21,11 +21,12 @@ Pycord is a modern, easy to use, feature-rich, and async ready API wrapper for D .. image:: https://img.shields.io/github/v/release/Pycord-Development/pycord?include_prereleases&label=Latest%20Release&logo=github&sort=semver&style=for-the-badge&logoColor=white :target: https://github.com/Pycord-Development/pycord/releases :alt: Latest release - .. image:: https://img.shields.io/discord/881207955029110855?label=discord&style=for-the-badge&logo=discord&color=5865F2&logoColor=white :target: https://pycord.dev/discord :alt: Discord server invite - +.. image:: https://img.shields.io/github/sponsors/Pycord-Development?style=for-the-badge + :target: https://github.com/sponsors/Pycord-Development + :alt: GitHub Sponsors .. image:: https://badges.crowdin.net/badge/dark/crowdin-on-light.png :target: https://translations.pycord.dev/documentation/?utm_source=badge&utm_medium=referral&utm_campaign=badge-add-on :alt: Crowdin | Agile localization for tech companies From cbe39f775a52bf00fc131be2e4c0fa09214a32d4 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sun, 31 Aug 2025 18:12:37 +0200 Subject: [PATCH 19/35] Update supported versions in SECURITY.md Signed-off-by: Lala Sabathil (cherry picked from commit 283277e01d71545434bc610660a75d6b23d61b5b) --- .github/SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 8cf63b4949..d01f9919ed 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -5,7 +5,7 @@ | Version | Supported | | ------- | ------------------ | | 2.x | :white_check_mark: | -| <2.0.0 | :x: | +| <2.6.1 | :x: | ## Reporting a Vulnerability From 22461a22c2d8f91c68e61ba4fda9f13a9c3328ff Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Mon, 1 Sep 2025 23:34:38 +0200 Subject: [PATCH 20/35] fix: usage of SlashCommandOptionType since its a subclass of tuple (#2901) --- discord/commands/options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/commands/options.py b/discord/commands/options.py index 70092c616e..ffc7f449d4 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -333,6 +333,9 @@ def __init__(self, input_type: InputType = str, /, description: str | None = Non @staticmethod def _strip_none_type(input_type): + if isinstance(input_type, SlashCommandOptionType): + return input_type + if input_type is type(None): raise TypeError("Option type cannot be only NoneType") From cf0e203f69dd0c9246a2f65f32bf43bdb26908c1 Mon Sep 17 00:00:00 2001 From: Ice Wolfy <44532864+icebluewolf@users.noreply.github.com> Date: Wed, 3 Sep 2025 19:53:04 +0200 Subject: [PATCH 21/35] feat: Implement with_response For Interaction Callbacks (#2711) Signed-off-by: Ice Wolfy <44532864+Icebluewolf@users.noreply.github.com> Signed-off-by: Lala Sabathil Signed-off-by: Paillat Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: Paillat (cherry picked from commit 3fd5d704786973eb2021c2a80be7ce8218b2eb9c) Signed-off-by: Paillat-dev --- CHANGELOG.md | 21 +++++ discord/interactions.py | 171 +++++++++++++++++++++++++--------- discord/types/interactions.py | 21 +++++ discord/webhook/async_.py | 5 + docs/api/models.rst | 5 + 5 files changed, 179 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c592ebb98c..cb710e49c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,27 @@ possible (see our [Version Guarantees] for more info). These changes are available on the `master` branch, but have not yet been released. +### Added + +- Implemented `with_response` for interaction callbacks, adding + `Interaction.callback.is_loading()` and `Interaction.callback.is_ephemeral()`. + ([#2711](https://github.com/Pycord-Development/pycord/pull/2711)) +- Added `RawMessageUpdateEvent.new_message` - message update events now contain full + message objects ([#2780](https://github.com/Pycord-Development/pycord/pull/2780)) + +### Changed + +### Fixed + +- Manage silence for new SSRC with existing user_id. + ([#2808](https://github.com/Pycord-Development/pycord/pull/2808)) +- Unbound `raw` reference in `parse_message_update` causing errors on message updates. + ([#2905](https://github.com/Pycord-Development/pycord/pull/2905)) + +### Removed + +## [2.7.0rc1] - 2025-08-30 + ⚠️ **This version removes support for Python 3.8.** ⚠️ ### Added diff --git a/discord/interactions.py b/discord/interactions.py index 3e86c9a877..063f28ec36 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -62,6 +62,7 @@ "MessageInteraction", "InteractionMetadata", "AuthorizingIntegrationOwners", + "InteractionCallback", ) if TYPE_CHECKING: @@ -84,7 +85,8 @@ from .state import ConnectionState from .threads import Thread from .types.interactions import Interaction as InteractionPayload - from .types.interactions import InteractionData + from .types.interactions import InteractionCallback as InteractionCallbackPayload + from .types.interactions import InteractionCallbackResponse, InteractionData from .types.interactions import InteractionMetadata as InteractionMetadataPayload from .types.interactions import MessageInteraction as MessageInteractionPayload from .ui.modal import Modal @@ -154,6 +156,11 @@ class Interaction: The context in which this command was executed. .. versionadded:: 2.6 + callback: Optional[:class:`InteractionCallback`] + The callback of the interaction. Contains information about the status of the interaction response. + Will be `None` until the interaction is responded to. + + .. versionadded:: 2.7 command: Optional[:class:`ApplicationCommand`] The command that this interaction belongs to. @@ -190,6 +197,7 @@ class Interaction: "entitlements", "context", "authorizing_integration_owners", + "callback", "command", "view", "modal", @@ -213,6 +221,7 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState): self._state: ConnectionState = state self._session: ClientSession = state.http._HTTPClient__session self._original_response: InteractionMessage | None = None + self.callback: InteractionCallback | None = None self._from_data(data) def _from_data(self, data: InteractionPayload): @@ -443,7 +452,9 @@ async def original_response(self) -> InteractionMessage: # TODO: fix later to not raise? channel = self.channel if channel is None: - raise ClientException("Channel for message could not be resolved") + raise ClientException( + "Channel for message could not be resolved. Please open a issue on GitHub if you encounter this error." + ) adapter = async_context.get() http = self._state.http @@ -828,18 +839,21 @@ async def defer(self, *, ephemeral: bool = False, invisible: bool = True) -> Non if defer_type: adapter = async_context.get() http = parent._state.http - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=defer_type, - data=data, - proxy=http.proxy, - proxy_auth=http.proxy_auth, + callback_response: InteractionCallbackResponse = ( + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=defer_type, + data=data, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + ) ) ) self._responded = True + await self._process_callback_response(callback_response) async def pong(self) -> None: """|coro| @@ -862,17 +876,36 @@ async def pong(self) -> None: if parent.type is InteractionType.ping: adapter = async_context.get() http = parent._state.http - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - type=InteractionResponseType.pong.value, + callback_response: InteractionCallbackResponse = ( + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + type=InteractionResponseType.pong.value, + ) ) ) self._responded = True + await self._process_callback_response(callback_response) + + async def _process_callback_response( + self, callback_response: InteractionCallbackResponse + ): + if callback_response.get("resource", {}).get("message"): + # TODO: fix later to not raise? + channel = self._parent.channel + if channel is None: + raise ClientException( + "Channel for message could not be resolved. Please open a issue on GitHub if you encounter this error." + ) + state = _InteractionMessageState(self._parent, self._parent._state) + message = InteractionMessage(state=state, channel=channel, data=callback_response["resource"]["message"]) # type: ignore + self._parent._original_response = message + + self._parent.callback = InteractionCallback(callback_response["interaction"]) async def send_message( self, @@ -1008,16 +1041,18 @@ async def send_message( adapter = async_context.get() http = parent._state.http try: - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.channel_message.value, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - data=payload, - files=files, + callback_response: InteractionCallbackResponse = ( + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.channel_message.value, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + data=payload, + files=files, + ) ) ) finally: @@ -1034,6 +1069,7 @@ async def send_message( self._parent._state.store_view(view) self._responded = True + await self._process_callback_response(callback_response) if delete_after is not None: await self._parent.delete_original_response(delay=delete_after) return self._parent @@ -1165,16 +1201,18 @@ async def edit_message( adapter = async_context.get() http = parent._state.http try: - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.message_update.value, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - data=payload, - files=files, + callback_response: InteractionCallbackResponse = ( + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.message_update.value, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + data=payload, + files=files, + ) ) ) finally: @@ -1187,6 +1225,7 @@ async def edit_message( state.store_view(view, message_id) self._responded = True + await self._process_callback_response(callback_response) if delete_after is not None: await self._parent.delete_original_response(delay=delete_after) @@ -1222,7 +1261,7 @@ async def send_autocomplete_result( adapter = async_context.get() http = parent._state.http - await self._locked_response( + callback_response: InteractionCallbackResponse = await self._locked_response( adapter.create_interaction_response( parent.id, parent.token, @@ -1235,6 +1274,7 @@ async def send_autocomplete_result( ) self._responded = True + await self._process_callback_response(callback_response) async def send_modal(self, modal: Modal) -> Interaction: """|coro| @@ -1261,7 +1301,7 @@ async def send_modal(self, modal: Modal) -> Interaction: payload = modal.to_dict() adapter = async_context.get() http = parent._state.http - await self._locked_response( + callback_response: InteractionCallbackResponse = await self._locked_response( adapter.create_interaction_response( parent.id, parent.token, @@ -1273,6 +1313,7 @@ async def send_modal(self, modal: Modal) -> Interaction: ) ) self._responded = True + await self._process_callback_response(callback_response) self._parent._state.store_modal(modal, self._parent.user.id) return self._parent @@ -1300,7 +1341,7 @@ async def premium_required(self) -> Interaction: adapter = async_context.get() http = parent._state.http - await self._locked_response( + callback_response: InteractionCallbackResponse = await self._locked_response( adapter.create_interaction_response( parent.id, parent.token, @@ -1311,9 +1352,10 @@ async def premium_required(self) -> Interaction: ) ) self._responded = True + await self._process_callback_response(callback_response) return self._parent - async def _locked_response(self, coro: Coroutine[Any, Any, Any]) -> None: + async def _locked_response(self, coro: Coroutine[Any, Any, Any]) -> Any: """|coro| Wraps a response and makes sure that it's locked while executing. @@ -1323,16 +1365,24 @@ async def _locked_response(self, coro: Coroutine[Any, Any, Any]) -> None: coro: Coroutine[Any] The coroutine to wrap. + Returns + ------- + Any + The result of the coroutine. + Raises ------ InteractionResponded This interaction has already been responded to before. + + .. versionchanged:: 2.7 + Return the result of the coroutine """ async with self._response_lock: if self.is_done(): coro.close() # cleanup un-awaited coroutine raise InteractionResponded(self._parent) - await coro + return await coro class _InteractionMessageState: @@ -1646,3 +1696,36 @@ def guild(self) -> Guild | None: if not self.guild_id: return None return self._state._get_guild(self.guild_id) + + +class InteractionCallback: + """Information about the status of the interaction response. + + .. versionadded:: 2.7 + """ + + def __init__(self, data: InteractionCallbackPayload): + self._response_message_loading: bool = data.get( + "response_message_loading", False + ) + self._response_message_ephemeral: bool = data.get( + "response_message_ephemeral", False + ) + + def __repr__(self): + return ( + f"" + ) + + def is_loading(self) -> bool: + """Indicates whether the response message is in a loading state.""" + return self._response_message_loading + + def is_ephemeral(self) -> bool: + """Indicates whether the response message is ephemeral. + + This might be useful for determining if the message was forced to be ephemeral. + """ + return self._response_message_ephemeral diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 4f5bfbd64f..235d9722c9 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -261,3 +261,24 @@ class EditApplicationCommand(TypedDict): _StringApplicationIntegrationType = Literal["0", "1"] AuthorizingIntegrationOwners = Dict[_StringApplicationIntegrationType, Snowflake] + + +class InteractionCallbackResponse(TypedDict): + interaction: InteractionCallback + resource: NotRequired[InteractionCallbackResource] + + +class InteractionCallback(TypedDict): + id: Snowflake + type: InteractionType + activity_instance_id: NotRequired[str] + response_message_id: NotRequired[Snowflake] + response_message_loading: NotRequired[bool] + response_message_ephemeral: NotRequired[bool] + + +class InteractionCallbackResource(TypedDict): + type: InteractionResponseType + # This is not fully typed as activities are out of scope + activity_instance: NotRequired[dict] + message: NotRequired[Message] diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 9e154a9136..a11ac45913 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -530,6 +530,10 @@ def create_interaction_response( webhook_token=token, ) + params: dict[str, Any] = { + "with_response": "true", + } + return self.request( route, session=session, @@ -537,6 +541,7 @@ def create_interaction_response( proxy_auth=proxy_auth, files=files, multipart=form, + params=params, ) def get_original_interaction_response( diff --git a/docs/api/models.rst b/docs/api/models.rst index ef5aaabdbe..5075d7084b 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -373,6 +373,11 @@ Interactions .. autoclass:: AuthorizingIntegrationOwners() :members: +.. attributetable:: InteractionCallback + +.. autoclass:: InteractionCallback() + :members: + Message Components ------------------ From 062d571c666bebda7f5112baa260f4e5d6a3a6a8 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+soheab@users.noreply.github.com> Date: Wed, 10 Sep 2025 22:47:24 +0200 Subject: [PATCH 22/35] =?UTF-8?q?feat:=20=E2=9C=A8=20Add=20support=20for?= =?UTF-8?q?=20new=20member=20fields=20for=20bots=20(#2910)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for new current member fields https://github.com/discord/discord-api-docs/pull/7807 * chore: add changelog entry * style(pre-commit): auto fixes from pre-commit.com hooks * Update CHANGELOG.md Co-authored-by: plun1331 Signed-off-by: Soheab <33902984+Soheab@users.noreply.github.com> * style(pre-commit): auto fixes from pre-commit.com hooks * chore: do requested change https://github.com/Pycord-Development/pycord/pull/2910#discussion_r2324401017 * Update discord/member.py Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> Signed-off-by: Soheab <33902984+Soheab@users.noreply.github.com> --------- Signed-off-by: Soheab <33902984+Soheab@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: plun1331 Co-authored-by: Paillat Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> (cherry picked from commit dda0daaaad95a607d91f82325091533c80ee2a97) Signed-off-by: Paillat-dev --- CHANGELOG.md | 3 +++ discord/http.py | 32 ------------------------ discord/member.py | 64 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb710e49c4..8e2ce9b183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2711](https://github.com/Pycord-Development/pycord/pull/2711)) - Added `RawMessageUpdateEvent.new_message` - message update events now contain full message objects ([#2780](https://github.com/Pycord-Development/pycord/pull/2780)) +- Added support for setting guild-specific `avatar`, `banner`, and `bio` for the bot + user through `Member.edit`. + ([#2908](https://github.com/Pycord-Development/pycord/pull/2908)) ### Changed diff --git a/discord/http.py b/discord/http.py index 8a4867934e..f43b041861 100644 --- a/discord/http.py +++ b/discord/http.py @@ -972,38 +972,6 @@ def guild_voice_state( def edit_profile(self, payload: dict[str, Any]) -> Response[user.User]: return self.request(Route("PATCH", "/users/@me"), json=payload) - def change_my_nickname( - self, - guild_id: Snowflake, - nickname: str, - *, - reason: str | None = None, - ) -> Response[member.Nickname]: - r = Route("PATCH", "/guilds/{guild_id}/members/@me", guild_id=guild_id) - payload = { - "nick": nickname, - } - return self.request(r, json=payload, reason=reason) - - def change_nickname( - self, - guild_id: Snowflake, - user_id: Snowflake, - nickname: str, - *, - reason: str | None = None, - ) -> Response[member.Member]: - r = Route( - "PATCH", - "/guilds/{guild_id}/members/{user_id}", - guild_id=guild_id, - user_id=user_id, - ) - payload = { - "nick": nickname, - } - return self.request(r, json=payload, reason=reason) - def edit_my_voice_state(self, guild_id: Snowflake, payload: dict[str, Any]) -> Response[None]: r = Route("PATCH", "/guilds/{guild_id}/voice-states/@me", guild_id=guild_id) return self.request(r, json=payload) diff --git a/discord/member.py b/discord/member.py index 40f745d979..e8ac415c0c 100644 --- a/discord/member.py +++ b/discord/member.py @@ -39,6 +39,7 @@ from .asset import Asset from .colour import Colour from .enums import Status, try_enum +from .errors import InvalidArgument from .flags import MemberFlags from .object import Object from .permissions import Permissions @@ -752,6 +753,9 @@ async def edit( reason: str | None = None, communication_disabled_until: datetime.datetime | None | utils.Undefined = MISSING, bypass_verification: bool | None | utils.Undefined = MISSING, + banner: bytes | None = MISSING, + avatar: bytes | None = MISSING, + bio: str | None = MISSING, ) -> Member | None: """|coro| @@ -787,6 +791,14 @@ async def edit( - Client has ALL THREE of :attr:`Permissions.moderate_members`, :attr:`Permissions.kick_members`, and :attr:`Permissions.ban_members` + .. note:: + + The following parameters are only available when editing the bot's own member: + + - ``avatar`` + - ``banner`` + - ``bio`` + All parameters are optional. .. versionchanged:: 1.1 @@ -824,6 +836,26 @@ async def edit( Indicates if the member should bypass the guild's verification requirements. .. versionadded:: 2.6 + banner: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the banner. + Could be ``None`` to denote removal of the banner. + + This is only available when editing the bot's own member (i.e. :attr:`Guild.me`). + + .. versionadded:: 2.7 + avatar: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the avatar. + Could be ``None`` to denote removal of the avatar. + + This is only available when editing the bot's own member (i.e. :attr:`Guild.me`). + + .. versionadded:: 2.7 + bio: Optional[:class:`str`] + The new bio for the member. Could be ``None`` to denote removal of the bio. + + This is only available when editing the bot's own member (i.e. :attr:`Guild.me`). + + .. versionadded:: 2.7 Returns ------- @@ -837,16 +869,19 @@ async def edit( You do not have the proper permissions to the action requested. HTTPException The operation failed. + InvalidArgument + You tried to edit the avatar, banner, or bio of a member that is not the bot. """ http = self._state.http guild_id = self.guild.id me = self._state.self_id == self.id payload: dict[str, Any] = {} + bot_payload: dict[str, Any] = {} if nick is not MISSING: nick = nick or "" if me: - await http.change_my_nickname(guild_id, nick, reason=reason) + bot_payload["nick"] = nick else: payload["nick"] = nick @@ -889,9 +924,34 @@ async def edit( flags.bypasses_verification = bypass_verification payload["flags"] = flags.value + if avatar is not MISSING: + if avatar is None: + bot_payload["avatar"] = None + else: + bot_payload["avatar"] = utils._bytes_to_base64_data(avatar) + + if banner is not MISSING: + if banner is None: + bot_payload["banner"] = None + else: + bot_payload["banner"] = utils._bytes_to_base64_data(banner) + + if bio is not MISSING: + bot_payload["bio"] = bio or "" + + if bot_payload and not me: + raise InvalidArgument( + "Can only edit avatar, banner, or bio for the bot's member." + ) + if payload: data = await http.edit_member(guild_id, self.id, reason=reason, **payload) - return Member(data=data, guild=self.guild, state=self._state) + elif bot_payload: + data = await http.edit_member(guild_id, "@me", reason=reason, **bot_payload) + else: + return None + + return Member(data=data, guild=self.guild, state=self._state) async def timeout(self, until: datetime.datetime | None, *, reason: str | None = None) -> None: """|coro| From a13260c961345a1d43117f14dc02987efc863513 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+lumabots@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:10:35 +0200 Subject: [PATCH 23/35] fix: typehint issue (#2917) Update role.py Co-authored-by: Paillat (cherry picked from commit 02865fb820dc9f8d46deab7bfb2d624137bc4882) Signed-off-by: Paillat-dev --- discord/role.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/role.py b/discord/role.py index b0d880c1fb..f65bd9fad5 100644 --- a/discord/role.py +++ b/discord/role.py @@ -381,7 +381,7 @@ def _update(self, data: RolePayload): self._permissions: int = int(data.get("permissions", 0)) self.position: int = data.get("position", 0) self._colour: int = data.get("color", 0) - self.colours: RoleColours | None = RoleColours._from_payload(data["colors"]) + self.colours: RoleColours = RoleColours._from_payload(data["colors"]) self.hoist: bool = data.get("hoist", False) self.managed: bool = data.get("managed", False) self.mentionable: bool = data.get("mentionable", False) @@ -543,8 +543,8 @@ async def edit( permissions: Permissions | utils.Undefined = MISSING, colour: Colour | int | utils.Undefined = MISSING, color: Colour | int | utils.Undefined = MISSING, - colours: RoleColours | None | utils.Undefined = MISSING, - colors: RoleColours | None | utils.Undefined = MISSING, + colours: RoleColours | utils.Undefined = MISSING, + colors: RoleColours | utils.Undefined = MISSING, holographic: bool | utils.Undefined = MISSING, hoist: bool | utils.Undefined = MISSING, mentionable: bool | utils.Undefined = MISSING, From b9dead965f18f082d53c39df32384cf13e3349ee Mon Sep 17 00:00:00 2001 From: Ice Wolfy <44532864+icebluewolf@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:11:43 +0200 Subject: [PATCH 24/35] fix: Remove Extra Bytes Added By Discord Before OPUS Decoding (#2925) * fix: Remove Extra Bytes Added By Discord Before OPUS Decoding * chore: Changelog (cherry picked from commit e5739a28ea27675067dbd606082135eb3b87287d) Signed-off-by: Paillat-dev --- CHANGELOG.md | 24 +++++++++++++++++++++++- discord/voice_client.py | 6 ++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2ce9b183..3098be39c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,14 @@ These changes are available on the `master` branch, but have not yet been releas - Added support for setting guild-specific `avatar`, `banner`, and `bio` for the bot user through `Member.edit`. ([#2908](https://github.com/Pycord-Development/pycord/pull/2908)) +- Added support for select default values. + ([#2899](https://github.com/Pycord-Development/pycord/pull/2899)) + - Adds a new generic parameter to selects to type `ui.Select.values` return type. + - Adds `SelectDefaultValue` object to create select default values. + - Adds `SelectDefaultValueType` enum. + - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the + different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, + `ui.MentionableSelect`, and `ui.ChannelSelect`. ### Changed @@ -29,6 +37,14 @@ These changes are available on the `master` branch, but have not yet been releas ([#2808](https://github.com/Pycord-Development/pycord/pull/2808)) - Unbound `raw` reference in `parse_message_update` causing errors on message updates. ([#2905](https://github.com/Pycord-Development/pycord/pull/2905)) +- `view=None` in various methods causing an AttributeError. + ([#2915](https://github.com/Pycord-Development/pycord/pull/2915)) +- `View.message` being `None` when it had not been interacted with yet. + ([#2916](https://github.com/Pycord-Development/pycord/pull/2916)) +- Fixed a crash when processing message edit events while message cache was disabled. + ([#2924](https://github.com/Pycord-Development/pycord/pull/2924)) +- Fixed OPUS Decode Error when recording audio. + ([#2925](https://github.com/Pycord-Development/pycord/pull/2925)) ### Removed @@ -106,10 +122,14 @@ These changes are available on the `master` branch, but have not yet been releas ([#2826](https://github.com/Pycord-Development/pycord/pull/2826)) - Added `Interaction.attachment_size_limit`. ([#2854](https://github.com/Pycord-Development/pycord/pull/2854)) +- Added support for selects and text displays in modals. + ([#2858](https://github.com/Pycord-Development/pycord/pull/2858)) - Added `AuditLogDiff.communication_disabled_until`. ([#2883](https://github.com/Pycord-Development/pycord/pull/2883)) - Added `discord.User.primary_guild` and the `PrimaryGuild` class. ([#2876](https://github.com/Pycord-Development/pycord/pull/2876)) +- Added `get_component` to `Message`, `Section`, `Container` and `ActionRow`. + ([#2849](https://github.com/Pycord-Development/pycord/pull/2849)) ### Fixed @@ -1136,7 +1156,9 @@ These changes are available on the `master` branch, but have not yet been releas - Fix py3.10 UnionType checks issue. ([#1240](https://github.com/Pycord-Development/pycord/pull/1240)) -[unreleased]: https://github.com/Pycord-Development/pycord/compare/v2.6.0...HEAD +[unreleased]: https://github.com/Pycord-Development/pycord/compare/v2.7.0rc1...HEAD +[2.7.0rc1]: https://github.com/Pycord-Development/pycord/compare/v2.6.0...v2.7.0rc1 +[2.6.1]: https://github.com/Pycord-Development/pycord/compare/v2.6.0...v2.6.1 [2.6.0]: https://github.com/Pycord-Development/pycord/compare/v2.5.0...v2.6.0 [2.5.0]: https://github.com/Pycord-Development/pycord/compare/v2.4.1...v2.5.0 [2.4.1]: https://github.com/Pycord-Development/pycord/compare/v2.4.0...v2.4.1 diff --git a/discord/voice_client.py b/discord/voice_client.py index d8e40da852..f381e51550 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -634,7 +634,9 @@ def _decrypt_aead_xchacha20_poly1305_rtpsize(self, header, data): nonce[:4] = data[-4:] data = data[:-4] - return self.strip_header_ext(box.decrypt(bytes(data), bytes(header), bytes(nonce))) + r =box.decrypt(bytes(data), bytes(header), bytes(nonce))# Discord adds 8 bytes of data before the opus data. + # This can be removed, and at this time, discarded as it is unclear what they are for. + return r[8:] @staticmethod def strip_header_ext(data): @@ -750,7 +752,7 @@ def unpack_audio(self, data): data: :class:`bytes` Bytes received by Discord via the UDP connection used for sending and receiving voice data. """ - if data[1] != 0x78: + if data[1] & 0x78 != 0x78: # We Should Ignore Any Payload Types We Do Not Understand # Ref RFC 3550 5.1 payload type # At Some Point We Noted That We Should Ignore Only Types 200 - 204 inclusive. From 796465c245249f2e528da2d70f023cd3b24d1c26 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Thu, 9 Oct 2025 11:57:22 +0200 Subject: [PATCH 25/35] :rotating_light: :art: Format code and fix linter warnings Signed-off-by: Paillat-dev --- discord/asset.py | 8 +- discord/audit_logs.py | 4 +- discord/channel.py | 12 +- discord/client.py | 5 +- discord/guild.py | 36 ++--- discord/http.py | 24 +--- discord/interactions.py | 94 ++++++------ discord/iterators.py | 6 +- discord/member.py | 4 +- discord/message.py | 4 +- discord/primary_guild.py | 10 +- discord/soundboard.py | 36 ++--- discord/state.py | 10 +- discord/threads.py | 4 +- discord/user.py | 4 +- discord/utils/private.py | 2 +- discord/voice_client.py | 2 +- examples/soundboard.py | 36 ++--- uv.lock | 302 +-------------------------------------- 19 files changed, 96 insertions(+), 507 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index 6387c23e61..afdd47f3aa 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -207,9 +207,7 @@ def _from_avatar_decoration(cls, state, user_id: int, avatar_decoration: str) -> ) @classmethod - def _from_user_primary_guild_tag( - cls, state: ConnectionState, identity_guild_id: int, badge_id: str - ) -> Asset: + def _from_user_primary_guild_tag(cls, state: ConnectionState, identity_guild_id: int, badge_id: str) -> Asset: """Creates an Asset for a user's primary guild (tag) badge. Parameters @@ -274,9 +272,7 @@ def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asse ) @classmethod - def _from_collectible( - cls, state: ConnectionState, asset: str, animated: bool = False - ) -> Asset: + def _from_collectible(cls, state: ConnectionState, asset: str, animated: bool = False) -> Asset: name = "static.png" if not animated else "asset.webm" return cls( state, diff --git a/discord/audit_logs.py b/discord/audit_logs.py index b5ff2efba4..097685565f 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -196,9 +196,7 @@ def _transform_trigger_metadata( return AutoModTriggerMetadata.from_dict(data) -def _transform_communication_disabled_until( - entry: AuditLogEntry, data: str -) -> datetime.datetime | None: +def _transform_communication_disabled_until(entry: AuditLogEntry, data: str) -> datetime.datetime | None: if data: return datetime.datetime.fromisoformat(data) return None diff --git a/discord/channel.py b/discord/channel.py index 44a2117a43..d85b2181f9 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -49,14 +49,12 @@ InviteTarget, SortOrder, StagePrivacyLevel, -) -from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum -from .enums import ( VideoQualityMode, VoiceChannelEffectAnimationType, VoiceRegion, try_enum, ) +from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum from .errors import ClientException, InvalidArgument from .file import File from .flags import ChannelFlags, MessageFlags @@ -1062,9 +1060,7 @@ async def edit( sync_permissions: bool = ..., category: CategoryChannel | None = ..., slowmode_delay: int = ..., - default_auto_archive_duration: ( - ThreadArchiveDuration | ThreadArchiveDurationEnum - ) = ..., + default_auto_archive_duration: (ThreadArchiveDuration | ThreadArchiveDurationEnum) = ..., default_thread_slowmode_delay: int = ..., default_sort_order: SortOrder = ..., default_reaction_emoji: GuildEmoji | int | str | None = ..., @@ -2801,9 +2797,7 @@ def __init__(self, *, state: ConnectionState, guild: Guild, data: CategoryChanne self._update(guild, data) def __repr__(self) -> str: - return ( - f" None: # This data will always exist diff --git a/discord/client.py b/discord/client.py index c5de967f6e..225904bd50 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2325,7 +2325,4 @@ async def fetch_default_sounds(self) -> list[SoundboardSound]: The bot's default sounds. """ data = await self._connection.http.get_default_sounds() - return [ - SoundboardSound(http=self.http, state=self._connection, data=s) - for s in data - ] + return [SoundboardSound(http=self.http, state=self._connection, data=s) for s in data] diff --git a/discord/guild.py b/discord/guild.py index 1e46e39ea2..9a1aacb6e1 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -292,21 +292,11 @@ class Guild(Hashable): ) _PREMIUM_GUILD_LIMITS: ClassVar[dict[int | None, _GuildLimit]] = { - None: _GuildLimit( - emoji=50, stickers=5, soundboard=8, bitrate=96e3, filesize=10_485_760 - ), - 0: _GuildLimit( - emoji=50, stickers=5, soundboard=8, bitrate=96e3, filesize=10_485_760 - ), - 1: _GuildLimit( - emoji=100, stickers=15, soundboard=24, bitrate=128e3, filesize=10_485_760 - ), - 2: _GuildLimit( - emoji=150, stickers=30, soundboard=36, bitrate=256e3, filesize=52_428_800 - ), - 3: _GuildLimit( - emoji=250, stickers=60, soundboard=48, bitrate=384e3, filesize=104_857_600 - ), + None: _GuildLimit(emoji=50, stickers=5, soundboard=8, bitrate=96e3, filesize=10_485_760), + 0: _GuildLimit(emoji=50, stickers=5, soundboard=8, bitrate=96e3, filesize=10_485_760), + 1: _GuildLimit(emoji=100, stickers=15, soundboard=24, bitrate=128e3, filesize=10_485_760), + 2: _GuildLimit(emoji=150, stickers=30, soundboard=36, bitrate=256e3, filesize=52_428_800), + 3: _GuildLimit(emoji=250, stickers=60, soundboard=48, bitrate=384e3, filesize=104_857_600), } def __init__(self, *, data: GuildPayload, state: ConnectionState): @@ -650,9 +640,7 @@ async def create_sound( else: payload["emoji_id"] = partial_emoji.id - data = await self._state.http.create_guild_sound( - self.id, reason=reason, **payload - ) + data = await self._state.http.create_guild_sound(self.id, reason=reason, **payload) return SoundboardSound( state=self._state, http=self._state.http, @@ -951,9 +939,7 @@ def soundboard_limit(self) -> int: .. versionadded:: 2.7 """ more_soundboard = 48 if "MORE_SOUNDBOARD" in self.features else 0 - return max( - more_soundboard, self._PREMIUM_GUILD_LIMITS[self.premium_tier].soundboard - ) + return max(more_soundboard, self._PREMIUM_GUILD_LIMITS[self.premium_tier].soundboard) @property def bitrate_limit(self) -> int: @@ -1734,9 +1720,7 @@ async def create_forum_channel( options["available_tags"] = [tag.to_dict() for tag in available_tags] if default_sort_order is not MISSING: - options["default_sort_order"] = ( - default_sort_order.value if default_sort_order else None - ) + options["default_sort_order"] = default_sort_order.value if default_sort_order else None if default_thread_slowmode_delay is not MISSING: options["default_thread_slowmode_delay"] = default_thread_slowmode_delay @@ -1757,9 +1741,7 @@ async def create_forum_channel( raise InvalidArgument("default_reaction_emoji must be of type: GuildEmoji | int | str | None") options["default_reaction_emoji"] = ( - default_reaction_emoji._to_forum_reaction_payload() - if default_reaction_emoji - else None + default_reaction_emoji._to_forum_reaction_payload() if default_reaction_emoji else None ) data = await self._create_channel( diff --git a/discord/http.py b/discord/http.py index f43b041861..9d015b6be1 100644 --- a/discord/http.py +++ b/discord/http.py @@ -880,9 +880,7 @@ def pins_from( return self.request(r, params=params) - def legacy_pins_from( - self, channel_id: Snowflake - ) -> Response[list[message.Message]]: + def legacy_pins_from(self, channel_id: Snowflake) -> Response[list[message.Message]]: return self.request(Route("GET", "/channels/{channel_id}/pins", channel_id=channel_id)) # Member management @@ -3028,9 +3026,7 @@ async def get_bot_gateway(self, *, encoding: str = "json", zlib: bool = True) -> def get_user(self, user_id: Snowflake) -> Response[user.User]: return self.request(Route("GET", "/users/{user_id}", user_id=user_id)) - def delete_sound( - self, sound: SoundboardSound, *, reason: str | None - ) -> Response[None]: + def delete_sound(self, sound: SoundboardSound, *, reason: str | None) -> Response[None]: return self.request( Route( "DELETE", @@ -3063,16 +3059,10 @@ def create_guild_sound( reason=reason, ) - def get_all_guild_sounds( - self, guild_id: Snowflake - ) -> Response[list[SoundboardSoundPayload]]: - return self.request( - Route("GET", "/guilds/{guild_id}/soundboard-sounds", guild_id=guild_id) - ) + def get_all_guild_sounds(self, guild_id: Snowflake) -> Response[list[SoundboardSoundPayload]]: + return self.request(Route("GET", "/guilds/{guild_id}/soundboard-sounds", guild_id=guild_id)) - def get_guild_sound( - self, guild_id: Snowflake, sound_id: Snowflake - ) -> Response[SoundboardSoundPayload]: + def get_guild_sound(self, guild_id: Snowflake, sound_id: Snowflake) -> Response[SoundboardSoundPayload]: return self.request( Route( "GET", @@ -3105,9 +3095,7 @@ def edit_guild_sound( reason=reason, ) - def send_soundboard_sound( - self, channel_id: int, sound: PartialSoundboardSound - ) -> Response[None]: + def send_soundboard_sound(self, channel_id: int, sound: PartialSoundboardSound) -> Response[None]: payload = { "sound_id": sound.id, } diff --git a/discord/interactions.py b/discord/interactions.py index 063f28ec36..455b70b45a 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -839,17 +839,15 @@ async def defer(self, *, ephemeral: bool = False, invisible: bool = True) -> Non if defer_type: adapter = async_context.get() http = parent._state.http - callback_response: InteractionCallbackResponse = ( - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=defer_type, - data=data, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - ) + callback_response: InteractionCallbackResponse = await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=defer_type, + data=data, + proxy=http.proxy, + proxy_auth=http.proxy_auth, ) ) self._responded = True @@ -876,24 +874,20 @@ async def pong(self) -> None: if parent.type is InteractionType.ping: adapter = async_context.get() http = parent._state.http - callback_response: InteractionCallbackResponse = ( - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - type=InteractionResponseType.pong.value, - ) + callback_response: InteractionCallbackResponse = await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + type=InteractionResponseType.pong.value, ) ) self._responded = True await self._process_callback_response(callback_response) - async def _process_callback_response( - self, callback_response: InteractionCallbackResponse - ): + async def _process_callback_response(self, callback_response: InteractionCallbackResponse): if callback_response.get("resource", {}).get("message"): # TODO: fix later to not raise? channel = self._parent.channel @@ -1041,18 +1035,16 @@ async def send_message( adapter = async_context.get() http = parent._state.http try: - callback_response: InteractionCallbackResponse = ( - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.channel_message.value, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - data=payload, - files=files, - ) + callback_response: InteractionCallbackResponse = await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.channel_message.value, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + data=payload, + files=files, ) ) finally: @@ -1201,18 +1193,16 @@ async def edit_message( adapter = async_context.get() http = parent._state.http try: - callback_response: InteractionCallbackResponse = ( - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.message_update.value, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - data=payload, - files=files, - ) + callback_response: InteractionCallbackResponse = await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.message_update.value, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + data=payload, + files=files, ) ) finally: @@ -1705,12 +1695,8 @@ class InteractionCallback: """ def __init__(self, data: InteractionCallbackPayload): - self._response_message_loading: bool = data.get( - "response_message_loading", False - ) - self._response_message_ephemeral: bool = data.get( - "response_message_ephemeral", False - ) + self._response_message_loading: bool = data.get("response_message_loading", False) + self._response_message_ephemeral: bool = data.get("response_message_ephemeral", False) def __repr__(self): return ( diff --git a/discord/iterators.py b/discord/iterators.py index 069fc7598c..a8957ee716 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -1194,8 +1194,8 @@ async def next(self) -> MessagePin: try: return self.queue.get_nowait() - except asyncio.QueueEmpty: - raise NoMoreItems() + except asyncio.QueueEmpty as e: + raise NoMoreItems() from e @staticmethod def get_last_pinned(data: MessagePinPayload) -> str: @@ -1226,7 +1226,7 @@ async def fill_queue(self) -> None: self.before = self.update_before(pins[-1]) def create_pin(self, data: MessagePinPayload) -> MessagePin: - from .message import MessagePin + from .message import MessagePin # noqa: PLC0415 return MessagePin(state=self.channel._state, channel=self.channel, data=data) diff --git a/discord/member.py b/discord/member.py index e8ac415c0c..dbb1879721 100644 --- a/discord/member.py +++ b/discord/member.py @@ -940,9 +940,7 @@ async def edit( bot_payload["bio"] = bio or "" if bot_payload and not me: - raise InvalidArgument( - "Can only edit avatar, banner, or bio for the bot's member." - ) + raise InvalidArgument("Can only edit avatar, banner, or bio for the bot's member.") if payload: data = await http.edit_member(guild_id, self.id, reason=reason, **payload) diff --git a/discord/message.py b/discord/message.py index cf9899e767..9c140538ca 100644 --- a/discord/message.py +++ b/discord/message.py @@ -784,9 +784,7 @@ def __init__( ): self._state: ConnectionState = state self._pinned_at: datetime.datetime = utils.parse_time(data["pinned_at"]) - self._message: Message = state.create_message( - channel=channel, data=data["message"] - ) + self._message: Message = state.create_message(channel=channel, data=data["message"]) @property def message(self) -> Message: diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 27edba7e97..a7f8002382 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -51,13 +51,11 @@ class PrimaryGuild: """ def __init__(self, data: PrimaryGuildPayload, state: "ConnectionState") -> None: - self.identity_guild_id: int | None = ( - int(data.get("identity_guild_id") or 0) or None - ) + self.identity_guild_id: int | None = int(data.get("identity_guild_id") or 0) or None self.identity_enabled: bool | None = data.get("identity_enabled", None) self.tag: str | None = data.get("tag", None) self._badge: str | None = data.get("badge", None) - self._state: "ConnectionState" = state + self._state: ConnectionState = state def __repr__(self) -> str: return f"" @@ -70,6 +68,4 @@ def badge(self) -> Asset | None: """ if self._badge is None: return None - return Asset._from_user_primary_guild_tag( - self._state, self.identity_guild_id, self._badge - ) + return Asset._from_user_primary_guild_tag(self._state, self.identity_guild_id, self._badge) diff --git a/discord/soundboard.py b/discord/soundboard.py index 6fae037090..7566a5d07f 100644 --- a/discord/soundboard.py +++ b/discord/soundboard.py @@ -76,38 +76,26 @@ def __init__( self._state = state self._from_data(data) - def _from_data( - self, data: SoundboardSoundPayload | VoiceChannelEffectSendEventPayload - ) -> None: + def _from_data(self, data: SoundboardSoundPayload | VoiceChannelEffectSendEventPayload) -> None: self.id = int(data.get("sound_id", 0)) - self.volume = ( - float(data.get("volume", 0) or data.get("sound_volume", 0)) or None - ) + self.volume = float(data.get("volume", 0) or data.get("sound_volume", 0)) or None self.emoji = None - if raw_emoji := data.get( - "emoji" - ): # From gateway event (VoiceChannelEffectSendEventPayload) + if raw_emoji := data.get("emoji"): # From gateway event (VoiceChannelEffectSendEventPayload) self.emoji = PartialEmoji.from_dict(raw_emoji) - elif data.get("emoji_name") or data.get( - "emoji_id" - ): # From HTTP response (SoundboardSoundPayload) + elif data.get("emoji_name") or data.get("emoji_id"): # From HTTP response (SoundboardSoundPayload) self.emoji = PartialEmoji( name=data.get("emoji_name"), id=int(data.get("emoji_id", 0) or 0) or None, ) @override - def __eq__( - self, other: PartialSoundboardSound - ) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] + def __eq__(self, other: PartialSoundboardSound) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] if isinstance(other, self, __class__): return self.id == other.id return NotImplemented @override - def __ne__( - self, other: PartialSoundboardSound - ) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] + def __ne__(self, other: PartialSoundboardSound) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] return not self.__eq__(other) @property @@ -162,9 +150,7 @@ def __init__( super().__init__(data, state, http) @override - def _from_data( - self, data: SoundboardSoundPayload - ) -> None: # pyright: ignore[reportIncompatibleMethodOverride] + def _from_data(self, data: SoundboardSoundPayload) -> None: # pyright: ignore[reportIncompatibleMethodOverride] super()._from_data(data) self.name = data["name"] self.available: bool = data["available"] @@ -178,9 +164,7 @@ def guild(self) -> Guild | None: return self._state._get_guild(self.guild_id) if self.guild_id else None @override - def __eq__( - self, other: SoundboardSound - ) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] + def __eq__(self, other: SoundboardSound) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] return isinstance(other, SoundboardSound) and self.__dict__ == other.__dict__ @property @@ -242,9 +226,7 @@ def edit( else: payload["emoji_id"] = partial_emoji.id - return self._http.edit_guild_sound( - self.guild_id, self.id, reason=reason, **payload - ) + return self._http.edit_guild_sound(self.guild_id, self.id, reason=reason, **payload) def delete(self, *, reason: str | None = None) -> Coroutine[Any, Any, None]: """Deletes the sound. diff --git a/discord/state.py b/discord/state.py index fd4934eacf..1784a9a20e 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1908,11 +1908,7 @@ def _update_sound(self, sound: SoundboardSound) -> SoundboardSound | None: def parse_soundboard_sounds(self, data) -> None: guild_id = int(data["guild_id"]) for sound_data in data["soundboard_sounds"]: - self._add_sound( - SoundboardSound( - state=self, http=self.http, data=sound_data, guild_id=guild_id - ) - ) + self._add_sound(SoundboardSound(state=self, http=self.http, data=sound_data, guild_id=guild_id)) def parse_guild_soundboard_sounds_update(self, data): before_sounds = [] @@ -1943,9 +1939,7 @@ def parse_guild_soundboard_sound_delete(self, data): if sound is not None: self._remove_sound(sound) self.dispatch("soundboard_sound_delete", sound) - self.dispatch( - "raw_soundboard_sound_delete", RawSoundboardSoundDeleteEvent(data) - ) + self.dispatch("raw_soundboard_sound_delete", RawSoundboardSoundDeleteEvent(data)) async def _add_default_sounds(self) -> None: default_sounds = await self.http.get_default_sounds() diff --git a/discord/threads.py b/discord/threads.py index 7b22fa2743..3a8e9e884b 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -32,11 +32,9 @@ from .abc import Messageable, _purge_messages_helper from .enums import ( ChannelType, -) -from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum -from .enums import ( try_enum, ) +from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum from .errors import ClientException from .flags import ChannelFlags from .mixins import Hashable diff --git a/discord/user.py b/discord/user.py index 4ba25810ca..1877df46a0 100644 --- a/discord/user.py +++ b/discord/user.py @@ -147,9 +147,7 @@ def _update(self, data: UserPayload) -> None: self.nameplate = None primary_guild_payload = data.get("primary_guild", None) if primary_guild_payload and primary_guild_payload.get("identity_enabled"): - self.primary_guild = PrimaryGuild( - data=primary_guild_payload, state=self._state - ) + self.primary_guild = PrimaryGuild(data=primary_guild_payload, state=self._state) else: self.primary_guild = None self._public_flags = data.get("public_flags", 0) diff --git a/discord/utils/private.py b/discord/utils/private.py index 78fb7a80e4..f1ec5d4557 100644 --- a/discord/utils/private.py +++ b/discord/utils/private.py @@ -88,7 +88,7 @@ def get_mime_type_for_file(data: bytes): return "image/gif" elif data.startswith(b"RIFF") and data[8:12] == b"WEBP": return "image/webp" - elif data.startswith(b"\x49\x44\x33") or data.startswith(b"\xff\xfb"): + elif data.startswith((b"\x49\x44\x33", b"\xff\xfb")): return "audio/mpeg" else: raise InvalidArgument("Unsupported file type given") diff --git a/discord/voice_client.py b/discord/voice_client.py index f381e51550..bc4e2196a3 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -634,7 +634,7 @@ def _decrypt_aead_xchacha20_poly1305_rtpsize(self, header, data): nonce[:4] = data[-4:] data = data[:-4] - r =box.decrypt(bytes(data), bytes(header), bytes(nonce))# Discord adds 8 bytes of data before the opus data. + r = box.decrypt(bytes(data), bytes(header), bytes(nonce)) # Discord adds 8 bytes of data before the opus data. # This can be removed, and at this time, discarded as it is unclear what they are for. return r[8:] diff --git a/examples/soundboard.py b/examples/soundboard.py index d19faa666d..fc4cee248c 100644 --- a/examples/soundboard.py +++ b/examples/soundboard.py @@ -21,9 +21,7 @@ def __init__(self, bot: discord.Bot): self.bot = bot @discord.Cog.listener() - async def on_voice_channel_effect_send( - self, event: discord.VoiceChannelEffectSendEvent - ): + async def on_voice_channel_effect_send(self, event: discord.VoiceChannelEffectSendEvent): """Called when someone uses a soundboard effect in a voice channel.""" if event.sound: print(f"{event.user} played sound '{event.sound.name}' in {event.channel}") @@ -43,21 +41,13 @@ async def list_sounds(self, ctx: discord.ApplicationContext): # List default sounds if default_sounds: - default_list = "\n".join( - f"{s.emoji} {s.name} (Volume: {s.volume})" for s in default_sounds - ) - embed.add_field( - name="Default Sounds", value=default_list or "None", inline=False - ) + default_list = "\n".join(f"{s.emoji} {s.name} (Volume: {s.volume})" for s in default_sounds) + embed.add_field(name="Default Sounds", value=default_list or "None", inline=False) # List guild sounds if guild_sounds: - guild_list = "\n".join( - f"{s.emoji} {s.name} (Volume: {s.volume})" for s in guild_sounds - ) - embed.add_field( - name="Guild Sounds", value=guild_list or "None", inline=False - ) + guild_list = "\n".join(f"{s.emoji} {s.name} (Volume: {s.volume})" for s in guild_sounds) + embed.add_field(name="Guild Sounds", value=guild_list or "None", inline=False) await ctx.respond(embed=embed) @@ -80,13 +70,11 @@ async def add_sound( sound_bytes = await attachment.read() emoji = discord.PartialEmoji.from_str(emoji) - new_sound = await ctx.guild.create_sound( - name=name, sound=sound_bytes, volume=1.0, emoji=emoji - ) + new_sound = await ctx.guild.create_sound(name=name, sound=sound_bytes, volume=1.0, emoji=emoji) await ctx.respons(f"Added new sound: {new_sound.emoji} {new_sound.name}") except Exception as e: - await ctx.respond(f"Failed to add sound: {str(e)}") + await ctx.respond(f"Failed to add sound: {e!s}") @discord.slash_command() @discord.default_permissions(manage_guild=True) @@ -111,16 +99,12 @@ async def edit_sound( try: await sound.edit( name=new_name or sound.name, - emoji=( - discord.PartialEmoji.from_str(new_emoji) - if new_emoji - else sound.emoji - ), + emoji=(discord.PartialEmoji.from_str(new_emoji) if new_emoji else sound.emoji), volume=new_volume or sound.volume, ) await ctx.respond(f"Updated sound: {sound.emoji} {sound.name}") except Exception as e: - await ctx.respond(f"Failed to edit sound: {str(e)}") + await ctx.respond(f"Failed to edit sound: {e!s}") @discord.slash_command() async def play_sound( @@ -162,7 +146,7 @@ async def play_sound( await voice_client.disconnect() except Exception as e: - await ctx.respond(f"Failed to play sound: {str(e)}") + await ctx.respond(f"Failed to play sound: {e!s}") bot.add_cog(SoundboardCog(bot)) diff --git a/uv.lock b/uv.lock index 19462ace60..0d2535cd91 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.10" -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version < '3.14'", -] +requires-python = ">=3.10, <3.14" [[package]] name = "aiodns" @@ -341,28 +337,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] @@ -424,17 +398,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] @@ -550,32 +513,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, - { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, - { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, - { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, - { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, - { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, - { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, - { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, - { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] @@ -604,21 +541,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/2b/531e37408573e1da33adfb4c58875013ee8ac7d548d1548967d94a0ae5c4/cryptography-46.0.2-cp311-abi3-win32.whl", hash = "sha256:8b9bf67b11ef9e28f4d78ff88b04ed0929fcd0e4f70bb0f704cfc32a5c6311ee", size = 3056077, upload-time = "2025-10-01T00:27:48.424Z" }, { url = "https://files.pythonhosted.org/packages/a8/cd/2f83cafd47ed2dc5a3a9c783ff5d764e9e70d3a160e0df9a9dcd639414ce/cryptography-46.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:758cfc7f4c38c5c5274b55a57ef1910107436f4ae842478c4989abbd24bd5acb", size = 3512585, upload-time = "2025-10-01T00:27:50.521Z" }, { url = "https://files.pythonhosted.org/packages/00/36/676f94e10bfaa5c5b86c469ff46d3e0663c5dc89542f7afbadac241a3ee4/cryptography-46.0.2-cp311-abi3-win_arm64.whl", hash = "sha256:218abd64a2e72f8472c2102febb596793347a3e65fafbb4ad50519969da44470", size = 2927474, upload-time = "2025-10-01T00:27:52.91Z" }, - { url = "https://files.pythonhosted.org/packages/6f/cc/47fc6223a341f26d103cb6da2216805e08a37d3b52bee7f3b2aee8066f95/cryptography-46.0.2-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:bda55e8dbe8533937956c996beaa20266a8eca3570402e52ae52ed60de1faca8", size = 7198626, upload-time = "2025-10-01T00:27:54.8Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/d66a8591207c28bbe4ac7afa25c4656dc19dc0db29a219f9809205639ede/cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a", size = 4287584, upload-time = "2025-10-01T00:27:57.018Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/fac3ab6302b928e0398c269eddab5978e6c1c50b2b77bb5365ffa8633b37/cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b", size = 4433796, upload-time = "2025-10-01T00:27:58.631Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/24392e5d3c58e2d83f98fe5a2322ae343360ec5b5b93fe18bc52e47298f5/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20", size = 4292126, upload-time = "2025-10-01T00:28:00.643Z" }, - { url = "https://files.pythonhosted.org/packages/ed/38/3d9f9359b84c16c49a5a336ee8be8d322072a09fac17e737f3bb11f1ce64/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73", size = 3993056, upload-time = "2025-10-01T00:28:02.8Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a3/4c44fce0d49a4703cc94bfbe705adebf7ab36efe978053742957bc7ec324/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee", size = 4967604, upload-time = "2025-10-01T00:28:04.783Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c2/49d73218747c8cac16bb8318a5513fde3129e06a018af3bc4dc722aa4a98/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2", size = 4465367, upload-time = "2025-10-01T00:28:06.864Z" }, - { url = "https://files.pythonhosted.org/packages/1b/64/9afa7d2ee742f55ca6285a54386ed2778556a4ed8871571cb1c1bfd8db9e/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f", size = 4291678, upload-time = "2025-10-01T00:28:08.982Z" }, - { url = "https://files.pythonhosted.org/packages/50/48/1696d5ea9623a7b72ace87608f6899ca3c331709ac7ebf80740abb8ac673/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e", size = 4931366, upload-time = "2025-10-01T00:28:10.74Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/9dfc778401a334db3b24435ee0733dd005aefb74afe036e2d154547cb917/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36", size = 4464738, upload-time = "2025-10-01T00:28:12.491Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b1/abcde62072b8f3fd414e191a6238ce55a0050e9738090dc6cded24c12036/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a", size = 4419305, upload-time = "2025-10-01T00:28:14.145Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1f/3d2228492f9391395ca34c677e8f2571fb5370fe13dc48c1014f8c509864/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c", size = 4681201, upload-time = "2025-10-01T00:28:15.951Z" }, - { url = "https://files.pythonhosted.org/packages/de/77/b687745804a93a55054f391528fcfc76c3d6bfd082ce9fb62c12f0d29fc1/cryptography-46.0.2-cp314-cp314t-win32.whl", hash = "sha256:e8633996579961f9b5a3008683344c2558d38420029d3c0bc7ff77c17949a4e1", size = 3022492, upload-time = "2025-10-01T00:28:17.643Z" }, - { url = "https://files.pythonhosted.org/packages/60/a5/8d498ef2996e583de0bef1dcc5e70186376f00883ae27bf2133f490adf21/cryptography-46.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:48c01988ecbb32979bb98731f5c2b2f79042a6c58cc9a319c8c2f9987c7f68f9", size = 3496215, upload-time = "2025-10-01T00:28:19.272Z" }, - { url = "https://files.pythonhosted.org/packages/56/db/ee67aaef459a2706bc302b15889a1a8126ebe66877bab1487ae6ad00f33d/cryptography-46.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8e2ad4d1a5899b7caa3a450e33ee2734be7cc0689010964703a7c4bcc8dd4fd0", size = 2919255, upload-time = "2025-10-01T00:28:21.115Z" }, { url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" }, { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" }, { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" }, @@ -769,38 +691,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] @@ -1035,28 +925,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] @@ -1215,42 +1083,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, ] @@ -1290,12 +1122,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, ] @@ -1472,36 +1298,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/11/938e67c07189b662a6c72551d48285a02496de885408392447c25657dd47/propcache-0.4.0-cp313-cp313t-win32.whl", hash = "sha256:515b610a364c8cdd2b72c734cc97dece85c416892ea8d5c305624ac8734e81db", size = 41321, upload-time = "2025-10-04T21:56:31.406Z" }, { url = "https://files.pythonhosted.org/packages/f4/6e/72b11a4dcae68c728b15126cc5bc830bf275c84836da2633412b768d07e0/propcache-0.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7ea86eb32e74f9902df57e8608e8ac66f1e1e1d24d1ed2ddeb849888413b924d", size = 44846, upload-time = "2025-10-04T21:56:32.5Z" }, { url = "https://files.pythonhosted.org/packages/94/09/0ef3c025e0621e703ef71b69e0085181a3124bcc1beef29e0ffef59ed7f4/propcache-0.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c1443fa4bb306461a3a8a52b7de0932a2515b100ecb0ebc630cc3f87d451e0a9", size = 39689, upload-time = "2025-10-04T21:56:33.686Z" }, - { url = "https://files.pythonhosted.org/packages/60/89/7699d8e9f8c222bbef1fae26afd72d448353f164a52125d5f87dd9fec2c7/propcache-0.4.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de8e310d24b5a61de08812dd70d5234da1458d41b059038ee7895a9e4c8cae79", size = 77977, upload-time = "2025-10-04T21:56:34.836Z" }, - { url = "https://files.pythonhosted.org/packages/77/c5/2758a498199ce46d6d500ba4391a8594df35400cc85738aa9f0c9b8366db/propcache-0.4.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:55a54de5266bc44aa274915cdf388584fa052db8748a869e5500ab5993bac3f4", size = 44715, upload-time = "2025-10-04T21:56:36.075Z" }, - { url = "https://files.pythonhosted.org/packages/0d/da/5a44e10282a28c2dd576e5e1a2c7bb8145587070ddab7375fb643f7129d7/propcache-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:88d50d662c917ec2c9d3858920aa7b9d5bfb74ab9c51424b775ccbe683cb1b4e", size = 46463, upload-time = "2025-10-04T21:56:37.227Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5a/b2c314f655f46c10c204dc0d69e19fadfb1cc4d40ab33f403698a35c3281/propcache-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae3adf88a66f5863cf79394bc359da523bb27a2ed6ba9898525a6a02b723bfc5", size = 206980, upload-time = "2025-10-04T21:56:38.828Z" }, - { url = "https://files.pythonhosted.org/packages/7c/4e/f6643ec2cd5527b92c93488f9b67a170494736bb1c5460136399d709ce5a/propcache-0.4.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f088e21d15b3abdb9047e4b7b7a0acd79bf166893ac2b34a72ab1062feb219e", size = 211385, upload-time = "2025-10-04T21:56:40.2Z" }, - { url = "https://files.pythonhosted.org/packages/71/41/362766a346c3f8d3bbeb7899e1ff40f18844e0fe37e9f6f536553cf6b6be/propcache-0.4.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4efbaf10793fd574c76a5732c75452f19d93df6e0f758c67dd60552ebd8614b", size = 215315, upload-time = "2025-10-04T21:56:41.574Z" }, - { url = "https://files.pythonhosted.org/packages/ff/98/17385d51816d56fa6acc035d8625fbf833b6a795d7ef7fb37ea3f62db6c9/propcache-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:681a168d06284602d56e97f09978057aa88bcc4177352b875b3d781df4efd4cb", size = 201416, upload-time = "2025-10-04T21:56:42.947Z" }, - { url = "https://files.pythonhosted.org/packages/7a/83/801178ca1c29e217564ee507ff2a49d3f24a4dd85c9b9d681fd1d62b15f2/propcache-0.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a7f06f077fc4ef37e8a37ca6bbb491b29e29db9fb28e29cf3896aad10dbd4137", size = 197726, upload-time = "2025-10-04T21:56:44.313Z" }, - { url = "https://files.pythonhosted.org/packages/d2/38/c8743917bca92b7e5474366b6b04c7b3982deac32a0fe4b705f2e92c09bb/propcache-0.4.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:082a643479f49a6778dcd68a80262fc324b14fd8e9b1a5380331fe41adde1738", size = 192819, upload-time = "2025-10-04T21:56:45.702Z" }, - { url = "https://files.pythonhosted.org/packages/0b/74/3de3ef483e8615aaaf62026fcdcb20cbfc4535ea14871b12f72d52c1d6dc/propcache-0.4.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:26692850120241a99bb4a4eec675cd7b4fdc431144f0d15ef69f7f8599f6165f", size = 202492, upload-time = "2025-10-04T21:56:47.388Z" }, - { url = "https://files.pythonhosted.org/packages/46/86/a130dd85199d651a6986ba6bf1ce297b7bbcafc01c8e139e6ba2b8218a20/propcache-0.4.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:33ad7d37b9a386f97582f5d042cc7b8d4b3591bb384cf50866b749a17e4dba90", size = 204106, upload-time = "2025-10-04T21:56:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f7/44eab58659d71d21995146c94139e63882bac280065b3a9ed10376897bcc/propcache-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e7fd82d4a5b7583588f103b0771e43948532f1292105f13ee6f3b300933c4ca", size = 198043, upload-time = "2025-10-04T21:56:50.561Z" }, - { url = "https://files.pythonhosted.org/packages/96/14/df37be1bf1423d2dda201a4cdb1c5cb44048d34e31a97df227cc25b0a55c/propcache-0.4.0-cp314-cp314-win32.whl", hash = "sha256:213eb0d3bc695a70cffffe11a1c2e1c2698d89ffd8dba35a49bc44a035d45c93", size = 38036, upload-time = "2025-10-04T21:56:51.868Z" }, - { url = "https://files.pythonhosted.org/packages/99/96/9cea65d6c50224737e80c57a3f3db4ca81bc7b1b52bc73346df8c50db400/propcache-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:087e2d3d7613e1b59b2ffca0daabd500c1a032d189c65625ee05ea114afcad0b", size = 41156, upload-time = "2025-10-04T21:56:53.242Z" }, - { url = "https://files.pythonhosted.org/packages/52/4d/91523dcbe23cc127b097623a6ba177da51fca6b7c979082aa49745b527b7/propcache-0.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:94b0f7407d18001dbdcbb239512e753b1b36725a6e08a4983be1c948f5435f79", size = 37976, upload-time = "2025-10-04T21:56:54.351Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f7/7118a944cb6cdb548c9333cf311bda120f9793ecca54b2ca4a3f7e58723e/propcache-0.4.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b730048ae8b875e2c0af1a09ca31b303fc7b5ed27652beec03fa22b29545aec9", size = 81270, upload-time = "2025-10-04T21:56:55.516Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f9/04a8bc9977ea201783f3ccb04106f44697f635f70439a208852d4d08554d/propcache-0.4.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f495007ada16a4e16312b502636fafff42a9003adf1d4fb7541e0a0870bc056f", size = 46224, upload-time = "2025-10-04T21:56:56.695Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3d/808b074034156f130a0047304d811a5a5df3bb0976c9adfb9383718fd888/propcache-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:659a0ea6d9017558ed7af00fb4028186f64d0ba9adfc70a4d2c85fcd3d026321", size = 48246, upload-time = "2025-10-04T21:56:57.926Z" }, - { url = "https://files.pythonhosted.org/packages/66/eb/e311f3a59ddc93078cb079b12699af9fd844142c4b4d382b386ee071d921/propcache-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d74aa60b1ec076d4d5dcde27c9a535fc0ebb12613f599681c438ca3daa68acac", size = 275562, upload-time = "2025-10-04T21:56:59.221Z" }, - { url = "https://files.pythonhosted.org/packages/f4/05/a146094d6a00bb2f2036dd2a2f4c2b2733ff9574b59ce53bd8513edfca5d/propcache-0.4.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34000e31795bdcda9826e0e70e783847a42e3dcd0d6416c5d3cb717905ebaec0", size = 273627, upload-time = "2025-10-04T21:57:00.582Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/a6d138f6e3d5f6c9b34dbd336b964a1293f2f1a79cafbe70ae3403d7cc46/propcache-0.4.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bcb5bfac5b9635e6fc520c8af6efc7a0a56f12a1fe9e9d3eb4328537e316dd6a", size = 279778, upload-time = "2025-10-04T21:57:01.944Z" }, - { url = "https://files.pythonhosted.org/packages/ac/09/19594a20da0519bfa00deef8cf35dda6c9a5b51bba947f366e85ea59b3de/propcache-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ea11fceb31fa95b0fa2007037f19e922e2caceb7dc6c6cac4cb56e2d291f1a2", size = 262833, upload-time = "2025-10-04T21:57:03.326Z" }, - { url = "https://files.pythonhosted.org/packages/b5/92/60d2ddc7662f7b2720d3b628ad8ce888015f4ab5c335b7b1b50183194e68/propcache-0.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cd8684f628fe285ea5c86f88e1c30716239dc9d6ac55e7851a4b7f555b628da3", size = 260456, upload-time = "2025-10-04T21:57:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e2/4c2e25c77cf43add2e05a86c4fcf51107edc4d92318e5c593bbdc2515d57/propcache-0.4.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:790286d3d542c0ef9f6d0280d1049378e5e776dcba780d169298f664c39394db", size = 247284, upload-time = "2025-10-04T21:57:06.566Z" }, - { url = "https://files.pythonhosted.org/packages/dc/3e/c273ab8edc80683ec8b15b486e95c03096ef875d99e4b0ab0a36c1e42c94/propcache-0.4.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:009093c9b5dbae114a5958e6a649f8a5d94dd6866b0f82b60395eb92c58002d4", size = 262368, upload-time = "2025-10-04T21:57:08.231Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a9/3fa231f65a9f78614c5aafa9cee788d7f55c22187cc2f33e86c7c16d0262/propcache-0.4.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:728d98179e92d77096937fdfecd2c555a3d613abe56c9909165c24196a3b5012", size = 263010, upload-time = "2025-10-04T21:57:09.641Z" }, - { url = "https://files.pythonhosted.org/packages/38/a0/f4f5d368e60c9dc04d3158eaf1ca0ad899b40ac3d29c015bf62735225a6f/propcache-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9725d96a81e17e48a0fe82d0c3de2f5e623d7163fec70a6c7df90753edd1bec", size = 257298, upload-time = "2025-10-04T21:57:11.125Z" }, - { url = "https://files.pythonhosted.org/packages/c7/30/f78d6758dc36a98f1cddc39b3185cefde616cc58248715b7c65495491cb1/propcache-0.4.0-cp314-cp314t-win32.whl", hash = "sha256:0964c55c95625193defeb4fd85f8f28a9a754ed012cab71127d10e3dc66b1373", size = 42484, upload-time = "2025-10-04T21:57:12.652Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ad/de0640e9b56d2caa796c4266d7d1e6cc4544cc327c25b7ced5c59893b625/propcache-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:24403152e41abf09488d3ae9c0c3bf7ff93e2fb12b435390718f21810353db28", size = 46229, upload-time = "2025-10-04T21:57:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/da/bf/5aed62dddbf2bbe62a3564677436261909c9dd63a0fa1fb6cf0629daa13c/propcache-0.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0363a696a9f24b37a04ed5e34c2e07ccbe92798c998d37729551120a1bb744c4", size = 40329, upload-time = "2025-10-04T21:57:15.198Z" }, { url = "https://files.pythonhosted.org/packages/c7/16/794c114f6041bbe2de23eb418ef58a0f45de27224d5540f5dbb266a73d72/propcache-0.4.0-py3-none-any.whl", hash = "sha256:015b2ca2f98ea9e08ac06eecc409d5d988f78c5fd5821b2ad42bc9afcd6b1557", size = 13183, upload-time = "2025-10-04T21:57:38.054Z" }, ] @@ -1649,32 +1445,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/99/60f19eb1c8eb898882dd8875ea51ad0aac3aff5780b27247969e637cc26a/pycares-4.11.0-cp313-cp313-win32.whl", hash = "sha256:faa8321bc2a366189dcf87b3823e030edf5ac97a6b9a7fc99f1926c4bf8ef28e", size = 118918, upload-time = "2025-09-09T15:17:23.327Z" }, { url = "https://files.pythonhosted.org/packages/2a/14/bc89ad7225cba73068688397de09d7cad657d67b93641c14e5e18b88e685/pycares-4.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:6f74b1d944a50fa12c5006fd10b45e1a45da0c5d15570919ce48be88e428264c", size = 144556, upload-time = "2025-09-09T15:17:24.341Z" }, { url = "https://files.pythonhosted.org/packages/af/88/4309576bd74b5e6fc1f39b9bc5e4b578df2cadb16bdc026ac0cc15663763/pycares-4.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f7581793d8bb3014028b8397f6f80b99db8842da58f4409839c29b16397ad", size = 115692, upload-time = "2025-09-09T15:17:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/2a/70/a723bc79bdcac60361b40184b649282ac0ab433b90e9cc0975370c2ff9c9/pycares-4.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:df0a17f4e677d57bca3624752bbb515316522ad1ce0de07ed9d920e6c4ee5d35", size = 145910, upload-time = "2025-09-09T15:17:26.774Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/46311ef5a384b5f0bb206851135dde8f86b3def38fdbee9e3c03475d35ae/pycares-4.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b44e54cad31d3c3be5e8149ac36bc1c163ec86e0664293402f6f846fb22ad00", size = 142053, upload-time = "2025-09-09T15:17:27.956Z" }, - { url = "https://files.pythonhosted.org/packages/74/23/d236fc4f134d6311e4ad6445571e8285e84a3e155be36422ff20c0fbe471/pycares-4.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:80752133442dc7e6dd9410cec227c49f69283c038c316a8585cca05ec32c2766", size = 637878, upload-time = "2025-09-09T15:17:29.173Z" }, - { url = "https://files.pythonhosted.org/packages/f7/92/6edd41282b3f0e3d9defaba7b05c39730d51c37c165d9d3b319349c975aa/pycares-4.11.0-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:84b0b402dd333403fdce0e204aef1ef834d839c439c0c1aa143dc7d1237bb197", size = 687865, upload-time = "2025-09-09T15:17:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a9/4d7cf4d72600fd47d9518f9ce99703a3e8711fb08d2ef63d198056cdc9a9/pycares-4.11.0-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c0eec184df42fc82e43197e073f9cc8f93b25ad2f11f230c64c2dc1c80dbc078", size = 678396, upload-time = "2025-09-09T15:17:32.304Z" }, - { url = "https://files.pythonhosted.org/packages/0b/4b/e546eeb1d8ff6559e2e3bef31a6ea0c6e57ec826191941f83a3ce900ca89/pycares-4.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ee751409322ff10709ee867d5aea1dc8431eec7f34835f0f67afd016178da134", size = 640786, upload-time = "2025-09-09T15:17:33.602Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f5/b4572d9ee9c26de1f8d1dc80730df756276b9243a6794fa3101bbe56613d/pycares-4.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1732db81e348bfce19c9bf9448ba660aea03042eeeea282824da1604a5bd4dcf", size = 621857, upload-time = "2025-09-09T15:17:34.74Z" }, - { url = "https://files.pythonhosted.org/packages/17/f2/639090376198bcaeff86562b25e1bce05a481cfb1e605f82ce62285230cd/pycares-4.11.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:702d21823996f139874aba5aa9bb786d69e93bde6e3915b99832eb4e335d31ae", size = 670130, upload-time = "2025-09-09T15:17:35.982Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c4/cf40773cd9c36a12cebbe1e9b6fb120f9160dc9bfe0398d81a20b6c69972/pycares-4.11.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:218619b912cef7c64a339ab0e231daea10c994a05699740714dff8c428b9694a", size = 653133, upload-time = "2025-09-09T15:17:37.179Z" }, - { url = "https://files.pythonhosted.org/packages/32/6b/06054d977b0a9643821043b59f523f3db5e7684c4b1b4f5821994d5fa780/pycares-4.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:719f7ddff024fdacde97b926b4b26d0cc25901d5ef68bb994a581c420069936d", size = 629344, upload-time = "2025-09-09T15:17:38.308Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6f/14bb0c2171a286d512e3f02d6168e608ffe5f6eceab78bf63e3073091ae3/pycares-4.11.0-cp314-cp314-win32.whl", hash = "sha256:d552fb2cb513ce910d1dc22dbba6420758a991a356f3cd1b7ec73a9e31f94d01", size = 121804, upload-time = "2025-09-09T15:17:39.388Z" }, - { url = "https://files.pythonhosted.org/packages/24/dc/6822f9ad6941027f70e1cf161d8631456531a87061588ed3b1dcad07d49d/pycares-4.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:23d50a0842e8dbdddf870a7218a7ab5053b68892706b3a391ecb3d657424d266", size = 148005, upload-time = "2025-09-09T15:17:40.44Z" }, - { url = "https://files.pythonhosted.org/packages/ea/24/24ff3a80aa8471fbb62785c821a8e90f397ca842e0489f83ebf7ee274397/pycares-4.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:836725754c32363d2c5d15b931b3ebd46b20185c02e850672cb6c5f0452c1e80", size = 119239, upload-time = "2025-09-09T15:17:42.094Z" }, - { url = "https://files.pythonhosted.org/packages/54/fe/2f3558d298ff8db31d5c83369001ab72af3b86a0374d9b0d40dc63314187/pycares-4.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c9d839b5700542b27c1a0d359cbfad6496341e7c819c7fea63db9588857065ed", size = 146408, upload-time = "2025-09-09T15:17:43.74Z" }, - { url = "https://files.pythonhosted.org/packages/3c/c8/516901e46a1a73b3a75e87a35f3a3a4fe085f1214f37d954c9d7e782bd6d/pycares-4.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:31b85ad00422b38f426e5733a71dfb7ee7eb65a99ea328c508d4f552b1760dc8", size = 142371, upload-time = "2025-09-09T15:17:45.186Z" }, - { url = "https://files.pythonhosted.org/packages/ac/99/c3fba0aa575f331ebed91f87ba960ffbe0849211cdf103ab275bc0107ac6/pycares-4.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cdac992206756b024b371760c55719eb5cd9d6b2cb25a8d5a04ae1b0ff426232", size = 647504, upload-time = "2025-09-09T15:17:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e4/1cdc3ec9c92f8069ec18c58b016b2df7c44a088e2849f37ed457554961aa/pycares-4.11.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:ffb22cee640bc12ee0e654eba74ecfb59e2e0aebc5bccc3cc7ef92f487008af7", size = 697122, upload-time = "2025-09-09T15:17:47.772Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d5/bd8f370b97bb73e5bdd55dc2a78e18d6f49181cf77e88af0599d16f5c073/pycares-4.11.0-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:00538826d2eaf4a0e4becb0753b0ac8d652334603c445c9566c9eb273657eb4c", size = 687543, upload-time = "2025-09-09T15:17:49.183Z" }, - { url = "https://files.pythonhosted.org/packages/33/38/49b77b9cf5dffc0b1fdd86656975c3bc1a58b79bdc883a9ef749b17a013c/pycares-4.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:29daa36548c04cdcd1a78ae187a4b7b003f0b357a2f4f1f98f9863373eedc759", size = 649565, upload-time = "2025-09-09T15:17:51.03Z" }, - { url = "https://files.pythonhosted.org/packages/3c/23/f6d57bfb99d00a6a7363f95c8d3a930fe82a868d9de24c64c8048d66f16a/pycares-4.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cf306f3951740d7bed36149a6d8d656a7d5432dd4bbc6af3bb6554361fc87401", size = 631242, upload-time = "2025-09-09T15:17:52.298Z" }, - { url = "https://files.pythonhosted.org/packages/33/a2/7b9121c71cfe06a8474e221593f83a78176fae3b79e5853d2dfd13ab01cc/pycares-4.11.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:386da2581db4ea2832629e275c061103b0be32f9391c5dfaea7f6040951950ad", size = 680304, upload-time = "2025-09-09T15:17:53.638Z" }, - { url = "https://files.pythonhosted.org/packages/5b/07/dfe76807f637d8b80e1a59dfc4a1bceabdd0205a45b2ebf78b415ae72af3/pycares-4.11.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:45d3254a694459fdb0640ef08724ca9d4b4f6ff6d7161c9b526d7d2e2111379e", size = 661039, upload-time = "2025-09-09T15:17:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9b/55d50c5acd46cbe95d0da27740a83e721d89c0ce7e42bff9891a9f29a855/pycares-4.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eddf5e520bb88b23b04ac1f28f5e9a7c77c718b8b4af3a4a7a2cc4a600f34502", size = 637560, upload-time = "2025-09-09T15:17:56.492Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/2b2e723d1b929dbe7f99e80a56abb29a4f86988c1f73195d960d706b1629/pycares-4.11.0-cp314-cp314t-win32.whl", hash = "sha256:8a75a406432ce39ce0ca41edff7486df6c970eb0fe5cfbe292f195a6b8654461", size = 122235, upload-time = "2025-09-09T15:17:57.576Z" }, - { url = "https://files.pythonhosted.org/packages/93/fe/bf3b3ed9345a38092e72cd9890a5df5c2349fc27846a714d823a41f0ee27/pycares-4.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3784b80d797bcc2ff2bf3d4b27f46d8516fe1707ff3b82c2580dc977537387f9", size = 148575, upload-time = "2025-09-09T15:17:58.699Z" }, - { url = "https://files.pythonhosted.org/packages/ce/20/c0c5cfcf89725fe533b27bc5f714dc4efa8e782bf697c36f9ddf04ba975d/pycares-4.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:afc6503adf8b35c21183b9387be64ca6810644ef54c9ef6c99d1d5635c01601b", size = 119690, upload-time = "2025-09-09T15:17:59.809Z" }, ] [[package]] @@ -1828,24 +1598,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -1919,26 +1671,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/a0/f46fca44457ca1f25f23cc1f06867454fc3c3be118cd10b552b0ab3e58a2/rapidfuzz-3.14.1-cp313-cp313t-win32.whl", hash = "sha256:40875e0c06f1a388f1cab3885744f847b557e0b1642dfc31ff02039f9f0823ef", size = 1760666, upload-time = "2025-09-08T21:07:12.884Z" }, { url = "https://files.pythonhosted.org/packages/9b/d0/7a5d9c04446f8b66882b0fae45b36a838cf4d31439b5d1ab48a9d17c8e57/rapidfuzz-3.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:876dc0c15552f3d704d7fb8d61bdffc872ff63bedf683568d6faad32e51bbce8", size = 1579760, upload-time = "2025-09-08T21:07:14.718Z" }, { url = "https://files.pythonhosted.org/packages/4e/aa/2c03ae112320d0746f2c869cae68c413f3fe3b6403358556f2b747559723/rapidfuzz-3.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:61458e83b0b3e2abc3391d0953c47d6325e506ba44d6a25c869c4401b3bc222c", size = 832088, upload-time = "2025-09-08T21:07:17.03Z" }, - { url = "https://files.pythonhosted.org/packages/d6/36/53debca45fbe693bd6181fb05b6a2fd561c87669edb82ec0d7c1961a43f0/rapidfuzz-3.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e84d9a844dc2e4d5c4cabd14c096374ead006583304333c14a6fbde51f612a44", size = 1926336, upload-time = "2025-09-08T21:07:18.809Z" }, - { url = "https://files.pythonhosted.org/packages/ae/32/b874f48609665fcfeaf16cbaeb2bbc210deef2b88e996c51cfc36c3eb7c3/rapidfuzz-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:40301b93b99350edcd02dbb22e37ca5f2a75d0db822e9b3c522da451a93d6f27", size = 1389653, upload-time = "2025-09-08T21:07:20.667Z" }, - { url = "https://files.pythonhosted.org/packages/97/25/f6c5a1ff4ec11edadacb270e70b8415f51fa2f0d5730c2c552b81651fbe3/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fedd5097a44808dddf341466866e5c57a18a19a336565b4ff50aa8f09eb528f6", size = 1380911, upload-time = "2025-09-08T21:07:22.584Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f3/d322202ef8fab463759b51ebfaa33228100510c82e6153bd7a922e150270/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e3e61c9e80d8c26709d8aa5c51fdd25139c81a4ab463895f8a567f8347b0548", size = 1673515, upload-time = "2025-09-08T21:07:24.417Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b9/6b2a97f4c6be96cac3749f32301b8cdf751ce5617b1c8934c96586a0662b/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da011a373722fac6e64687297a1d17dc8461b82cb12c437845d5a5b161bc24b9", size = 2219394, upload-time = "2025-09-08T21:07:26.402Z" }, - { url = "https://files.pythonhosted.org/packages/11/bf/afb76adffe4406e6250f14ce48e60a7eb05d4624945bd3c044cfda575fbc/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5967d571243cfb9ad3710e6e628ab68c421a237b76e24a67ac22ee0ff12784d6", size = 3163582, upload-time = "2025-09-08T21:07:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/42/34/e6405227560f61e956cb4c5de653b0f874751c5ada658d3532d6c1df328e/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:474f416cbb9099676de54aa41944c154ba8d25033ee460f87bb23e54af6d01c9", size = 1221116, upload-time = "2025-09-08T21:07:30.8Z" }, - { url = "https://files.pythonhosted.org/packages/55/e6/5b757e2e18de384b11d1daf59608453f0baf5d5d8d1c43e1a964af4dc19a/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae2d57464b59297f727c4e201ea99ec7b13935f1f056c753e8103da3f2fc2404", size = 2402670, upload-time = "2025-09-08T21:07:32.702Z" }, - { url = "https://files.pythonhosted.org/packages/43/c4/d753a415fe54531aa882e288db5ed77daaa72e05c1a39e1cbac00d23024f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:57047493a1f62f11354c7143c380b02f1b355c52733e6b03adb1cb0fe8fb8816", size = 2521659, upload-time = "2025-09-08T21:07:35.218Z" }, - { url = "https://files.pythonhosted.org/packages/cd/28/d4e7fe1515430db98f42deb794c7586a026d302fe70f0216b638d89cf10f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4acc20776f225ee37d69517a237c090b9fa7e0836a0b8bc58868e9168ba6ef6f", size = 2788552, upload-time = "2025-09-08T21:07:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/4f/00/eab05473af7a2cafb4f3994bc6bf408126b8eec99a569aac6254ac757db4/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4373f914ff524ee0146919dea96a40a8200ab157e5a15e777a74a769f73d8a4a", size = 3306261, upload-time = "2025-09-08T21:07:39.624Z" }, - { url = "https://files.pythonhosted.org/packages/d1/31/2feb8dfcfcff6508230cd2ccfdde7a8bf988c6fda142fe9ce5d3eb15704d/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:37017b84953927807847016620d61251fe236bd4bcb25e27b6133d955bb9cafb", size = 4269522, upload-time = "2025-09-08T21:07:41.663Z" }, - { url = "https://files.pythonhosted.org/packages/a3/99/250538d73c8fbab60597c3d131a11ef2a634d38b44296ca11922794491ac/rapidfuzz-3.14.1-cp314-cp314-win32.whl", hash = "sha256:c8d1dd1146539e093b84d0805e8951475644af794ace81d957ca612e3eb31598", size = 1745018, upload-time = "2025-09-08T21:07:44.313Z" }, - { url = "https://files.pythonhosted.org/packages/c5/15/d50839d20ad0743aded25b08a98ffb872f4bfda4e310bac6c111fcf6ea1f/rapidfuzz-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:f51c7571295ea97387bac4f048d73cecce51222be78ed808263b45c79c40a440", size = 1587666, upload-time = "2025-09-08T21:07:46.917Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ff/d73fec989213fb6f0b6f15ee4bbdf2d88b0686197951a06b036111cd1c7d/rapidfuzz-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:01eab10ec90912d7d28b3f08f6c91adbaf93458a53f849ff70776ecd70dd7a7a", size = 835780, upload-time = "2025-09-08T21:07:49.256Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e7/f0a242687143cebd33a1fb165226b73bd9496d47c5acfad93de820a18fa8/rapidfuzz-3.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:60879fcae2f7618403c4c746a9a3eec89327d73148fb6e89a933b78442ff0669", size = 1945182, upload-time = "2025-09-08T21:07:51.84Z" }, - { url = "https://files.pythonhosted.org/packages/96/29/ca8a3f8525e3d0e7ab49cb927b5fb4a54855f794c9ecd0a0b60a6c96a05f/rapidfuzz-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f94d61e44db3fc95a74006a394257af90fa6e826c900a501d749979ff495d702", size = 1413946, upload-time = "2025-09-08T21:07:53.702Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ef/6fd10aa028db19c05b4ac7fe77f5613e4719377f630c709d89d7a538eea2/rapidfuzz-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:93b6294a3ffab32a9b5f9b5ca048fa0474998e7e8bb0f2d2b5e819c64cb71ec7", size = 1795851, upload-time = "2025-09-08T21:07:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/e4/30/acd29ebd906a50f9e0f27d5f82a48cf5e8854637b21489bd81a2459985cf/rapidfuzz-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6cb56b695421538fdbe2c0c85888b991d833b8637d2f2b41faa79cea7234c000", size = 1626748, upload-time = "2025-09-08T21:07:58.166Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f4/dfc7b8c46b1044a47f7ca55deceb5965985cff3193906cb32913121e6652/rapidfuzz-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7cd312c380d3ce9d35c3ec9726b75eee9da50e8a38e89e229a03db2262d3d96b", size = 853771, upload-time = "2025-09-08T21:08:00.816Z" }, { url = "https://files.pythonhosted.org/packages/6d/10/0ed838b296fdac08ecbaa3a220fb4f1d887ff41b0be44fe8eade45bb650e/rapidfuzz-3.14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:673ce55a9be5b772dade911909e42382c0828b8a50ed7f9168763fa6b9f7054d", size = 1860246, upload-time = "2025-09-08T21:08:02.762Z" }, { url = "https://files.pythonhosted.org/packages/a4/70/a08f4a86387dec97508ead51cc7a4b3130d4e62ac0eae938a6d8e1feff14/rapidfuzz-3.14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:45c62ada1980ebf4c64c4253993cc8daa018c63163f91db63bb3af69cb74c2e3", size = 1336749, upload-time = "2025-09-08T21:08:04.783Z" }, { url = "https://files.pythonhosted.org/packages/d4/39/c12f76f69184bcfb9977d6404b2c5dac7dd4d70ee6803e61556e539d0097/rapidfuzz-3.14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4d51efb29c0df0d4f7f64f672a7624c2146527f0745e3572098d753676538800", size = 1512629, upload-time = "2025-09-08T21:08:06.697Z" }, @@ -2348,37 +2080,5 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, ] From 419a24e9603f001fafab5d59ea0e7891acab3b50 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Thu, 9 Oct 2025 11:59:42 +0200 Subject: [PATCH 26/35] :bug: Fix imports Signed-off-by: Paillat-dev --- discord/soundboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/soundboard.py b/discord/soundboard.py index 7566a5d07f..9534d52dc5 100644 --- a/discord/soundboard.py +++ b/discord/soundboard.py @@ -35,7 +35,7 @@ VoiceChannelEffectSendEvent as VoiceChannelEffectSendEventPayload, ) from .types.soundboard import SoundboardSound as SoundboardSoundPayload -from .utils import cached_slot_property +from .utils.private import cached_slot_property if TYPE_CHECKING: from .guild import Guild From f51992e245b889545ef3299445afb4a5018fec89 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Thu, 9 Oct 2025 12:05:38 +0200 Subject: [PATCH 27/35] :fire: Remove deprecated attributes on CategoryChannel Signed-off-by: Paillat-dev --- discord/channel.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index d85b2181f9..260b8f1bda 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2988,25 +2988,6 @@ async def create_forum_channel(self, name: str, **options: Any) -> ForumChannel: """ return await self.guild.create_forum_channel(name, category=self, **options) - @utils.deprecated( - since="2.7", - removed="3.0", - reference="NSFW categories are not available in the Discord API.", - ) - def is_nsfw(self) -> bool: - return False - - # TODO: Remove in 3.0 - - @property - @utils.deprecated( - since="2.7", - removed="3.0", - reference="NSFW categories are not available in the Discord API.", - ) - def nsfw(self) -> bool: - return False - DMC = TypeVar("DMC", bound="DMChannel") From 158a1fc83fd84988eeaff3e9d622bd86dfadc53e Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 9 Oct 2025 12:28:49 +0200 Subject: [PATCH 28/35] Update examples/soundboard.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Paillat --- examples/soundboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/soundboard.py b/examples/soundboard.py index fc4cee248c..a964c3d1c8 100644 --- a/examples/soundboard.py +++ b/examples/soundboard.py @@ -72,7 +72,7 @@ async def add_sound( new_sound = await ctx.guild.create_sound(name=name, sound=sound_bytes, volume=1.0, emoji=emoji) - await ctx.respons(f"Added new sound: {new_sound.emoji} {new_sound.name}") + await ctx.respond(f"Added new sound: {new_sound.emoji} {new_sound.name}") except Exception as e: await ctx.respond(f"Failed to add sound: {e!s}") From d517720d3a9825ec962a36afd01a01214dda5db4 Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 9 Oct 2025 12:29:05 +0200 Subject: [PATCH 29/35] Update discord/soundboard.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Paillat --- discord/soundboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/soundboard.py b/discord/soundboard.py index 9534d52dc5..74f33ae79c 100644 --- a/discord/soundboard.py +++ b/discord/soundboard.py @@ -90,7 +90,7 @@ def _from_data(self, data: SoundboardSoundPayload | VoiceChannelEffectSendEventP @override def __eq__(self, other: PartialSoundboardSound) -> bool: # pyright: ignore[reportIncompatibleMethodOverride] - if isinstance(other, self, __class__): + if isinstance(other, self.__class__): return self.id == other.id return NotImplemented From 0c5a3c740e84ed0337752105b12c46242b3eafdf Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 9 Oct 2025 12:29:15 +0200 Subject: [PATCH 30/35] Update discord/raw_models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Paillat --- discord/raw_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/raw_models.py b/discord/raw_models.py index 6611953575..4f9e207ca8 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -841,7 +841,7 @@ def __init__(self, data: MessagePollVoteEvent, added: bool) -> None: class RawSoundboardSoundDeleteEvent(_RawReprMixin): """Represents the payload for an :func:`on_raw_soundboard_sound_delete`. - .. versionadded 2.7 + .. versionadded:: 2.7 """ __slots__ = ("sound_id", "guild_id", "data") From 56831c9d5251911c503ffce6ead1b48793acf51e Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 9 Oct 2025 12:29:33 +0200 Subject: [PATCH 31/35] Update discord/components.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Paillat --- discord/components.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/components.py b/discord/components.py index a6d525cf79..f379f2eb92 100644 --- a/discord/components.py +++ b/discord/components.py @@ -676,6 +676,7 @@ def __str__(self) -> str: @property def url(self) -> str: + """The URL of this media item. This can either be an arbitrary URL or an ``attachment://`` URL to work with local files.""" return self._url @url.setter From 0267f6f78177b52f686058f1a18ee7a4a32d9239 Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 9 Oct 2025 12:29:51 +0200 Subject: [PATCH 32/35] Update discord/components.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Paillat --- discord/components.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/components.py b/discord/components.py index f379f2eb92..3e74caae58 100644 --- a/discord/components.py +++ b/discord/components.py @@ -778,6 +778,10 @@ def __init__(self, url, *, description=None, spoiler=False): @property def url(self) -> str: + """The URL of this gallery item. + + This can either be an arbitrary URL or an ``attachment://`` URL to work with local files. + """ return self.media.url def is_dispatchable(self) -> bool: From 4deddb522582dea2e479a2d0a2e1cd0d8da29159 Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 9 Oct 2025 12:30:21 +0200 Subject: [PATCH 33/35] Update discord/channel.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Paillat --- discord/channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/channel.py b/discord/channel.py index 260b8f1bda..fb8b196265 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2797,7 +2797,7 @@ def __init__(self, *, state: ConnectionState, guild: Guild, data: CategoryChanne self._update(guild, data) def __repr__(self) -> str: - return f"" def _update(self, guild: Guild, data: CategoryChannelPayload) -> None: # This data will always exist From 2ef09746fc5153a517f779c5e8ca808c37ada01e Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 9 Oct 2025 12:30:32 +0200 Subject: [PATCH 34/35] Update discord/ext/commands/core.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Paillat --- discord/ext/commands/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index afcb9c4f97..36d5eafea0 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -1783,7 +1783,7 @@ def check_any(*checks: Check) -> Callable[[T], T]: the :func:`check` decorator. Raises - ------ + ------- TypeError A check passed has not been decorated with the :func:`check` decorator. From e6f1e4d510880c53196becc8f9fc1a34176695b9 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Thu, 9 Oct 2025 12:42:15 +0200 Subject: [PATCH 35/35] :goal_net: Add error handling for missing emojis.json file in utils.py from 682377f8ae3a6ea0cbe43bb4d155050ad1f10013 Signed-off-by: Paillat-dev --- discord/utils/public.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/discord/utils/public.py b/discord/utils/public.py index 470ba37929..edc932c097 100644 --- a/discord/utils/public.py +++ b/discord/utils/public.py @@ -5,6 +5,7 @@ import importlib.resources import itertools import json +import logging import re from collections.abc import Awaitable, Callable, Iterable from enum import Enum, auto @@ -16,6 +17,8 @@ from ..commands.options import OptionChoice from ..permissions import Permissions +_log = logging.getLogger(__name__) + T = TypeVar("T") @@ -542,7 +545,13 @@ def find(predicate: Callable[[T], Any], seq: Iterable[T]) -> T | None: return None -with importlib.resources.files(__package__).joinpath("../emojis.json").open(encoding="utf-8") as f: - EMOJIS_MAP = json.load(f) +try: + with importlib.resources.files(__package__).joinpath("../emojis.json").open(encoding="utf-8") as f: + EMOJIS_MAP = json.load(f) +except FileNotFoundError: + _log.debug( + "Couldn't find emojis.json. Is the package data missing? Discord emojis names will not work.", + ) +EMOJIS_MAP = {} UNICODE_EMOJIS = set(EMOJIS_MAP.values())