diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3608efb6c..229e42207 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -41,7 +41,7 @@ jobs: python-version: 3.12 - name: Run Ruff - run: poetry run ruff check . --no-fix --extend-select TD002,TD003 + run: poetry run ruff check . --no-fix poetry-check: runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c1e6be53..80f63e1a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,13 +34,13 @@ We recommend also reading the following if you're unsure or not confident: * [How To Make A Pull Request](https://makeapullrequest.com) * [Contributing To An Open Source Project For The First Time](https://firsttimersonly.com) -This bot is written in [Python](https://python.org) using [Pycord](https://pycord.dev) and uses Discord's [slash-commands](https://support.discord.com/hc/articles/1500000368501-Slash-Commands-FAQ) & [user-commands](https://guide.pycord.dev/interactions/application-commands/context-menus). +TeX-Bot is written in [Python](https://python.org) using [Pycord](https://pycord.dev) and uses Discord's [slash-commands](https://support.discord.com/hc/articles/1500000368501-Slash-Commands-FAQ) & [user-commands](https://guide.pycord.dev/interactions/application-commands/context-menus). We would recommend being somewhat familiar with the [Pycord library](https://docs.pycord.dev), [Python language](https://docs.python.org/3/reference/index) & [project terminology](README.md#terminology) before contributing. ## Using the Issue Tracker We use [GitHub issues](https://docs.github.com/issues) to track bugs and feature requests. -If you find an issue with the bot, the best place to report it is through the issue tracker. +If you find an issue with TeX-Bot, the best place to report it is through the issue tracker. If you are looking for issues to contribute code to, it's a good idea to look at the [issues labelled "good-first-issue"](https://github.com/CSSUoB/TeX-Bot-Py-V2/issues?q=label%3A%22good+first+issue%22)! When submitting an issue, please be as descriptive as possible. @@ -79,7 +79,7 @@ There are separate cog files for each activity, and one [`__init__.py`](cogs/__i * [`cogs/delete_all.py`](cogs/delete_all.py): cogs for deleting all permanent data stored in a specific object's table in the database -* [`cogs/edit_message.py`](cogs/edit_message.py): cogs for editing messages that were previously sent by the bot +* [`cogs/edit_message.py`](cogs/edit_message.py): cogs for editing messages that were previously sent by TeX-Bot * [`cogs/induct.py`](cogs/induct.py): cogs for inducting people into your group's Discord guild @@ -89,7 +89,7 @@ There are separate cog files for each activity, and one [`__init__.py`](cogs/__i * [`cogs/ping.py`](cogs/ping.py): cog to request a [ping](https://wikipedia.org/wiki/Ping-pong_scheme#Internet) response -* [`cogs/remind_me.py`](cogs/remind_me.py): cogs to ask the bot to send a reminder message at a later date +* [`cogs/remind_me.py`](cogs/remind_me.py): cogs to ask TeX-Bot to send a reminder message at a later date * [`cogs/send_get_roles_reminders.py`](cogs/send_get_roles_reminders.py): cogs relating to sending reminders, to Discord members, about opt-in roles. (See [Repeated Tasks Conditions](README.md#repeated-tasks-conditions) for which conditions are required to be met, to execute this task) diff --git a/Dockerfile b/Dockerfile index b7621c890..4371a0cef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,9 +38,9 @@ WORKDIR /app COPY LICENSE .en[v] config.py main.py messages.json ./ RUN chmod +x main.py -COPY cogs/ ./cogs/ -COPY db/ ./db/ -COPY utils/ ./utils/ COPY exceptions/ ./exceptions/ +COPY utils/ ./utils/ +COPY db/ ./db/ +COPY cogs/ ./cogs/ ENTRYPOINT ["python", "-m", "main"] diff --git a/README.md b/README.md index 0e2e44fa8..8d130cad9 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,10 @@ The meaning of each error code is given here: * `E1023` - Your [Discord guild](https://discord.com/developers/docs/resources/guild) does not contain a [role](https://discord.com/developers/docs/topics/permissions#role-object) with the name "@**Member**". (This [role](https://discord.com/developers/docs/topics/permissions#role-object) is required for the `/makemember` & `/ensure-members-inducted` [commands](https://discord.com/developers/docs/interactions/application-commands)) -* `E1024` - Your [Discord guild](https://discord.com/developers/docs/resources/guild) does not contain a [role](https://discord.com/developers/docs/topics/permissions#role-object) with the name "**@Archivist**". +* `E1024` - Your [Discord guild](https://discord.com/developers/docs/resources/guild) does not contain a [role](https://discord.com/developers/docs/topics/permissions#role-object) with the name "@**Archivist**". (This [role](https://discord.com/developers/docs/topics/permissions#role-object) is required for the `/archive` [command](https://discord.com/developers/docs/interactions/application-commands)) -* `E1025` - Your [Discord guild](https://discord.com/developers/docs/resources/guild) does not contain a [role](https://discord.com/developers/docs/topics/permissions#role-object) with the name "**@Applicant**". (This [role](https://discord.com/developers/docs/topics/permissions#role-object) is required for the `/make-applicant` [command](https://discord.com/developers/docs/interactions/application-commands) and respective user and message commands) +* `E1025` - Your [Discord guild](https://discord.com/developers/docs/resources/guild) does not contain a [role](https://discord.com/developers/docs/topics/permissions#role-object) with the name "@**Applicant**". (This [role](https://discord.com/developers/docs/topics/permissions#role-object) is required for the `/make-applicant` [command](https://discord.com/developers/docs/interactions/application-commands) and respective user and message commands) * `E1026` - Your [Discord guild](https://discord.com/developers/docs/resources/guild) does not contain a [role](https://discord.com/developers/docs/topics/permissions#role-objec) with the name "@**Committee-Elect**". (This [role](https://discord.com/developers/docs/topics/permissions#role-object) is required for the `/handover` [command](https://discord.com/developers/docs/interactions/application-commands)) diff --git a/cogs/__init__.py b/cogs/__init__.py index a9bb494f5..0117e0682 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -9,7 +9,7 @@ __all__: Sequence[str] = ( "ArchiveCommandCog", - "GetTokenAuthorisationCommand", + "GetTokenAuthorisationCommandCog", "CommandErrorCog", "DeleteAllCommandsCog", "EditMessageCommandCog", @@ -49,7 +49,7 @@ from cogs.command_error import CommandErrorCog from cogs.delete_all import DeleteAllCommandsCog from cogs.edit_message import EditMessageCommandCog -from cogs.get_token_authorisation import GetTokenAuthorisationCommand +from cogs.get_token_authorisation import GetTokenAuthorisationCommandCog from cogs.induct import ( EnsureMembersInductedCommandCog, InductContextCommandsCog, @@ -80,7 +80,7 @@ def setup(bot: TeXBot) -> None: """Add all the cogs to the bot, at bot startup.""" cogs: Iterable[type[TeXBotBaseCog]] = ( ArchiveCommandCog, - GetTokenAuthorisationCommand, + GetTokenAuthorisationCommandCog, CommandErrorCog, DeleteAllCommandsCog, EditMessageCommandCog, diff --git a/cogs/annual_handover_and_reset.py b/cogs/annual_handover_and_reset.py index 57371a114..e5cd08ace 100644 --- a/cogs/annual_handover_and_reset.py +++ b/cogs/annual_handover_and_reset.py @@ -22,7 +22,7 @@ class CommitteeHandoverCommandCog(TeXBotBaseCog): @discord.slash_command( # type: ignore[no-untyped-call, misc] name="committee-handover", - description="Initiates the annual Discord handover procedure for new committee", + description="Initiates the annual Discord handover procedure for new committee.", ) @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild @@ -40,9 +40,9 @@ async def committee_handover(self, ctx: TeXBotApplicationContext) -> None: To do this, TeX-Bot will need to hold a role above that of the "Committee" role. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - main_guild: discord.Guild = self.bot.main_guild - committee_role: discord.Role = await self.bot.committee_role - committee_elect_role: discord.Role = await self.bot.committee_elect_role + main_guild: discord.Guild = self.tex_bot.main_guild + committee_role: discord.Role = await self.tex_bot.committee_role + committee_elect_role: discord.Role = await self.tex_bot.committee_elect_role initial_response: discord.Interaction | discord.WebhookMessage = await ctx.respond( content=":hourglass: Running handover procedures... :hourglass:", @@ -163,7 +163,7 @@ class AnnualRolesResetCommandCog(TeXBotBaseCog): @discord.slash_command( # type: ignore[no-untyped-call, misc] name="annual-roles-reset", - description="Removes the @Member role and academic year roles from all users", + description="Removes the @Member role and academic year roles from all users.", ) @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild @@ -176,8 +176,8 @@ async def annual_roles_reset(self, ctx: TeXBotApplicationContext) -> None: the GroupMadeMember database model. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - main_guild: discord.Guild = self.bot.main_guild - member_role: discord.Role = await self.bot.member_role + main_guild: discord.Guild = self.tex_bot.main_guild + member_role: discord.Role = await self.tex_bot.member_role logger.debug("Reset roles command called.") initial_response: discord.Interaction | discord.WebhookMessage = await ctx.respond( diff --git a/cogs/archive.py b/cogs/archive.py index de8bc9052..30bdf12fc 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -7,7 +7,9 @@ import logging import re +from collections.abc import Set from logging import Logger +from typing import Final import discord @@ -21,34 +23,34 @@ TeXBotBaseCog, ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class ArchiveCommandCog(TeXBotBaseCog): """Cog class that defines the "/archive" command and its call-back method.""" @staticmethod - async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice]: # noqa: E501 + async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable categories. - The list of available selectable categories is unique to each member, and is used in + The list of available selectable categories is unique to each member and is used in any of the "archive" slash-command options that have a category input-type. """ if not ctx.interaction.user: return set() try: - main_guild: discord.Guild = ctx.bot.main_guild - interaction_user: discord.Member = await ctx.bot.get_main_guild_member( + if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): + return set() + + main_guild: discord.Guild = ctx.tex_bot.main_guild + interaction_user: discord.Member = await ctx.tex_bot.get_main_guild_member( ctx.interaction.user, ) except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() - if not await ctx.bot.check_user_has_committee_role(interaction_user): - return set() - return { discord.OptionChoice(name=category.name, value=str(category.id)) for category @@ -80,15 +82,15 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> have the "Archivist" role. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - main_guild: discord.Guild = self.bot.main_guild - interaction_member: discord.Member = await self.bot.get_main_guild_member(ctx.user) - committee_role: discord.Role = await self.bot.committee_role - guest_role: discord.Role = await self.bot.guest_role - member_role: discord.Role = await self.bot.member_role - archivist_role: discord.Role = await self.bot.archivist_role - everyone_role: discord.Role = await self.bot.get_everyone_role() - - if not re.match(r"\A\d{17,20}\Z", str_category_id): + main_guild: discord.Guild = self.tex_bot.main_guild + interaction_member: discord.Member = await self.tex_bot.get_main_guild_member(ctx.user) + committee_role: discord.Role = await self.tex_bot.committee_role + guest_role: discord.Role = await self.tex_bot.guest_role + member_role: discord.Role = await self.tex_bot.member_role + archivist_role: discord.Role = await self.tex_bot.archivist_role + everyone_role: discord.Role = await self.tex_bot.get_everyone_role() + + if not re.fullmatch(r"\A\d{17,20}\Z", str_category_id): await self.command_send_error( ctx, message=f"{str_category_id!r} is not a valid category ID.", @@ -121,19 +123,19 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> channel: AllChannelTypes for channel in category.channels: try: - channel_needs_committee_archiving: bool = ( + CHANNEL_NEEDS_COMMITTEE_ARCHIVING: bool = ( channel.permissions_for(committee_role).is_superset( discord.Permissions(view_channel=True), ) and not channel.permissions_for(guest_role).is_superset( discord.Permissions(view_channel=True), ) ) - channel_needs_normal_archiving: bool = channel.permissions_for( - guest_role, - ).is_superset( - discord.Permissions(view_channel=True), + CHANNEL_NEEDS_NORMAL_ARCHIVING: bool = ( + channel.permissions_for(guest_role).is_superset( + discord.Permissions(view_channel=True), + ) ) - if channel_needs_committee_archiving: + if CHANNEL_NEEDS_COMMITTEE_ARCHIVING: await channel.set_permissions( everyone_role, reason=f"{interaction_member.display_name} used \"/archive\".", @@ -155,7 +157,7 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> reason=f"{interaction_member.display_name} used \"/archive\".", ) - elif channel_needs_normal_archiving: + elif CHANNEL_NEEDS_NORMAL_ARCHIVING: await channel.set_permissions( everyone_role, reason=f"{interaction_member.display_name} used \"/archive\".", @@ -197,12 +199,14 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> await self.command_send_error( ctx, message=( - "Bot does not have access to the channels in the selected category." + "TeX-Bot does not have access to " + "the channels in the selected category." ), ) logger.error( # noqa: TRY400 ( - "Bot did not have access to the channels in the selected category: " + "TeX-Bot did not have access to " + "the channels in the selected category: " "%s." ), category.name, diff --git a/cogs/command_error.py b/cogs/command_error.py index 9e01254c6..37319586b 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -8,6 +8,7 @@ import contextlib import logging from logging import Logger +from typing import Final import discord from discord import Forbidden @@ -20,7 +21,7 @@ from exceptions.base import BaseErrorWithErrorCode from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class CommandErrorCog(TeXBotBaseCog): @@ -48,7 +49,8 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro elif isinstance(error, CheckAnyFailure): if CommandChecks.is_interaction_user_in_main_guild_failure(error.checks[0]): message = ( - f"You must be a member of the {self.bot.group_short_name} Discord server " + "You must be a member of " + f"the {self.tex_bot.group_short_name} Discord server " "to use this command." ) @@ -56,7 +58,7 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro # noinspection PyUnusedLocal committee_role_mention: str = "@Committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_role_mention = (await self.bot.committee_role).mention + committee_role_mention = (await self.tex_bot.committee_role).mention message = f"Only {committee_role_mention} members can run this command." await self.command_send_error( @@ -85,4 +87,4 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro if message_part ), ) - await self.bot.close() + await self.tex_bot.close() diff --git a/cogs/delete_all.py b/cogs/delete_all.py index aafb9eccb..fb43de5cb 100644 --- a/cogs/delete_all.py +++ b/cogs/delete_all.py @@ -16,8 +16,10 @@ class DeleteAllCommandsCog(TeXBotBaseCog): """Cog class that defines the "/delete-all" command group and command call-back methods.""" delete_all: discord.SlashCommandGroup = discord.SlashCommandGroup( - "delete-all", - "Delete all instances of the selected object type from the backend database", + name="delete-all", + description=( + "Delete all instances of the selected object type from the backend database" + ), ) @staticmethod diff --git a/cogs/edit_message.py b/cogs/edit_message.py index fb230b4bc..842b8270b 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -6,6 +6,7 @@ import re +from collections.abc import Set import discord @@ -21,34 +22,30 @@ class EditMessageCommandCog(TeXBotBaseCog): # noinspection SpellCheckingInspection - """Cog class that defines the "/editmessage" command and its call-back method.""" + """Cog class that defines the "/edit-message" command and its call-back method.""" @staticmethod - async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice]: # noqa: E501 + async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable channels. - The list of available selectable channels is unique to each member, and is used in any + The list of available selectable channels is unique to each member and is used in any of the "edit-message" slash-command options that have a channel input-type. """ if not ctx.interaction.user: return set() try: - interaction_user: discord.Member = await ctx.bot.get_main_guild_member( - ctx.interaction.user, - ) + if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): + return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() - if not await ctx.bot.check_user_has_committee_role(interaction_user): - return set() - return await TeXBotBaseCog.autocomplete_get_text_channels(ctx) # noinspection SpellCheckingInspection @discord.slash_command( # type: ignore[no-untyped-call, misc] - name="editmessage", + name="edit-message", description="Edits a message sent by TeX-Bot to the value supplied.", ) @discord.option( # type: ignore[no-untyped-call, misc] @@ -86,9 +83,9 @@ async def edit_message(self, ctx: TeXBotApplicationContext, str_channel_id: str, The "write_roles" command edits a message sent by TeX-Bot to the value supplied. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - main_guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild - if not re.match(r"\A\d{17,20}\Z", str_channel_id): + if not re.fullmatch(r"\A\d{17,20}\Z", str_channel_id): await self.command_send_error( ctx, message=f"{str_channel_id!r} is not a valid channel ID.", @@ -97,7 +94,7 @@ async def edit_message(self, ctx: TeXBotApplicationContext, str_channel_id: str, channel_id: int = int(str_channel_id) - if not re.match(r"\A\d{17,20}\Z", str_message_id): + if not re.fullmatch(r"\A\d{17,20}\Z", str_message_id): await self.command_send_error( ctx, message=f"{str_message_id!r} is not a valid message ID.", @@ -113,7 +110,7 @@ async def edit_message(self, ctx: TeXBotApplicationContext, str_channel_id: str, if not channel: await self.command_send_error( ctx, - message=f"Text channel with ID \"{channel_id}\" does not exist.", + message=f"Text channel with ID '{channel_id}' does not exist.", ) return @@ -122,7 +119,7 @@ async def edit_message(self, ctx: TeXBotApplicationContext, str_channel_id: str, except discord.NotFound: await self.command_send_error( ctx, - message=f"Message with ID \"{message_id}\" does not exist.", + message=f"Message with ID '{message_id}' does not exist.", ) return diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 122c79268..eda8a767a 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -2,7 +2,7 @@ from collections.abc import Sequence -__all__: Sequence[str] = ("GetTokenAuthorisationCommand",) +__all__: Sequence[str] = ("GetTokenAuthorisationCommandCog",) import contextlib @@ -23,10 +23,10 @@ logger: Logger = logging.getLogger("TeX-Bot") -class GetTokenAuthorisationCommand(TeXBotBaseCog): +class GetTokenAuthorisationCommandCog(TeXBotBaseCog): """Cog class that defines the "/get_token_authorisation" command.""" - @discord.slash_command( # type: ignore[no-untyped-call, misc] + @discord.slash_command( # type: ignore[no-untyped-call, misc] name="get-token-authorisation", description="Checks the authorisations held by the token.", ) @@ -36,7 +36,7 @@ async def get_token_authorisation(self, ctx: TeXBotApplicationContext) -> None: """ Definition of the "get_token_authorisation" command. - The "get_token_authorisation" command will retrieve the profle for the token user. + The "get_token_authorisation" command will retrieve the profile for the token user. The profile page will contain the user's name and a list of the MSL organisations the user has administrative access to. """ @@ -108,9 +108,10 @@ async def get_token_authorisation(self, ctx: TeXBotApplicationContext) -> None: user_name.text, ) + # noinspection PyUnusedLocal guest_role: discord.Role | None = None with contextlib.suppress(GuestRoleDoesNotExistError): - guest_role = await ctx.bot.guest_role + guest_role = await ctx.tex_bot.guest_role await ctx.respond( f"Admin token has access to the following MSL Organisations as " @@ -120,6 +121,6 @@ async def get_token_authorisation(self, ctx: TeXBotApplicationContext) -> None: ephemeral=bool( (not guest_role) or ctx.channel.permissions_for(guest_role).is_superset( discord.Permissions(view_channel=True), - ), + ) # noqa: COM812 ), ) diff --git a/cogs/induct.py b/cogs/induct.py index afbf97577..defc59d43 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -14,6 +14,7 @@ import contextlib import logging import random +from collections.abc import Set from logging import Logger from typing import Literal @@ -40,6 +41,7 @@ logger: Logger = logging.getLogger("TeX-Bot") + class InductSendMessageCog(TeXBotBaseCog): """Cog class that defines the "/induct" command and its call-back method.""" @@ -53,13 +55,13 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) a guest into your group's Discord guild. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild - if before.guild != guild or after.guild != guild or before.bot or after.bot: + if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: return try: - guest_role: discord.Role = await self.bot.guest_role + guest_role: discord.Role = await self.tex_bot.guest_role except GuestRoleDoesNotExistError: return @@ -72,36 +74,36 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) ).adelete() async for message in after.history(): - message_is_introduction_reminder: bool = ( + MESSAGE_IS_INTRODUCTION_REMINDER: bool = ( ( "joined the " in message.content ) and ( " Discord guild but have not yet introduced" in message.content ) and message.author.bot ) - if message_is_introduction_reminder: + if MESSAGE_IS_INTRODUCTION_REMINDER: await message.delete( reason="Delete introduction reminders after member is inducted.", ) # noinspection PyUnusedLocal - rules_channel_mention: str = "`#welcome`" + rules_channel_mention: str = "**`#welcome`**" with contextlib.suppress(RulesChannelDoesNotExistError): - rules_channel_mention = (await self.bot.rules_channel).mention + rules_channel_mention = (await self.tex_bot.rules_channel).mention # noinspection PyUnusedLocal - roles_channel_mention: str = "#roles" + roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.bot.roles_channel).mention + roles_channel_mention = (await self.tex_bot.roles_channel).mention user_type: Literal["guest", "member"] = "guest" with contextlib.suppress(MemberRoleDoesNotExistError): - if await self.bot.member_role in after.roles: + if await self.tex_bot.member_role in after.roles: user_type = "member" try: await after.send( - f"**Congrats on joining the {self.bot.group_short_name} Discord server " + f"**Congrats on joining the {self.tex_bot.group_short_name} Discord server " f"as a {user_type}!** " "You now have access to communicate in all the public channels.\n\n" "Some things to do to get started:\n" @@ -115,12 +117,11 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) if user_type != "member": await after.send( f"You can also get yourself an annual membership " - f"to {self.bot.group_full_name} for only £5! " - f"""Just head to {settings["PURCHASE_MEMBERSHIP_URL"]}. """ + f"to {self.tex_bot.group_full_name} for only £5! " + f"Just head to {settings["PURCHASE_MEMBERSHIP_URL"]}. " "You'll get awesome perks like a free T-shirt:shirt:, " - "access to member only events:calendar_spiral: " - f"& a cool green name on the {self.bot.group_short_name} Discord server" - ":green_square:! " + "access to member only events:calendar_spiral: and a cool green name on " + f"the {self.tex_bot.group_short_name} Discord server:green_square:! " f"Checkout all the perks at {settings["MEMBERSHIP_PERKS_URL"]}", ) except discord.Forbidden: @@ -153,7 +154,7 @@ async def get_random_welcome_message(self, induction_member: discord.User | disc if "" in random_welcome_message: try: - committee_role_mention: str = (await self.bot.committee_role).mention + committee_role_mention: str = (await self.tex_bot.committee_role).mention except CommitteeRoleDoesNotExistError: return await self.get_random_welcome_message(induction_member) else: @@ -174,7 +175,7 @@ async def get_random_welcome_message(self, induction_member: discord.User | disc if "" in random_welcome_message: random_welcome_message = random_welcome_message.replace( "", - self.bot.group_short_name, + self.tex_bot.group_short_name, ) return random_welcome_message.strip() @@ -182,8 +183,8 @@ async def get_random_welcome_message(self, induction_member: discord.User | disc async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_member: discord.Member, *, silent: bool) -> None: # noqa: E501 """Perform the actual process of inducting a member by giving them the Guest role.""" # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - guest_role: discord.Role = await self.bot.guest_role - main_guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild + guest_role: discord.Role = await self.tex_bot.guest_role intro_channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, @@ -212,18 +213,21 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb return if not silent: - general_channel: discord.TextChannel = await self.bot.general_channel + general_channel: discord.TextChannel = await self.tex_bot.general_channel # noinspection PyUnusedLocal - roles_channel_mention: str = "#roles" + roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.bot.roles_channel).mention + roles_channel_mention = (await self.tex_bot.roles_channel).mention message_already_sent: bool = False message: discord.Message async for message in general_channel.history(limit=7): - if message.author == self.bot.user and "grab your roles" in message.content: - message_already_sent = True + message_already_sent = ( + message.author == self.tex_bot.user + and "grab your roles" in message.content + ) + if message_already_sent: break if not message_already_sent: @@ -238,9 +242,10 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb reason=f"{ctx.user} used TeX Bot slash-command: \"/induct\"", ) + # noinspection PyUnusedLocal applicant_role: discord.Role | None = None with contextlib.suppress(ApplicantRoleDoesNotExistError): - applicant_role = await ctx.bot.applicant_role + applicant_role = await ctx.tex_bot.applicant_role if applicant_role and applicant_role in induction_member.roles: await induction_member.remove_roles( @@ -248,7 +253,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb reason=f"{ctx.user} used TeX Bot slash-command: \"/induct\"", ) - tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213) + tex_emoji: discord.Emoji | None = self.tex_bot.get_emoji(743218410409820213) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") @@ -267,7 +272,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb logger.info( "Failed to add reactions because the user, %s, " - "has blocked the bot.", + "has blocked TeX-Bot.", recent_message.author, ) break @@ -279,7 +284,7 @@ class InductSlashCommandCog(BaseInductCog): """Cog class that defines the "/induct" command and its call-back method.""" @staticmethod - async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice]: # noqa: E501 + async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable members. @@ -287,18 +292,17 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discor that have a member input-type. """ try: - guild: discord.Guild = ctx.bot.main_guild - except GuildDoesNotExistError: + main_guild: discord.Guild = ctx.tex_bot.main_guild + guest_role: discord.Role = await ctx.tex_bot.guest_role + except (GuildDoesNotExistError, GuestRoleDoesNotExistError): return set() - members: set[discord.Member] = {member for member in guild.members if not member.bot} - - try: - guest_role: discord.Role = await ctx.bot.guest_role - except GuestRoleDoesNotExistError: - return set() - else: - members = {member for member in members if guest_role not in member.roles} + members: set[discord.Member] = { + member + for member + in main_guild.members + if not member.bot and guest_role not in member.roles + } if not ctx.value or ctx.value.startswith("@"): return { @@ -313,7 +317,6 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discor in members } - @discord.slash_command( # type: ignore[no-untyped-call, misc] name="induct", description=( @@ -346,14 +349,13 @@ async def induct(self, ctx: TeXBotApplicationContext, str_induct_member_id: str, """ member_id_not_integer_error: ValueError try: - induct_member: discord.Member = await self.bot.get_member_from_str_id( + induct_member: discord.Member = await self.tex_bot.get_member_from_str_id( str_induct_member_id, ) except ValueError as member_id_not_integer_error: await self.command_send_error(ctx, message=member_id_not_integer_error.args[0]) return - # noinspection PyUnboundLocalVariable await self._perform_induction(ctx, induct_member, silent=silent) @@ -363,27 +365,28 @@ class InductContextCommandsCog(BaseInductCog): @discord.user_command(name="Induct User") # type: ignore[no-untyped-call, misc] @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild - async def non_silent_induct(self, ctx: TeXBotApplicationContext, member: discord.Member) -> None: # noqa: E501 + async def non_silent_user_induct(self, ctx: TeXBotApplicationContext, member: discord.Member) -> None: # noqa: E501 """ Definition & callback response of the "non_silent_induct" user-context-command. - The "non_silent_induct" command executes the same process as the - "induct" slash-command, and thus inducts a given member - into your group's Discord guild by giving them the "Guest" role, - only without broadcasting a welcome message. + The "non_silent_induct" command executes the same process + as the "induct" slash-command, using the user-context-menu. + Therefore, it will induct a given member into your group's Discord guild + by giving them the "Guest" role. """ await self._perform_induction(ctx, member, silent=False) @discord.user_command(name="Silently Induct User") # type: ignore[no-untyped-call, misc] @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild - async def silent_induct(self, ctx: TeXBotApplicationContext, member: discord.Member) -> None: # noqa: E501 + async def silent_user_induct(self, ctx: TeXBotApplicationContext, member: discord.Member) -> None: # noqa: E501 """ Definition & callback response of the "silent_induct" user-context-command. The "silent_induct" command executes the same process as the "induct" slash-command, - and thus inducts a given member into your group's Discord guild by giving them the - "Guest" role. + using the user-context-menu. + Therefore, it will induct a given member into your group's Discord guild + by giving them the "Guest" role, only without broadcasting a welcome message. """ await self._perform_induction(ctx, member, silent=True) @@ -394,11 +397,13 @@ async def non_silent_message_induct(self, ctx: TeXBotApplicationContext, message """ Definition and callback response of the "non_silent_induct" message-context-command. - The non_silent_message_induct command executes the same process as the - induct slash command using the message-context-menu instead of the user-menu. + The "non_silent_induct" command executes the same process + as the "induct" slash-command, using the message-context-menu. + Therefore, it will induct a given member into your group's Discord guild + by giving them the "Guest" role. """ try: - member: discord.Member = await self.bot.get_member_from_str_id( + member: discord.Member = await self.tex_bot.get_member_from_str_id( str(message.author.id), ) except ValueError: @@ -421,11 +426,13 @@ async def silent_message_induct(self, ctx: TeXBotApplicationContext, message: di """ Definition and callback response of the "silent_induct" message-context-command. - The silent_message_induct command executes the same process as the - induct slash command using the message-context-menu instead of the user-menu. + The "silent_induct" command executes the same process as the "induct" slash-command, + using the message-context-menu. + Therefore, it will induct a given member into your group's Discord guild + by giving them the "Guest" role, only without broadcasting a welcome message. """ try: - member: discord.Member = await self.bot.get_member_from_str_id( + member: discord.Member = await self.tex_bot.get_member_from_str_id( str(message.author.id), ) except ValueError: @@ -461,9 +468,9 @@ async def ensure_members_inducted(self, ctx: TeXBotApplicationContext) -> None: have also been given the "Guest" role. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - main_guild: discord.Guild = self.bot.main_guild - member_role: discord.Role = await self.bot.member_role - guest_role: discord.Role = await self.bot.guest_role + main_guild: discord.Guild = self.tex_bot.main_guild + member_role: discord.Role = await self.tex_bot.member_role + guest_role: discord.Role = await self.tex_bot.guest_role await ctx.defer(ephemeral=True) diff --git a/cogs/kill.py b/cogs/kill.py index c84688ec9..44f146d88 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -8,6 +8,7 @@ import contextlib import logging from logging import Logger +from typing import Final import discord from discord.ui import View @@ -15,7 +16,7 @@ from exceptions import CommitteeRoleDoesNotExistError from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class ConfirmKillView(View): @@ -58,7 +59,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: """ committee_role: discord.Role | None = None with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_role = await self.bot.committee_role + committee_role = await self.tex_bot.committee_role response: discord.Message | discord.Interaction = await ctx.respond( content=( @@ -77,7 +78,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: else await response.original_response() ) - button_interaction: discord.Interaction = await self.bot.wait_for( + button_interaction: discord.Interaction = await self.tex_bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component @@ -93,8 +94,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: content="My battery is low and it's getting dark...", view=None, ) - await self.bot.perform_kill_and_close(initiated_by_user=ctx.interaction.user) - return + await self.tex_bot.perform_kill_and_close(initiated_by_user=ctx.interaction.user) if button_interaction.data["custom_id"] == "shutdown_cancel": # type: ignore[index, typeddict-item] await confirmation_message.edit( diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index adcdcf067..6bd37cd61 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -25,15 +25,16 @@ class BaseMakeApplicantCog(TeXBotBaseCog): """ Base making-applicant cog container class. - Defines the methods for making users into group-applicants, that are called by + Defines the methods for making users into group-applicants that are called by child cog container classes. """ async def _perform_make_applicant(self, ctx: TeXBotApplicationContext, applicant_member: discord.Member) -> None: # noqa: E501 """Perform the actual process of making the user into a group-applicant.""" - main_guild: discord.Guild = ctx.bot.main_guild - applicant_role: discord.Role = await ctx.bot.applicant_role - guest_role: discord.Role = await ctx.bot.guest_role + # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent + main_guild: discord.Guild = ctx.tex_bot.main_guild + applicant_role: discord.Role = await ctx.tex_bot.applicant_role + guest_role: discord.Role = await ctx.tex_bot.guest_role intro_channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, @@ -49,7 +50,7 @@ async def _perform_make_applicant(self, ctx: TeXBotApplicationContext, applicant ephemeral=True, ) - AUDIT_MESSAGE: Final[str] = f"{ctx.user} used TeX Bot Command \"Make User Applicant\"" + AUDIT_MESSAGE: Final[str] = f"{ctx.user} used TeX-Bot Command \"Make User Applicant\"" await applicant_member.add_roles(applicant_role, reason=AUDIT_MESSAGE) @@ -59,8 +60,7 @@ async def _perform_make_applicant(self, ctx: TeXBotApplicationContext, applicant await applicant_member.remove_roles(guest_role, reason=AUDIT_MESSAGE) logger.debug("Removed Guest role from user %s", applicant_member) - - tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213) + tex_emoji: discord.Emoji | None = self.tex_bot.get_emoji(743218410409820213) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") @@ -79,7 +79,7 @@ async def _perform_make_applicant(self, ctx: TeXBotApplicationContext, applicant logger.info( "Failed to add reactions because the user, %s, " - "has blocked the bot.", + "has blocked TeX-Bot.", recent_message.author, ) break @@ -91,7 +91,7 @@ class MakeApplicantSlashCommandCog(BaseMakeApplicantCog): """Cog class that defines the "/make_applicant" slash-command.""" @staticmethod - async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord.OptionChoice]: # noqa: E501 + async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord.OptionChoice]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable members. @@ -99,15 +99,15 @@ async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord options that have a member input-type. """ try: - guild: discord.Guild = ctx.bot.main_guild - applicant_role: discord.Role = await ctx.bot.applicant_role + main_guild: discord.Guild = ctx.tex_bot.main_guild + applicant_role: discord.Role = await ctx.tex_bot.applicant_role except (GuildDoesNotExistError, ApplicantRoleDoesNotExistError): return set() members: set[discord.Member] = { member for member - in guild.members + in main_guild.members if not member.bot and applicant_role not in member.roles } @@ -124,7 +124,6 @@ async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord in members } - @discord.slash_command( # type: ignore[no-untyped-call, misc] name="make-applicant", description=( @@ -133,7 +132,7 @@ async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord ) @discord.option( # type: ignore[no-untyped-call, misc] name="user", - description="The user to make an Applicant", + description="The user to make an Applicant.", input_type=str, autocomplete=discord.utils.basic_autocomplete(autocomplete_get_members), # type: ignore[arg-type] required=True, @@ -150,7 +149,7 @@ async def make_applicant(self, ctx: TeXBotApplicationContext, str_applicant_memb """ member_id_not_integer_error: ValueError try: - applicant_member: discord.Member = await self.bot.get_member_from_str_id( + applicant_member: discord.Member = await self.tex_bot.get_member_from_str_id( str_applicant_member_id, ) except ValueError as member_id_not_integer_error: @@ -163,20 +162,20 @@ async def make_applicant(self, ctx: TeXBotApplicationContext, str_applicant_memb class MakeApplicantContextCommandsCog(BaseMakeApplicantCog): """Cog class that defines the "/make_applicant" context commands.""" - @discord.user_command(name="Make Applicant") #type: ignore[no-untyped-call, misc] + @discord.user_command(name="Make Applicant") # type: ignore[no-untyped-call, misc] @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild - async def user_make_applicant(self, ctx: TeXBotApplicationContext, member: discord.Member) -> None: # noqa: E501 + async def user_make_applicant(self, ctx: TeXBotApplicationContext, member: discord.Member) -> None: # noqa: E501 """ Definition and callback response of the "make_applicant" user-context-command. The "make_applicant" user-context-command executes the same process as - the "make_applicant" slash-command, and thus gives the specified user the + the "make_applicant" slash-command and thus gives the specified user the "Applicant" role and removes the "Guest" role if they have it. """ await self._perform_make_applicant(ctx, member) - @discord.message_command(name="Make Message Author Applicant") # type: ignore[no-untyped-call, misc] + @discord.message_command(name="Make Message Author Applicant") # type: ignore[no-untyped-call, misc] @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild async def message_make_applicant(self, ctx: TeXBotApplicationContext, message: discord.Message) -> None: # noqa: E501 @@ -184,11 +183,11 @@ async def message_make_applicant(self, ctx: TeXBotApplicationContext, message: d Definition of the "message_make_applicant" message-context-command. The "make_applicant" message-context-command executes the same process as - the "make_applicant" slash-command, and thus gives the specified user the + the "make_applicant" slash-command and thus gives the specified user the "Applicant" role and removes the "Guest" role if they have it. """ try: - member: discord.Member = await self.bot.get_member_from_str_id( + member: discord.Member = await self.tex_bot.get_member_from_str_id( str(message.author.id), ) except ValueError: @@ -198,5 +197,6 @@ async def message_make_applicant(self, ctx: TeXBotApplicationContext, message: d ), ephemeral=True, ) + return await self._perform_make_applicant(ctx, member) diff --git a/cogs/make_member.py b/cogs/make_member.py index e3fe77bd7..b66687b63 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -26,7 +26,7 @@ ) from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") _GROUP_MEMBER_ID_ARGUMENT_DESCRIPTIVE_NAME: Final[str] = ( f"""{ @@ -107,8 +107,8 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) then gives the member the "Member" role. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - member_role: discord.Role = await self.bot.member_role - interaction_member: discord.Member = await ctx.bot.get_main_guild_member(ctx.user) + member_role: discord.Role = await self.tex_bot.member_role + interaction_member: discord.Member = await ctx.tex_bot.get_main_guild_member(ctx.user) if member_role in interaction_member.roles: await ctx.respond( @@ -120,11 +120,12 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) ) return - if not re.match(r"\A\d{7}\Z", group_member_id): + if not re.fullmatch(r"\A\d{7}\Z", group_member_id): await self.command_send_error( ctx, message=( - f"{group_member_id!r} is not a valid {self.bot.group_member_id_type} ID." + f"{group_member_id!r} is not a valid " + f"{self.tex_bot.group_member_id_type} ID." ), ) return @@ -132,14 +133,14 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) GROUP_MEMBER_ID_IS_ALREADY_USED: Final[bool] = await GroupMadeMember.objects.filter( hashed_group_member_id=GroupMadeMember.hash_group_member_id( group_member_id, - self.bot.group_member_id_type, + self.tex_bot.group_member_id_type, ), ).aexists() if GROUP_MEMBER_ID_IS_ALREADY_USED: # noinspection PyUnusedLocal committee_mention: str = "committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_mention = (await self.bot.committee_role).mention + committee_mention = (await self.tex_bot.committee_role).mention await ctx.respond( ( @@ -159,7 +160,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) "Expires": "0", } request_cookies: dict[str, str] = { - ".ASPXAUTH": settings["MEMBERS_LIST_URL_SESSION_COOKIE"], + ".ASPXAUTH": settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"], } async with aiohttp.ClientSession(headers=request_headers, cookies=request_cookies) as http_session: # noqa: E501, SIM117 async with http_session.get(url=settings["MEMBERS_LIST_URL"]) as http_response: @@ -212,11 +213,11 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) await self.command_send_error( ctx, message=( - f"You must be a member of {self.bot.group_full_name} " + f"You must be a member of {self.tex_bot.group_full_name} " "to use this command.\n" f"The provided {_GROUP_MEMBER_ID_ARGUMENT_NAME} must match " - f"the {self.bot.group_member_id_type} ID " - f"that you purchased your {self.bot.group_short_name} membership with." + f"the {self.tex_bot.group_member_id_type} ID " + f"that you purchased your {self.tex_bot.group_short_name} membership with." ), ) return @@ -245,7 +246,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) await ctx.respond("Successfully made you a member!", ephemeral=True) try: - guest_role: discord.Role = await self.bot.guest_role + guest_role: discord.Role = await self.tex_bot.guest_role except GuestRoleDoesNotExistError: logger.warning( "\"/makemember\" command used but the \"Guest\" role does not exist. " @@ -259,9 +260,10 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) reason="TeX Bot slash-command: \"/makemember\"", ) + # noinspection PyUnusedLocal applicant_role: discord.Role | None = None with contextlib.suppress(ApplicantRoleDoesNotExistError): - applicant_role = await ctx.bot.applicant_role + applicant_role = await ctx.tex_bot.applicant_role if applicant_role and applicant_role in interaction_member.roles: await interaction_member.remove_roles( diff --git a/cogs/ping.py b/cogs/ping.py index 639f8c906..f3f3d13aa 100644 --- a/cogs/ping.py +++ b/cogs/ping.py @@ -23,7 +23,7 @@ async def ping(self, ctx: TeXBotApplicationContext) -> None: random.choices( [ "Pong!", - "64 bytes from TeX-Bot: icmp_seq=1 ttl=63 time=0.01 ms", + "`64 bytes from TeX-Bot: icmp_seq=1 ttl=63 time=0.01 ms`", ], weights=( 100 - settings["PING_COMMAND_EASTER_EGG_PROBABILITY"], diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 3505cef13..0c1c47b17 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -10,8 +10,9 @@ import itertools import logging import re +from collections.abc import Set from logging import Logger -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, override import discord import parsedatetime @@ -27,14 +28,14 @@ from collections.abc import Iterator -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class RemindMeCommandCog(TeXBotBaseCog): - """Cog class that defines the "/remindme" command and its call-back method.""" + """Cog class that defines the "/remind-me" command and its call-back method.""" @staticmethod - async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: # noqa: C901, PLR0912, PLR0915 + async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: C901, PLR0912, PLR0915, E501 """ Autocomplete callable that generates the common delay input values. @@ -69,7 +70,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: delay_choices: set[str] = set() - if re.match(r"\Ain? ?\Z", ctx.value): + if re.fullmatch(r"\Ain? ?\Z", ctx.value): FORMATTED_TIME_NUMS: Final[Iterator[tuple[int, str, str]]] = itertools.product( range(1, 150), {"", " "}, @@ -89,7 +90,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: return {f"in {delay_choice}" for delay_choice in delay_choices} match: re.Match[str] | None - if match := re.match(r"\Ain (?P\d{0,3})\Z", ctx.value): + if match := re.fullmatch(r"\Ain (?P\d{0,3})\Z", ctx.value): for joiner, has_s in itertools.product({"", " "}, {"", "s"}): delay_choices.update( f"""{match.group("partial_date")}{joiner}{time_choice}{has_s}""" @@ -102,7 +103,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: current_year: int = discord.utils.utcnow().year - if re.match(r"\A\d{1,3}\Z", ctx.value): + if re.fullmatch(r"\A\d{1,3}\Z", ctx.value): for joiner, has_s in itertools.product({"", " "}, {"", "s"}): delay_choices.update( f"{joiner}{time_choice}{has_s}" @@ -124,7 +125,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: if month < 10: delay_choices.add(f"{joiner}0{month}{joiner}{year}") - elif match := re.match(r"\A\d{1,3}(?P ?[A-Za-z]*)\Z", ctx.value): + elif match := re.fullmatch(r"\A\d{1,3}(?P ?[A-Za-z]*)\Z", ctx.value): FORMATTED_TIME_CHOICES: Final[Iterator[tuple[str, str, str]]] = itertools.product( {"", " "}, TIME_CHOICES, @@ -142,7 +143,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: if match.group("ctx_time_choice").casefold() == formatted_time_choice[:slice_size]: # noqa: E501 delay_choices.add(formatted_time_choice[slice_size:]) - elif match := re.match(r"\A(?P\d{1,2}) ?[/\-.] ?\Z", ctx.value): + elif match := re.fullmatch(r"\A(?P\d{1,2}) ?[/\-.] ?\Z", ctx.value): if 1 <= int(match.group("date")) <= 31: FORMATTED_DAY_AND_JOINER_DATE_CHOICES: Final[Iterator[tuple[int, int, str]]] = itertools.product( # noqa: E501 range(1, 12), @@ -154,18 +155,18 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: if month < 10: delay_choices.add(f"0{month}{joiner}{year}") - elif match := re.match(r"\A(?P\d{1,2}) ?[/\-.] ?(?P\d{1,2})\Z", ctx.value): # noqa: E501 + elif match := re.fullmatch(r"\A(?P\d{1,2}) ?[/\-.] ?(?P\d{1,2})\Z", ctx.value): # noqa: E501 if 1 <= int(match.group("date")) <= 31 and 1 <= int(match.group("month")) <= 12: for year in range(current_year, current_year + 40): for joiner in ("/", " / ", "-", " - ", ".", " . "): delay_choices.add(f"{joiner}{year}") - elif match := re.match(r"\A(?P\d{1,2}) ?[/\-.] ?(?P\d{1,2}) ?[/\-.] ?\Z", ctx.value): # noqa: E501 + elif match := re.fullmatch(r"\A(?P\d{1,2}) ?[/\-.] ?(?P\d{1,2}) ?[/\-.] ?\Z", ctx.value): # noqa: E501 if 1 <= int(match.group("date")) <= 31 and 1 <= int(match.group("month")) <= 12: for year in range(current_year, current_year + 40): delay_choices.add(f"{year}") - elif match := re.match(r"\A(?P\d{1,2}) ?[/\-.] ?(?P\d{1,2}) ?[/\-.] ?(?P\d{1,3})\Z", ctx.value): # noqa: E501 + elif match := re.fullmatch(r"\A(?P\d{1,2}) ?[/\-.] ?(?P\d{1,2}) ?[/\-.] ?(?P\d{1,3})\Z", ctx.value): # noqa: E501 if 1 <= int(match.group("date")) <= 31 and 1 <= int(match.group("month")) <= 12: for year in range(current_year, current_year + 40): delay_choices.add(f"{year}"[len(match.group("partial_year")):]) @@ -173,13 +174,13 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: return {f"{ctx.value}{delay_choice}".casefold() for delay_choice in delay_choices} @discord.slash_command( # type: ignore[no-untyped-call, misc] - name="remindme", + name="remind-me", description="Responds with the given message after the specified time.", ) @discord.option( # type: ignore[no-untyped-call, misc] name="delay", input_type=str, - description="The amount of time to wait before reminding you", + description="The amount of time to wait before reminding you.", required=True, autocomplete=discord.utils.basic_autocomplete(autocomplete_get_delays), # type: ignore[arg-type] ) @@ -222,7 +223,7 @@ async def remind_me(self, ctx: TeXBotApplicationContext, delay: str, message: st channel_type=ctx.channel.type, ) except ValidationError as create_discord_reminder_error: - error_is_already_exists: bool = ( + ERROR_IS_ALREADY_EXISTS: Final[bool] = ( "__all__" in create_discord_reminder_error.message_dict and any( "already exists" in error @@ -230,14 +231,13 @@ async def remind_me(self, ctx: TeXBotApplicationContext, delay: str, message: st in create_discord_reminder_error.message_dict["__all__"] ) ) - if not error_is_already_exists: + if not ERROR_IS_ALREADY_EXISTS: await self.command_send_error(ctx, message="An unrecoverable error occurred.") logger.critical( "Error when creating DiscordReminder object: %s", create_discord_reminder_error, ) - await self.bot.close() - return + await self.tex_bot.close() await self.command_send_error( ctx, @@ -261,12 +261,14 @@ async def remind_me(self, ctx: TeXBotApplicationContext, delay: str, message: st class ClearRemindersBacklogTaskCog(TeXBotBaseCog): """Cog class that defines the clear_reminders_backlog task.""" + @override def __init__(self, bot: TeXBot) -> None: """Start all task managers when this cog is initialised.""" self.clear_reminders_backlog.start() super().__init__(bot) + @override def cog_unload(self) -> None: """ Unload hook that ends all running tasks whenever the tasks cog is unloaded. @@ -301,7 +303,7 @@ async def clear_reminders_backlog(self) -> None: ), _reminder=reminder, ), - self.bot.users, + self.tex_bot.users, ) if not user: @@ -312,7 +314,8 @@ async def clear_reminders_backlog(self) -> None: await reminder.adelete() continue - channel: discord.PartialMessageable = self.bot.get_partial_messageable( + # noinspection PyUnresolvedReferences + channel: discord.PartialMessageable = self.tex_bot.get_partial_messageable( reminder.channel_id, type=( discord.ChannelType(reminder.channel_type.value) @@ -331,8 +334,7 @@ async def clear_reminders_backlog(self) -> None: "Reminder's channel_id must refer to a valid text channel/DM.", ), ) - await self.bot.close() - return + await self.tex_bot.close() await channel.send( "**Sorry it's a bit late! " @@ -345,4 +347,4 @@ async def clear_reminders_backlog(self) -> None: @clear_reminders_backlog.before_loop async def before_tasks(self) -> None: """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" - await self.bot.wait_until_ready() + await self.tex_bot.wait_until_ready() diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index f6b404411..84d4cf484 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -9,7 +9,7 @@ import functools import logging from logging import Logger -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, override import discord from discord import AuditLogAction @@ -28,12 +28,13 @@ if TYPE_CHECKING: import datetime -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class SendGetRolesRemindersTaskCog(TeXBotBaseCog): """Cog class that defines the send_get_roles_reminders task.""" + @override def __init__(self, bot: TeXBot) -> None: """Start all task managers when this cog is initialised.""" if settings["SEND_GET_ROLES_REMINDERS"]: @@ -41,6 +42,7 @@ def __init__(self, bot: TeXBot) -> None: super().__init__(bot) + @override def cog_unload(self) -> None: """ Unload hook that ends all running tasks whenever the tasks cog is unloaded. @@ -67,13 +69,13 @@ async def send_get_roles_reminders(self) -> None: reminders are sent. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - guild: discord.Guild = self.bot.main_guild - guest_role: discord.Role = await self.bot.guest_role + main_guild: discord.Guild = self.tex_bot.main_guild + guest_role: discord.Role = await self.tex_bot.guest_role # noinspection PyUnusedLocal - roles_channel_mention: str = "#roles" + roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.bot.roles_channel).mention + roles_channel_mention = (await self.tex_bot.roles_channel).mention # noinspection SpellCheckingInspection OPT_IN_ROLE_NAMES: Final[frozenset[str]] = frozenset( @@ -110,7 +112,7 @@ async def send_get_roles_reminders(self) -> None: ) member: discord.Member - for member in guild.members: + for member in main_guild.members: member_requires_opt_in_roles_reminder: bool = ( not member.bot and utils.is_member_inducted(member) @@ -138,7 +140,7 @@ async def send_get_roles_reminders(self) -> None: guest_role_received_time = await anext( log.created_at async for log - in guild.audit_logs(action=AuditLogAction.member_role_update) + in main_guild.audit_logs(action=AuditLogAction.member_role_update) if ( log.target == member and guest_role not in log.before.roles @@ -153,7 +155,7 @@ async def send_get_roles_reminders(self) -> None: if time_since_role_received <= settings["SEND_GET_ROLES_REMINDERS_DELAY"]: continue - if member not in guild.members: # HACK: Caching errors can cause the member to no longer be part of the guild at this point, so this check must be performed before sending that member a message # noqa: FIX004 + if member not in main_guild.members: # HACK: Caching errors can cause the member to no longer be part of the guild at this point, so this check must be performed before sending that member a message # noqa: FIX004 logger.info( ( "Member with ID: %s does not need to be sent a reminder " @@ -166,7 +168,7 @@ async def send_get_roles_reminders(self) -> None: try: await member.send( "Hey! It seems like you have been given the `@Guest` role " - f"on the {self.bot.group_short_name} Discord server " + f"on the {self.tex_bot.group_short_name} Discord server " " but have not yet nabbed yourself any opt-in roles.\n" f"You can head to {roles_channel_mention} " "and click on the icons to get optional roles like pronouns " @@ -183,4 +185,4 @@ async def send_get_roles_reminders(self) -> None: @send_get_roles_reminders.before_loop async def before_tasks(self) -> None: """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" - await self.bot.wait_until_ready() + await self.tex_bot.wait_until_ready() diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 80394599c..a61ba1aa7 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -8,7 +8,7 @@ import functools import logging from logging import Logger -from typing import Final +from typing import Final, override import discord import emoji @@ -30,12 +30,13 @@ capture_guild_does_not_exist_error, ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class SendIntroductionRemindersTaskCog(TeXBotBaseCog): """Cog class that defines the send_introduction_reminders task.""" + @override def __init__(self, bot: TeXBot) -> None: """Start all task managers when this cog is initialised.""" if settings["SEND_INTRODUCTION_REMINDERS"]: @@ -46,6 +47,7 @@ def __init__(self, bot: TeXBot) -> None: super().__init__(bot) + @override def cog_unload(self) -> None: """ Unload hook that ends all running tasks whenever the tasks cog is unloaded. @@ -57,8 +59,8 @@ def cog_unload(self) -> None: @TeXBotBaseCog.listener() async def on_ready(self) -> None: """Add OptOutIntroductionRemindersView to the bot's list of permanent views.""" - self.bot.add_view( - self.OptOutIntroductionRemindersView(self.bot), + self.tex_bot.add_view( + self.OptOutIntroductionRemindersView(self.tex_bot), ) @tasks.loop(**settings["SEND_INTRODUCTION_REMINDERS_INTERVAL"]) # type: ignore[misc] @@ -79,10 +81,10 @@ async def send_introduction_reminders(self) -> None: reminders are sent. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild member: discord.Member - for member in guild.members: + for member in main_guild.members: if utils.is_member_inducted(member) or member.bot: continue @@ -129,16 +131,16 @@ async def send_introduction_reminders(self) -> None: async for message in member.history(): # noinspection PyUnresolvedReferences - message_contains_opt_in_out_button: bool = ( + MESSAGE_CONTAINS_OPT_IN_OUT_BUTTON: bool = ( bool(message.components) and isinstance(message.components[0], discord.ActionRow) and isinstance(message.components[0].children[0], discord.Button) and message.components[0].children[0].custom_id == "opt_out_introduction_reminders_button" # noqa: E501 ) - if message_contains_opt_in_out_button: + if MESSAGE_CONTAINS_OPT_IN_OUT_BUTTON: await message.edit(view=None) - if member not in guild.members: # HACK: Caching errors can cause the member to no longer be part of the guild at this point, so this check must be performed before sending that member a message # noqa: FIX004 + if member not in main_guild.members: # HACK: Caching errors can cause the member to no longer be part of the guild at this point, so this check must be performed before sending that member a message # noqa: FIX004 logger.info( ( "Member with ID: %s does not need to be sent a reminder " @@ -152,13 +154,13 @@ async def send_introduction_reminders(self) -> None: await member.send( content=( "Hey! It seems like you joined " - f"the {self.bot.group_short_name} Discord server " + f"the {self.tex_bot.group_short_name} Discord server " "but have not yet introduced yourself.\n" "You will only get access to the rest of the server after sending " "an introduction message." ), view=( - self.OptOutIntroductionRemindersView(self.bot) + self.OptOutIntroductionRemindersView(self.tex_bot) if settings["SEND_INTRODUCTION_REMINDERS"] == "interval" else None # type: ignore[arg-type] ), @@ -181,13 +183,14 @@ class OptOutIntroductionRemindersView(View): This discord.View contains a single button that can change the state of whether the member will be sent reminders to send an introduction message in your group's Discord guild. - The view object will be sent to the member's DMs, after a delay period after + The view object will be sent to the member's DMs after a delay period after joining your group's Discord guild. """ - def __init__(self, bot: TeXBot) -> None: - """Initialize a new discord.View, to opt-in/out of introduction reminders.""" - self.bot: TeXBot = bot + @override + def __init__(self, tex_bot: TeXBot) -> None: + """Initialise a new discord.View, to opt-in/out of introduction reminders.""" + self.tex_bot: TeXBot = tex_bot super().__init__(timeout=None) @@ -199,7 +202,7 @@ async def send_error(self, interaction: discord.Interaction, error_code: str | N to the given interaction. """ await TeXBotBaseCog.send_error( - self.bot, + self.tex_bot, interaction, interaction_name="opt_out_introduction_reminders", error_code=error_code, @@ -225,30 +228,30 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B BUTTON_WILL_MAKE_OPT_OUT: Final[bool] = bool( button.style == discord.ButtonStyle.red or str(button.emoji) == emoji.emojize(":no_good:", language="alias") - or (button.label and "Opt-out" in button.label), + or (button.label and "Opt-out" in button.label) # noqa: COM812 ) - _BUTTON_WILL_MAKE_OPT_IN: Final[bool] = bool( - button.style == discord.ButtonStyle.green - or str(button.emoji) == emoji.emojize( - ":raised_hand:", - language="alias", - ) - or button.label and "Opt back in" in button.label) + BUTTON_WILL_MAKE_OPT_IN: Final[bool] = bool( + button.style == discord.ButtonStyle.green + or str(button.emoji) == emoji.emojize(":raised_hand:", language="alias") + or (button.label and "Opt back in" in button.label) # noqa: COM812 + ) INCOMPATIBLE_BUTTONS: Final[bool] = bool( - (BUTTON_WILL_MAKE_OPT_OUT and _BUTTON_WILL_MAKE_OPT_IN) - or (not BUTTON_WILL_MAKE_OPT_OUT and not _BUTTON_WILL_MAKE_OPT_IN), + (BUTTON_WILL_MAKE_OPT_OUT and BUTTON_WILL_MAKE_OPT_IN) + or (not BUTTON_WILL_MAKE_OPT_OUT and not BUTTON_WILL_MAKE_OPT_IN) # noqa: COM812 ) if INCOMPATIBLE_BUTTONS: INCOMPATIBLE_BUTTONS_MESSAGE: Final[str] = "Conflicting buttons pressed" raise ValueError(INCOMPATIBLE_BUTTONS_MESSAGE) + del BUTTON_WILL_MAKE_OPT_IN + if not interaction.user: await self.send_error(interaction) return try: - interaction_member: discord.Member = await self.bot.get_main_guild_member( + interaction_member: discord.Member = await self.tex_bot.get_main_guild_member( interaction.user, ) except DiscordMemberNotInMainGuildError: @@ -256,7 +259,7 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B interaction, message=( f"You must be a member " - f"of the {self.bot.group_short_name} Discord server " + f"of the {self.tex_bot.group_short_name} Discord server " f"""to opt{ "-out of" if BUTTON_WILL_MAKE_OPT_OUT else " back in to" } introduction reminders.""" @@ -314,4 +317,4 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B @send_introduction_reminders.before_loop async def before_tasks(self) -> None: """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" - await self.bot.wait_until_ready() + await self.tex_bot.wait_until_ready() diff --git a/cogs/source.py b/cogs/source.py index 9e53be6e9..faef0a2da 100644 --- a/cogs/source.py +++ b/cogs/source.py @@ -14,14 +14,15 @@ class SourceCommandCog(TeXBotBaseCog): """Cog class that defines the "/source" command and its call-back method.""" @discord.slash_command( # type: ignore[no-untyped-call, misc] - description="Displays information about the source code of this bot.", + description="Displays information about the source code of TeX-Bot.", ) async def source(self, ctx: TeXBotApplicationContext) -> None: """Definition & callback response of the "source" command.""" await ctx.respond( ( - "TeX-Bot is an open-source project " - "made specifically for the CSS Discord server!\n" + f"{self.tex_bot.user.mention if self.tex_bot.user else "**`@TeX-Bot`**"} " + "is an open-source project, " + "originally made to help manage [the UoB CSS Discord server](https://cssbham.com/discord)!\n" "You can see and contribute to the source code at [CSSUoB/TeX-Bot-Py-V2](https://github.com/CSSUoB/TeX-Bot-Py-V2)." ), ephemeral=True, diff --git a/cogs/startup.py b/cogs/startup.py index 53c9d8ef3..ef35bf2da 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -7,6 +7,7 @@ import logging from logging import Logger +from typing import Final import discord from discord_logging.handler import DiscordHandler @@ -24,7 +25,7 @@ ) from utils import TeXBotBaseCog -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class StartupCog(TeXBotBaseCog): @@ -33,17 +34,17 @@ class StartupCog(TeXBotBaseCog): @TeXBotBaseCog.listener() async def on_ready(self) -> None: """ - Populate the shortcut accessors of the bot after initialisation. + Populate the shortcut accessors of TeX-Bot after initialisation. - Shortcut accessors should only be populated once the bot is ready to make API requests. + Shortcut accessors should only be populated onceTeX-Bot is ready to make API requests. """ if settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"]: discord_logging_handler: logging.Handler = DiscordHandler( - self.bot.user.name if self.bot.user else "TeXBot", + self.tex_bot.user.name if self.tex_bot.user else "TeXBot", settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"], avatar_url=( - self.bot.user.avatar.url - if self.bot.user and self.bot.user.avatar + self.tex_bot.user.avatar.url + if self.tex_bot.user and self.tex_bot.user.avatar else None ), ) @@ -62,30 +63,31 @@ async def on_ready(self) -> None: ) try: - main_guild: discord.Guild | None = self.bot.main_guild + main_guild: discord.Guild | None = self.tex_bot.main_guild except GuildDoesNotExistError: - main_guild = self.bot.get_guild(settings["DISCORD_GUILD_ID"]) + main_guild = self.tex_bot.get_guild(settings["_DISCORD_MAIN_GUILD_ID"]) if main_guild: - self.bot.set_main_guild(main_guild) + self.tex_bot.set_main_guild(main_guild) if not main_guild: - if self.bot.application_id: + if self.tex_bot.application_id: logger.info( "Invite URL: %s", utils.generate_invite_url( - self.bot.application_id, - settings["DISCORD_GUILD_ID"]), + self.tex_bot.application_id, + settings["_DISCORD_MAIN_GUILD_ID"]), ) - logger.critical(GuildDoesNotExistError(guild_id=settings["DISCORD_GUILD_ID"])) - await self.bot.close() - return + logger.critical(GuildDoesNotExistError( + guild_id=settings["_DISCORD_MAIN_GUILD_ID"]), + ) + await self.tex_bot.close() - if self.bot.application_id: + if self.tex_bot.application_id: logger.debug( "Invite URL: %s", utils.generate_invite_url( - self.bot.application_id, - settings["DISCORD_GUILD_ID"]), + self.tex_bot.application_id, + settings["_DISCORD_MAIN_GUILD_ID"]), ) if not discord.utils.get(main_guild.roles, name="Committee"): @@ -106,11 +108,11 @@ async def on_ready(self) -> None: if not discord.utils.get(main_guild.text_channels, name="general"): logger.warning(GeneralChannelDoesNotExistError()) - if settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"] != "DM": + if settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] != "DM": manual_moderation_warning_message_location_exists: bool = bool( discord.utils.get( main_guild.text_channels, - name=settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"], + name=settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"], ), ) if not manual_moderation_warning_message_location_exists: @@ -119,10 +121,10 @@ async def on_ready(self) -> None: "The channel %s does not exist, so cannot be used as the location " "for sending manual-moderation warning messages" ), - repr(settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"]), + repr(settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"]), ) manual_moderation_warning_message_location_similar_to_dm: bool = ( - settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"].lower() + settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"].lower() in ("dm", "dms") ) if manual_moderation_warning_message_location_similar_to_dm: @@ -135,7 +137,6 @@ async def on_ready(self) -> None: ), repr("DM"), ) - await self.bot.close() - return + await self.tex_bot.close() - logger.info("Ready! Logged in as %s", self.bot.user) + logger.info("Ready! Logged in as %s", self.tex_bot.user) diff --git a/cogs/stats.py b/cogs/stats.py index e27c263f3..92ed58079 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -8,6 +8,7 @@ import io import math import re +from collections.abc import AsyncIterable from typing import TYPE_CHECKING, Final import discord @@ -157,11 +158,11 @@ class StatsCommandsCog(TeXBotBaseCog): ).replace("the", "").replace("THE", "").replace("The", "").strip() ) else "our community group's" - } Discord server""" + }""" stats: discord.SlashCommandGroup = discord.SlashCommandGroup( - "stats", - f"Various statistics about {_DISCORD_SERVER_NAME}", + name="stats", + description=f"Various statistics about {_DISCORD_SERVER_NAME} Discord server", ) # noinspection SpellCheckingInspection @@ -186,10 +187,13 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str The "channel_stats" command sends a graph of the stats about messages sent in the given channel. """ + # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent + main_guild: discord.Guild = self.tex_bot.main_guild + channel_id: int = ctx.channel_id if str_channel_id: - if not re.match(r"\A\d{17,20}\Z", str_channel_id): + if not re.fullmatch(r"\A\d{17,20}\Z", str_channel_id): await self.command_send_error( ctx, message=f"{str_channel_id!r} is not a valid channel ID.", @@ -198,10 +202,8 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str channel_id = int(str_channel_id) - # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - guild: discord.Guild = self.bot.main_guild channel: discord.TextChannel | None = discord.utils.get( - guild.text_channels, + main_guild.text_channels, id=channel_id, ) if not channel: @@ -217,10 +219,10 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str role_name: str for role_name in settings["STATISTICS_ROLES"]: - if discord.utils.get(guild.roles, name=role_name): + if discord.utils.get(main_guild.roles, name=role_name): message_counts[f"@{role_name}"] = 0 - message_history_period: discord.iterators.HistoryIterator = channel.history( + message_history_period: AsyncIterable[discord.Message] = channel.history( after=discord.utils.utcnow() - settings["STATISTICS_DAYS"], ) message: discord.Message @@ -299,8 +301,8 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: of your group's Discord guild. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - guild: discord.Guild = self.bot.main_guild - guest_role: discord.Role = await self.bot.guest_role + main_guild: discord.Guild = self.tex_bot.main_guild + guest_role: discord.Role = await self.tex_bot.guest_role await ctx.defer(ephemeral=True) @@ -311,11 +313,11 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: role_name: str for role_name in settings["STATISTICS_ROLES"]: - if discord.utils.get(guild.roles, name=role_name): + if discord.utils.get(main_guild.roles, name=role_name): message_counts["roles"][f"@{role_name}"] = 0 channel: discord.TextChannel - for channel in guild.text_channels: + for channel in main_guild.text_channels: member_has_access_to_channel: bool = channel.permissions_for( guest_role, ).is_superset( @@ -326,7 +328,7 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: message_counts["channels"][f"#{channel.name}"] = 0 - message_history_period: discord.iterators.HistoryIterator = channel.history( + message_history_period: AsyncIterable[discord.Message] = channel.history( after=discord.utils.utcnow() - settings["STATISTICS_DAYS"], ) message: discord.Message @@ -383,12 +385,13 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: })""" ), title=( - f"Most Active Roles in the {self.bot.group_short_name} Discord Server" + "Most Active Roles in " + f"the {self.tex_bot.group_short_name} Discord Server" ), filename="roles_server_stats.png", description=( "Bar chart of the number of messages sent by different roles " - f"in the {self.bot.group_short_name} Discord server." + f"in the {self.tex_bot.group_short_name} Discord server." ), extra_text=( "Messages sent by members with multiple roles are counted once " @@ -409,12 +412,12 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: ), title=( "Most Active Channels " - f"in the {self.bot.group_short_name} Discord Server" + f"in the {self.tex_bot.group_short_name} Discord Server" ), filename="channels_server_stats.png", description=( "Bar chart of the number of messages sent in different text channels " - f"in the {self.bot.group_short_name} Discord server." + f"in the {self.tex_bot.group_short_name} Discord server." ), ), ], @@ -433,16 +436,17 @@ async def user_stats(self, ctx: TeXBotApplicationContext) -> None: member. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - guild: discord.Guild = self.bot.main_guild - interaction_member: discord.Member = await self.bot.get_main_guild_member(ctx.user) - guest_role: discord.Role = await self.bot.guest_role + main_guild: discord.Guild = self.tex_bot.main_guild + interaction_member: discord.Member = await self.tex_bot.get_main_guild_member(ctx.user) + guest_role: discord.Role = await self.tex_bot.guest_role if guest_role not in interaction_member.roles: await self.command_send_error( ctx, message=( "You must be inducted as a guest member " - f"of the {self.bot.group_short_name} Discord server to use this command." + f"of the {self.tex_bot.group_short_name} Discord server " + "to use this command." ), ) return @@ -452,7 +456,7 @@ async def user_stats(self, ctx: TeXBotApplicationContext) -> None: message_counts: dict[str, int] = {"Total": 0} channel: discord.TextChannel - for channel in guild.text_channels: + for channel in main_guild.text_channels: member_has_access_to_channel: bool = channel.permissions_for( guest_role, ).is_superset( @@ -463,7 +467,7 @@ async def user_stats(self, ctx: TeXBotApplicationContext) -> None: message_counts[f"#{channel.name}"] = 0 - message_history_period: discord.iterators.HistoryIterator = channel.history( + message_history_period: AsyncIterable[discord.Message] = channel.history( after=discord.utils.utcnow() - settings["STATISTICS_DAYS"], ) message: discord.Message @@ -496,19 +500,20 @@ async def user_stats(self, ctx: TeXBotApplicationContext) -> None: ), title=( "Your Most Active Channels " - f"in the {self.bot.group_short_name} Discord Server" + f"in the {self.tex_bot.group_short_name} Discord Server" ), filename=f"{ctx.user}_stats.png", description=( f"Bar chart of the number of messages sent by {ctx.user} " - f"in different channels in the {self.bot.group_short_name} Discord server." + "in different channels in " + f"the {self.tex_bot.group_short_name} Discord server." ), ), ) # noinspection SpellCheckingInspection @stats.command( - name="leftmembers", + name="left-members", description=f"Displays the stats about members that have left {_DISCORD_SERVER_NAME}", ) async def left_member_stats(self, ctx: TeXBotApplicationContext) -> None: @@ -519,7 +524,7 @@ async def left_member_stats(self, ctx: TeXBotApplicationContext) -> None: had when they left your group's Discord guild. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild await ctx.defer(ephemeral=True) @@ -529,7 +534,7 @@ async def left_member_stats(self, ctx: TeXBotApplicationContext) -> None: role_name: str for role_name in settings["STATISTICS_ROLES"]: - if discord.utils.get(guild.roles, name=role_name): + if discord.utils.get(main_guild.roles, name=role_name): left_member_counts[f"@{role_name}"] = 0 left_member: LeftDiscordMember @@ -563,16 +568,16 @@ async def left_member_stats(self, ctx: TeXBotApplicationContext) -> None: x_label="Role Name", y_label=( "Number of Members that have left " - f"the {self.bot.group_short_name} Discord Server" + f"the {self.tex_bot.group_short_name} Discord Server" ), title=( "Most Common Roles that Members had when they left " - f"the {self.bot.group_short_name} Discord Server" + f"the {self.tex_bot.group_short_name} Discord Server" ), filename="left_members_stats.png", description=( "Bar chart of the number of members with different roles " - f"that have left the {self.bot.group_short_name} Discord server." + f"that have left the {self.tex_bot.group_short_name} Discord server." ), extra_text=( "Members that left with multiple roles " @@ -586,7 +591,7 @@ async def left_member_stats(self, ctx: TeXBotApplicationContext) -> None: @capture_guild_does_not_exist_error async def on_member_leave(self, member: discord.Member) -> None: """Update the stats of the roles that members had when they left your Discord guild.""" - if member.guild != self.bot.main_guild or member.bot: + if member.guild != self.tex_bot.main_guild or member.bot: return await LeftDiscordMember.objects.acreate( diff --git a/cogs/strike.py b/cogs/strike.py index 48be3c6d4..aa5097c3d 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -19,7 +19,7 @@ import datetime import logging import re -from collections.abc import Mapping +from collections.abc import Mapping, Set from logging import Logger from typing import Final @@ -49,12 +49,19 @@ ) from utils.message_sender_components import ( ChannelMessageSender, - MessageSenderComponent, + MessageSavingSenderComponent, ResponseMessageSender, ) logger: Final[Logger] = logging.getLogger("TeX-Bot") +FORMATTED_MODERATION_ACTIONS: Final[Mapping[discord.AuditLogAction, str]] = { + discord.AuditLogAction.member_update: "timed-out", + discord.AuditLogAction.kick: "kicked", + discord.AuditLogAction.ban: "banned", + discord.AuditLogAction.auto_moderation_user_communication_disabled: "timed-out", +} + async def perform_moderation_action(strike_user: discord.Member, strikes: int, committee_member: discord.Member | discord.User) -> None: # noqa: E501 """ @@ -97,7 +104,7 @@ class ConfirmStrikeMemberView(View): ) async def yes_strike_member_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 """ - Delete the message associated with the view, when the Yes button is pressed. + Delete the message associated with the view when the Yes button is pressed. This function is attached as a button's callback, so will run whenever the button is pressed. @@ -114,7 +121,7 @@ async def yes_strike_member_button_callback(self, _: discord.Button, interaction ) async def no_strike_member_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 """ - Delete the message associated with the view, when the No button is pressed. + Delete the message associated with the view when the No button is pressed. This function is attached as a button's callback, so will run whenever the button is pressed. @@ -135,7 +142,7 @@ class ConfirmManualModerationView(View): ) async def yes_manual_moderation_action_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 """ - Delete the message associated with the view, when the Yes button is pressed. + Delete the message associated with the view when the Yes button is pressed. This function is attached as a button's callback, so will run whenever the button is pressed. @@ -153,7 +160,7 @@ async def yes_manual_moderation_action_button_callback(self, _: discord.Button, ) async def no_manual_moderation_action_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 """ - Delete the message associated with the view, when the No button is pressed. + Delete the message associated with the view when the No button is pressed. This function is attached as a button's callback, so will run whenever the button is pressed. @@ -175,7 +182,7 @@ class ConfirmStrikesOutOfSyncWithBanView(View): ) async def yes_out_of_sync_ban_member_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 """ - Delete the message associated with the view, when the Yes button is pressed. + Delete the message associated with the view when the Yes button is pressed. This function is attached as a button's callback, so will run whenever the button is pressed. @@ -193,7 +200,7 @@ async def yes_out_of_sync_ban_member_button_callback(self, _: discord.Button, in ) async def no_out_of_sync_ban_member_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 """ - Delete the message associated with the view, when the No button is pressed. + Delete the message associated with the view when the No button is pressed. This function is attached as a button's callback, so will run whenever the button is pressed. @@ -217,15 +224,15 @@ class BaseStrikeCog(TeXBotBaseCog): async def _send_strike_user_message(self, strike_user: discord.User | discord.Member, member_strikes: DiscordMemberStrikes) -> None: # noqa: E501 # noinspection PyUnusedLocal - rules_channel_mention: str = "`#welcome`" + rules_channel_mention: str = "**`#welcome`**" with contextlib.suppress(RulesChannelDoesNotExistError): - rules_channel_mention = (await self.bot.rules_channel).mention + rules_channel_mention = (await self.tex_bot.rules_channel).mention includes_ban_message: str = ( ( "\nBecause you now have been given 3 strikes, you have been banned from " - f"the {self.bot.group_short_name} Discord server " - f"and we have contacted {self.bot.group_moderation_contact} for " + f"the {self.tex_bot.group_short_name} Discord server " + f"and we have contacted {self.tex_bot.group_moderation_contact} for " "further action & advice." ) if member_strikes.strikes >= 3 @@ -240,25 +247,26 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me await strike_user.send( "Hi, a recent incident occurred in which you may have broken one or more of " - f"the {self.bot.group_short_name} Discord server's rules.\n" + f"the {self.tex_bot.group_short_name} Discord server's rules.\n" "We have increased the number of strikes associated with your account " f"to {actual_strike_amount} and " "the corresponding moderation action will soon be applied to you. " "To find what moderation action corresponds to which strike level, " - f"you can view the {self.bot.group_short_name} Discord server moderation document " + "you can view " + f"the {self.tex_bot.group_short_name} Discord server moderation document " f"[here](<{settings.MODERATION_DOCUMENT_URL}>)\nPlease ensure you have read " f"the rules in {rules_channel_mention} so that your future behaviour adheres " f"to them.{includes_ban_message}\n\nA committee member will be in contact " "with you shortly, to discuss this further.", ) - async def _confirm_perform_moderation_action(self, message_sender_component: MessageSenderComponent, interaction_user: discord.User, strike_user: discord.Member, confirm_strike_message: str, actual_strike_amount: int, button_callback_channel: discord.TextChannel | discord.DMChannel) -> None: # noqa: E501 + async def _confirm_perform_moderation_action(self, message_sender_component: MessageSavingSenderComponent, interaction_user: discord.User, strike_user: discord.Member, confirm_strike_message: str, actual_strike_amount: int, button_callback_channel: discord.TextChannel | discord.DMChannel) -> None: # noqa: E501 await message_sender_component.send( content=confirm_strike_message, view=ConfirmStrikeMemberView(), ) - button_interaction: discord.Interaction = await self.bot.wait_for( + button_interaction: discord.Interaction = await self.tex_bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component @@ -298,7 +306,7 @@ async def _confirm_perform_moderation_action(self, message_sender_component: Mes raise ValueError - async def _confirm_increase_strike(self, message_sender_component: MessageSenderComponent, interaction_user: discord.User, strike_user: discord.User | discord.Member, member_strikes: DiscordMemberStrikes, button_callback_channel: discord.TextChannel | discord.DMChannel, *, perform_action: bool) -> None: # noqa: E501 + async def _confirm_increase_strike(self, message_sender_component: MessageSavingSenderComponent, interaction_user: discord.User, strike_user: discord.User | discord.Member, member_strikes: DiscordMemberStrikes, button_callback_channel: discord.TextChannel | discord.DMChannel, *, perform_action: bool) -> None: # noqa: E501 if perform_action and isinstance(strike_user, discord.User): STRIKE_USER_TYPE_ERROR_MESSAGE: Final[str] = ( "Cannot perform moderation action on non-guild member." @@ -337,6 +345,7 @@ async def _confirm_increase_strike(self, message_sender_component: MessageSender ) if not perform_action: + # noinspection SpellCheckingInspection await message_sender_component.send( content=( f"{confirm_strike_message}\n" @@ -412,20 +421,20 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me """ Retrieve the correct channel to send the strike confirmation message to. - This is based upon the MANUAL_MODERATION_WARNING_MESSAGE_LOCATION config setting value. + This is based upon the STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION config setting value. """ - if settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"] == "DM": + if settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] == "DM": if user.bot: fetch_log_channel_error: RuntimeError try: - return await self.bot.fetch_log_channel() + return await self.tex_bot.fetch_log_channel() except RuntimeError as fetch_log_channel_error: raise StrikeTrackingError( str(fetch_log_channel_error), ) from fetch_log_channel_error raw_user: discord.User | None = ( - self.bot.get_user(user.id) + self.tex_bot.get_user(user.id) if isinstance(user, discord.Member) else user ) @@ -441,13 +450,13 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me return dm_confirmation_message_channel guild_confirmation_message_channel: discord.TextChannel | None = discord.utils.get( - self.bot.main_guild.text_channels, - name=settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"], + self.tex_bot.main_guild.text_channels, + name=settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"], ) if not guild_confirmation_message_channel: CHANNEL_DOES_NOT_EXIST_MESSAGE: Final[str] = ( "The channel " - f"""{settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"]!r} """ + f"""{settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"]!r} """ "does not exist, so cannot be used as the location " "for sending manual-moderation warning messages" ) @@ -455,11 +464,12 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me return guild_confirmation_message_channel + # noinspection PyTypeHints @capture_strike_tracking_error async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.Member, action: discord.AuditLogAction) -> None: # noqa: E501 # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - main_guild: discord.Guild = self.bot.main_guild - committee_role: discord.Role = await self.bot.committee_role + main_guild: discord.Guild = self.tex_bot.main_guild + committee_role: discord.Role = await self.tex_bot.committee_role try: # noinspection PyTypeChecker @@ -473,16 +483,15 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M if _audit_log_entry.target.id == strike_user.id # NOTE: IDs are checked here rather than the objects themselves as the audit log provides an unusual object type in some cases. ) except (StopIteration, StopAsyncIteration): - IRRETRIEVABLE_AUDIT_LOG_MESSAGE: Final[str] = ( - f"Unable to retrieve audit log entry of {str(action)!r} action " - f"on user {str(strike_user)!r}" - ) - logger.debug("Printing 5 most recent audit logs:") debug_audit_log_entry: discord.AuditLogEntry async for debug_audit_log_entry in main_guild.audit_logs(limit=5): logger.debug(debug_audit_log_entry) + IRRETRIEVABLE_AUDIT_LOG_MESSAGE: Final[str] = ( + f"Unable to retrieve audit log entry of {str(action)!r} action " + f"on user {str(strike_user)!r}" + ) raise NoAuditLogsStrikeTrackingError(IRRETRIEVABLE_AUDIT_LOG_MESSAGE) from None if not audit_log_entry.user: @@ -490,7 +499,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M applied_action_user: discord.User | discord.Member = audit_log_entry.user - if applied_action_user == self.bot.user: + if applied_action_user == self.tex_bot.user: return fetch_log_channel_error: RuntimeError @@ -498,31 +507,24 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M confirmation_message_channel: discord.DMChannel | discord.TextChannel = ( await self.get_confirmation_message_channel(applied_action_user) if applied_action_user != strike_user - else await self.bot.fetch_log_channel() + else await self.tex_bot.fetch_log_channel() ) except RuntimeError as fetch_log_channel_error: raise StrikeTrackingError( str(fetch_log_channel_error), ) from fetch_log_channel_error - MODERATION_ACTIONS: Final[Mapping[discord.AuditLogAction, str]] = { - discord.AuditLogAction.member_update: "timed-out", - discord.AuditLogAction.auto_moderation_user_communication_disabled: "timed-out", - discord.AuditLogAction.kick: "kicked", - discord.AuditLogAction.ban: "banned", - } - member_strikes: DiscordMemberStrikes = ( # type: ignore[assignment] await DiscordMemberStrikes.objects.aget_or_create( discord_id=strike_user.id, ) )[0] - strikes_out_of_sync_with_ban: bool = bool( + STRIKES_OUT_OF_SYNC_WITH_BAN: Final[bool] = bool( (action != discord.AuditLogAction.ban and member_strikes.strikes >= 3) - or (action == discord.AuditLogAction.ban and member_strikes.strikes > 3), + or (action == discord.AuditLogAction.ban and member_strikes.strikes > 3) # noqa: COM812 ) - if strikes_out_of_sync_with_ban: + if STRIKES_OUT_OF_SYNC_WITH_BAN: out_of_sync_ban_confirmation_message: discord.Message = await confirmation_message_channel.send( # noqa: E501 content=( f"""Hi { @@ -534,7 +536,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M "you" if not applied_action_user.bot else f"one of your other bots (namely {applied_action_user.mention})" - } {MODERATION_ACTIONS[action]} {strike_user.mention}. """ + } {FORMATTED_MODERATION_ACTIONS[action]} {strike_user.mention}. """ "Because this moderation action was done manually " "(rather than using my `/strike` command), I could not automatically " f"keep track of the moderation action to apply. " @@ -546,22 +548,24 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M view=ConfirmStrikesOutOfSyncWithBanView(), ) - out_of_sync_ban_button_interaction: discord.Interaction = await self.bot.wait_for( - "interaction", - check=lambda interaction: ( - interaction.type == discord.InteractionType.component - and ( - (interaction.user == applied_action_user) - if not applied_action_user.bot - else (committee_role in interaction.user.roles) - ) - and interaction.channel == confirmation_message_channel - and "custom_id" in interaction.data - and interaction.data["custom_id"] in { - "yes_out_of_sync_ban_member", - "no_out_of_sync_ban_member", - } - ), + out_of_sync_ban_button_interaction: discord.Interaction = ( + await self.tex_bot.wait_for( + "interaction", + check=lambda interaction: ( + interaction.type == discord.InteractionType.component + and ( + (interaction.user == applied_action_user) + if not applied_action_user.bot + else (committee_role in interaction.user.roles) + ) + and interaction.channel == confirmation_message_channel + and "custom_id" in interaction.data + and interaction.data["custom_id"] in { + "yes_out_of_sync_ban_member", + "no_out_of_sync_ban_member", + } + ), + ) ) if out_of_sync_ban_button_interaction.data["custom_id"] == "no_out_of_sync_ban_member": # type: ignore[index, typeddict-item] # noqa: E501 @@ -592,6 +596,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M "with number of strikes**" ), ) + # noinspection SpellCheckingInspection await out_of_sync_ban_confirmation_message.edit( content=( f"Successfully banned {strike_user.mention}.\n" @@ -623,7 +628,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M "you" if not applied_action_user.bot else f"one of your other bots (namely {applied_action_user.mention})" - } {MODERATION_ACTIONS[action]} {strike_user.mention}. """ + } {FORMATTED_MODERATION_ACTIONS[action]} {strike_user.mention}. """ "Because this moderation action was done manually " "(rather than using my `/strike` command), I could not automatically " f"keep track of the correct moderation action to apply. " @@ -634,7 +639,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M view=ConfirmManualModerationView(), ) - button_interaction: discord.Interaction = await self.bot.wait_for( + button_interaction: discord.Interaction = await self.tex_bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component @@ -653,6 +658,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M ) if button_interaction.data["custom_id"] == "no_manual_moderation_action": # type: ignore[index, typeddict-item] + # noinspection SpellCheckingInspection await confirmation_message.edit( content=( f"Aborted increasing {strike_user.mention}'s strikes " @@ -673,7 +679,9 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M return if button_interaction.data["custom_id"] == "yes_manual_moderation_action": # type: ignore[index, typeddict-item] - interaction_user: discord.User | None = self.bot.get_user(applied_action_user.id) + interaction_user: discord.User | None = self.tex_bot.get_user( + applied_action_user.id, + ) if not interaction_user: raise StrikeTrackingError @@ -693,7 +701,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M async def on_member_update(self, before: discord.Member, after: discord.Member) -> None: """Flag manually applied timeout & track strikes accordingly.""" # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - main_guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: return @@ -716,7 +724,6 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) ) return - # noinspection PyArgumentList await self._confirm_manual_add_strike( strike_user=after, action=discord.AuditLogAction.member_update, @@ -727,20 +734,19 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) async def on_member_remove(self, member: discord.Member) -> None: """Flag manually applied kick & track strikes accordingly.""" # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - main_guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild MEMBER_REMOVED_BECAUSE_OF_MANUALLY_APPLIED_KICK: Final[bool] = bool( - member.guild == self.bot.main_guild + member.guild == self.tex_bot.main_guild and not member.bot and not await asyncany( ban.user == member async for ban in main_guild.bans() - ), + ) # noqa: COM812 ) if not MEMBER_REMOVED_BECAUSE_OF_MANUALLY_APPLIED_KICK: return with contextlib.suppress(NoAuditLogsStrikeTrackingError): - # noinspection PyArgumentList await self._confirm_manual_add_strike( strike_user=member, action=discord.AuditLogAction.kick, @@ -750,10 +756,9 @@ async def on_member_remove(self, member: discord.Member) -> None: @capture_guild_does_not_exist_error async def on_member_ban(self, guild: discord.Guild, user: discord.User | discord.Member) -> None: # noqa: E501 """Flag manually applied ban & track strikes accordingly.""" - if guild != self.bot.main_guild or user.bot: + if guild != self.tex_bot.main_guild or user.bot: return - # noinspection PyArgumentList await self._confirm_manual_add_strike( strike_user=user, action=discord.AuditLogAction.ban, @@ -764,7 +769,7 @@ class StrikeCommandCog(BaseStrikeCog): """Cog class that defines the "/strike" command and its call-back method.""" @staticmethod - async def strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice]: # noqa: E501 + async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable members. @@ -772,13 +777,15 @@ async def strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set that have a member input-type. """ try: - guild: discord.Guild = ctx.bot.main_guild + main_guild: discord.Guild = ctx.tex_bot.main_guild except GuildDoesNotExistError: return set() - members: set[discord.Member] = {member for member in guild.members if not member.bot} + members: set[discord.Member] = { + member for member in main_guild.members if not member.bot + } - if not ctx.value or re.match(r"\A@.*\Z", ctx.value): + if not ctx.value or re.fullmatch(r"\A@.*\Z", ctx.value): return { discord.OptionChoice(name=f"@{member.name}", value=str(member.id)) for member @@ -802,7 +809,7 @@ async def strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set name="user", description="The user to give a strike to.", input_type=str, - autocomplete=discord.utils.basic_autocomplete(strike_autocomplete_get_members), # type: ignore[arg-type] + autocomplete=discord.utils.basic_autocomplete(autocomplete_get_members), # type: ignore[arg-type] required=True, parameter_name="str_strike_member_id", ) @@ -817,7 +824,7 @@ async def strike(self, ctx: TeXBotApplicationContext, str_strike_member_id: str) """ member_id_not_integer_error: ValueError try: - strike_member: discord.Member = await self.bot.get_member_from_str_id( + strike_member: discord.Member = await self.tex_bot.get_member_from_str_id( str_strike_member_id, ) except ValueError as member_id_not_integer_error: diff --git a/cogs/write_roles.py b/cogs/write_roles.py index f209e0b15..79789c7c5 100644 --- a/cogs/write_roles.py +++ b/cogs/write_roles.py @@ -13,11 +13,11 @@ class WriteRolesCommandCog(TeXBotBaseCog): # noinspection SpellCheckingInspection - """Cog class that defines the "/writeroles" command and its call-back method.""" + """Cog class that defines the "/write-roles" command and its call-back method.""" # noinspection SpellCheckingInspection @discord.slash_command( # type: ignore[no-untyped-call, misc] - name="writeroles", + name="write-roles", description="Populates #roles with the correct messages.", ) @CommandChecks.check_interaction_user_has_committee_role @@ -30,12 +30,12 @@ async def write_roles(self, ctx: TeXBotApplicationContext) -> None: defined in the messages.json file. """ # NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent - roles_channel: discord.TextChannel = await self.bot.roles_channel + roles_channel: discord.TextChannel = await self.tex_bot.roles_channel roles_message: str for roles_message in settings["ROLES_MESSAGES"]: await roles_channel.send( - roles_message.replace("", self.bot.group_short_name), + roles_message.replace("", self.tex_bot.group_short_name), ) await ctx.respond("All messages sent successfully.", ephemeral=True) diff --git a/config.py b/config.py index f1238577d..7e4b564c0 100644 --- a/config.py +++ b/config.py @@ -110,7 +110,7 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if item in self._settings: return self._settings[item] - if re.match(r"\A[A-Z](?:[A-Z_]*[A-Z])?\Z", item): + if re.fullmatch(r"\A[A-Z](?:[A-Z_]*[A-Z])?\Z", item): INVALID_SETTINGS_KEY_MESSAGE: Final[str] = self.get_invalid_settings_key_message( item, ) @@ -160,7 +160,7 @@ def _setup_discord_bot_token(cls) -> None: DISCORD_BOT_TOKEN_IS_VALID: Final[bool] = bool( raw_discord_bot_token - and re.match( + and re.fullmatch( r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", raw_discord_bot_token, ), @@ -205,7 +205,7 @@ def _setup_discord_guild_id(cls) -> None: DISCORD_GUILD_ID_IS_VALID: Final[bool] = bool( raw_discord_guild_id - and re.match(r"\A\d{17,20}\Z", raw_discord_guild_id), + and re.fullmatch(r"\A\d{17,20}\Z", raw_discord_guild_id), ) if not DISCORD_GUILD_ID_IS_VALID: INVALID_DISCORD_GUILD_ID_MESSAGE: Final[str] = ( @@ -214,7 +214,7 @@ def _setup_discord_guild_id(cls) -> None: ) raise ImproperlyConfiguredError(INVALID_DISCORD_GUILD_ID_MESSAGE) - cls._settings["DISCORD_GUILD_ID"] = int(raw_discord_guild_id) # type: ignore[arg-type] + cls._settings["_DISCORD_MAIN_GUILD_ID"] = int(raw_discord_guild_id) # type: ignore[arg-type] @classmethod def _setup_group_full_name(cls) -> None: @@ -222,7 +222,7 @@ def _setup_group_full_name(cls) -> None: GROUP_FULL_NAME_IS_VALID: Final[bool] = bool( not raw_group_full_name - or re.match(r"\A[A-Za-z0-9 '&!?:,.#%\"-]+\Z", raw_group_full_name), + or re.fullmatch(r"\A[A-Za-z0-9 '&!?:,.#%\"-]+\Z", raw_group_full_name), ) if not GROUP_FULL_NAME_IS_VALID: INVALID_GROUP_FULL_NAME: Final[str] = ( @@ -237,7 +237,7 @@ def _setup_group_short_name(cls) -> None: GROUP_SHORT_NAME_IS_VALID: Final[bool] = bool( not raw_group_short_name - or re.match(r"\A[A-Za-z0-9'&!?:,.#%\"-]+\Z", raw_group_short_name), + or re.fullmatch(r"\A[A-Za-z0-9'&!?:,.#%\"-]+\Z", raw_group_short_name), ) if not GROUP_SHORT_NAME_IS_VALID: INVALID_GROUP_SHORT_NAME: Final[str] = ( @@ -394,22 +394,24 @@ def _setup_members_list_url(cls) -> None: cls._settings["MEMBERS_LIST_URL"] = raw_members_list_url @classmethod - def _setup_members_list_url_session_cookie(cls) -> None: - raw_members_list_url_session_cookie: str | None = os.getenv( + def _setup_members_list_auth_session_cookie(cls) -> None: + raw_members_list_auth_session_cookie: str | None = os.getenv( "MEMBERS_LIST_URL_SESSION_COOKIE", ) - MEMBERS_LIST_URL_SESSION_COOKIE_IS_VALID: Final[bool] = bool( - raw_members_list_url_session_cookie - and re.match(r"\A[A-Fa-f\d]{128,256}\Z", raw_members_list_url_session_cookie), + MEMBERS_LIST_AUTH_SESSION_COOKIE_IS_VALID: Final[bool] = bool( + raw_members_list_auth_session_cookie + and re.fullmatch(r"\A[A-Fa-f\d]{128,256}\Z", raw_members_list_auth_session_cookie), ) - if not MEMBERS_LIST_URL_SESSION_COOKIE_IS_VALID: - INVALID_MEMBERS_LIST_URL_SESSION_COOKIE_MESSAGE: Final[str] = ( + if not MEMBERS_LIST_AUTH_SESSION_COOKIE_IS_VALID: + INVALID_MEMBERS_LIST_AUTH_SESSION_COOKIE_MESSAGE: Final[str] = ( "MEMBERS_LIST_URL_SESSION_COOKIE must be a valid .ASPXAUTH cookie." ) - raise ImproperlyConfiguredError(INVALID_MEMBERS_LIST_URL_SESSION_COOKIE_MESSAGE) + raise ImproperlyConfiguredError(INVALID_MEMBERS_LIST_AUTH_SESSION_COOKIE_MESSAGE) - cls._settings["MEMBERS_LIST_URL_SESSION_COOKIE"] = raw_members_list_url_session_cookie + cls._settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"] = ( + raw_members_list_auth_session_cookie + ) @classmethod def _setup_send_introduction_reminders(cls) -> None: @@ -441,7 +443,7 @@ def _setup_send_introduction_reminders_delay(cls) -> None: ) raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - raw_send_introduction_reminders_delay: Match[str] | None = re.match( + raw_send_introduction_reminders_delay: Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", str(os.getenv("SEND_INTRODUCTION_REMINDERS_DELAY", "40h")), ) @@ -489,7 +491,7 @@ def _setup_send_introduction_reminders_interval(cls) -> None: ) raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - raw_send_introduction_reminders_interval: Match[str] | None = re.match( + raw_send_introduction_reminders_interval: Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", str(os.getenv("SEND_INTRODUCTION_REMINDERS_INTERVAL", "6h")), ) @@ -544,7 +546,7 @@ def _setup_send_get_roles_reminders_delay(cls) -> None: ) raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - raw_send_get_roles_reminders_delay: Match[str] | None = re.match( + raw_send_get_roles_reminders_delay: Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", str(os.getenv("SEND_GET_ROLES_REMINDERS_DELAY", "40h")), ) @@ -592,7 +594,7 @@ def _setup_advanced_send_get_roles_reminders_interval(cls) -> None: ) raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - raw_advanced_send_get_roles_reminders_interval: Match[str] | None = re.match( + raw_advanced_send_get_roles_reminders_interval: Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", str(os.getenv("ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL", "24h")), ) @@ -667,20 +669,20 @@ def _setup_moderation_document_url(cls) -> None: cls._settings["MODERATION_DOCUMENT_URL"] = raw_moderation_document_url @classmethod - def _setup_manual_moderation_warning_message_location(cls) -> None: - raw_manual_moderation_warning_message_location: str = os.getenv( + def _setup_strike_performed_manually_warning_location(cls) -> None: + raw_strike_performed_manually_warning_location: str = os.getenv( "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", "DM", ) - if not raw_manual_moderation_warning_message_location: - MANUAL_MODERATION_WARNING_MESSAGE_LOCATION_MESSAGE: Final[str] = ( + if not raw_strike_performed_manually_warning_location: + STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_MESSAGE: Final[str] = ( "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION must be a valid name " "of a channel in your group's Discord guild." ) - raise ImproperlyConfiguredError(MANUAL_MODERATION_WARNING_MESSAGE_LOCATION_MESSAGE) + raise ImproperlyConfiguredError(STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_MESSAGE) - cls._settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"] = ( - raw_manual_moderation_warning_message_location + cls._settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] = ( + raw_strike_performed_manually_warning_location ) @classmethod @@ -707,7 +709,7 @@ def _setup_env_variables(cls) -> None: cls._setup_welcome_messages() cls._setup_roles_messages() cls._setup_members_list_url() - cls._setup_members_list_url_session_cookie() + cls._setup_members_list_auth_session_cookie() cls._setup_membership_perks_url() cls._setup_purchase_membership_url() cls._setup_send_introduction_reminders() @@ -719,7 +721,7 @@ def _setup_env_variables(cls) -> None: cls._setup_statistics_days() cls._setup_statistics_roles() cls._setup_moderation_document_url() - cls._setup_manual_moderation_warning_message_location() + cls._setup_strike_performed_manually_warning_location() cls._is_env_variables_setup = True diff --git a/db/core/app_config.py b/db/core/app_config.py index 3ca68d96e..0b3a7df5e 100644 --- a/db/core/app_config.py +++ b/db/core/app_config.py @@ -1,4 +1,4 @@ -"""Configurations to make core app ready to import into _settings.py.""" +"""Configurations to make the core app ready to import into _settings.py.""" from collections.abc import Sequence diff --git a/db/core/models/__init__.py b/db/core/models/__init__.py index 7127cabcb..bf57415d4 100644 --- a/db/core/models/__init__.py +++ b/db/core/models/__init__.py @@ -85,7 +85,7 @@ class SentGetRolesReminderMember(BaseDiscordMemberWrapper): """ Represents a Discord member that has already been sent an opt-in roles reminder. - The opt-in roles reminder suggests to the Discord member to visit the #roles channel + The opt-in roles reminder suggests the Discord member visit the #roles channel to claim some opt-in roles within your group's Discord guild. The Discord member is identified by their hashed Discord member ID. @@ -176,7 +176,7 @@ def hash_group_member_id(cls, group_member_id: str | int, group_member_id_type: that hashed_group_member_ids are stored in the database when new GroupMadeMember objects are created. """ - if not re.match(r"\A\d{7}\Z", str(group_member_id)): + if not re.fullmatch(r"\A\d{7}\Z", str(group_member_id)): INVALID_GROUP_MEMBER_ID_MESSAGE: Final[str] = ( f"{group_member_id!r} is not a valid {group_member_id_type} ID." ) diff --git a/db/core/models/managers.py b/db/core/models/managers.py index dc95cfaac..bcd5e8900 100644 --- a/db/core/models/managers.py +++ b/db/core/models/managers.py @@ -27,7 +27,7 @@ | None ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class BaseHashedIDManager(Manager["T_model"], abc.ABC): @@ -149,7 +149,7 @@ class HashedDiscordMemberManager(BaseHashedIDManager["DiscordMember"]): Manager class to create & retrieve DiscordMember model instances. This manager implements extra functionality to filter/create instances - using a given discord_id that with be automatically hashed, before saved to the database. + using a given discord_id that with be automatically hashed before saved to the database. """ # noinspection SpellCheckingInspection @@ -202,7 +202,7 @@ class RelatedDiscordMemberManager(BaseHashedIDManager["BaseDiscordMemberWrapper" Manager class to create & retrieve instances of any concrete `BaseDiscordMemberWrapper`. This manager implements extra functionality to filter/create instances - using a given discord_id that with be automatically hashed, before saved to the database. + using a given discord_id that with be automatically hashed before saved to the database. """ # noinspection SpellCheckingInspection diff --git a/db/core/models/utils.py b/db/core/models/utils.py index 454bd0581..1645756d5 100644 --- a/db/core/models/utils.py +++ b/db/core/models/utils.py @@ -177,9 +177,9 @@ def __setattr__(self, name: str, value: object) -> None: raise TypeError(MEMBER_ID_INVALID_TYPE_MESSAGE) self.hashed_discord_id = self.hash_discord_id(value) + return - else: - super().__setattr__(name, value) + super().__setattr__(name, value) @property def discord_id(self) -> NoReturn: @@ -225,7 +225,7 @@ def hash_discord_id(cls, discord_id: str | int) -> str: into the format that hashed_discord_ids are stored in the database when new objects of this class are created. """ - if not re.match(r"\A\d{17,20}\Z", str(discord_id)): + if not re.fullmatch(r"\A\d{17,20}\Z", str(discord_id)): INVALID_MEMBER_ID_MESSAGE: Final[str] = ( f"{discord_id!r} is not a valid Discord member ID " "(see https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id)" diff --git a/exceptions/__init__.py b/exceptions/__init__.py index 0a397c406..8f2e2446b 100644 --- a/exceptions/__init__.py +++ b/exceptions/__init__.py @@ -23,12 +23,13 @@ "MessagesJSONFileValueError", "InvalidMessagesJSONFileError", "ImproperlyConfiguredError", - "BotRequiresRestartAfterConfigChange", + "RestartRequiredDueToConfigChange", ) + from .config_changes import ( - BotRequiresRestartAfterConfigChange, ImproperlyConfiguredError, + RestartRequiredDueToConfigChange, ) from .does_not_exist import ( ApplicantRoleDoesNotExistError, diff --git a/exceptions/base.py b/exceptions/base.py index bc018c4f7..d10ed0f96 100644 --- a/exceptions/base.py +++ b/exceptions/base.py @@ -10,7 +10,7 @@ import abc -from typing import Final +from typing import Final, override from classproperties import classproperty @@ -21,15 +21,17 @@ class BaseTeXBotError(BaseException, abc.ABC): # noinspection PyMethodParameters,PyPep8Naming @classproperty @abc.abstractmethod - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + def DEFAULT_MESSAGE(cls) -> str: # noqa: N802, N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override def __init__(self, message: str | None = None) -> None: - """Initialize a new exception with the given error message.""" + """Initialise a new exception with the given error message.""" self.message: str = message or self.DEFAULT_MESSAGE super().__init__(self.message) + @override def __repr__(self) -> str: """Generate a developer-focused representation of the exception's attributes.""" formatted: str = self.message @@ -56,7 +58,7 @@ class BaseErrorWithErrorCode(BaseTeXBotError, abc.ABC): # noinspection PyMethodParameters,PyPep8Naming @classproperty @abc.abstractmethod - def ERROR_CODE(cls) -> str: # noqa: N802,N805 + def ERROR_CODE(cls) -> str: # noqa: N802, N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 @@ -65,41 +67,41 @@ class BaseDoesNotExistError(BaseErrorWithErrorCode, ValueError, abc.ABC): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802, N805 """ - The set of names of bot commands that require this Discord entity. + The set of names of commands that require this Discord entity. - This set being empty could mean that all bot commands require this Discord entity, - or no bot commands require this Discord entity. + This set being empty could mean that all commands require this Discord entity, + or no commands require this Discord entity. """ # noqa: D401 return frozenset() # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N802,N805 + def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N802, N805 """ - The set of names of bot tasks that require this Discord entity. + The set of names of tasks that require this Discord entity. - This set being empty could mean that all bot tasks require this Discord entity, - or no bot tasks require this Discord entity. + This set being empty could mean that all tasks require this Discord entity, + or no tasks require this Discord entity. """ # noqa: D401 return frozenset() # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_EVENTS(cls) -> frozenset[str]: # noqa: N802,N805 + def DEPENDENT_EVENTS(cls) -> frozenset[str]: # noqa: N802, N805 """ - The set of names of bot events that require this Discord entity. + The set of names of event listeners that require this Discord entity. - This set being empty could mean that all bot events require this Discord entity, - or no bot events require this Discord entity. + This set being empty could mean that all event listeners require this Discord entity, + or no event listeners require this Discord entity. """ # noqa: D401 return frozenset() # noinspection PyMethodParameters,PyPep8Naming @classproperty @abc.abstractmethod - def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N802,N805 + def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N802, N805 """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 @classmethod @@ -191,4 +193,3 @@ def get_formatted_message(cls, non_existent_object_identifier: str) -> str: # n partial_message += formatted_dependent_events return f"{partial_message}." - diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index 721a45872..d1d5914b1 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -4,12 +4,42 @@ __all__: Sequence[str] = ( "ImproperlyConfiguredError", - "BotRequiresRestartAfterConfigChange", + "RestartRequiredDueToConfigChange", ) -class ImproperlyConfiguredError(Exception): +from collections.abc import Set +from typing import override + +from classproperties import classproperty + +from .base import BaseTeXBotError + + +class ImproperlyConfiguredError(BaseTeXBotError, Exception): """Exception class to raise when environment variables are not correctly provided.""" -class BotRequiresRestartAfterConfigChange(Exception): - """Exception class to raise when the bot requires a reboot to apply changes.""" + # noinspection PyMethodParameters,PyPep8Naming + @classproperty + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 + return "One or more provided environment variable values are invalid." + + +class RestartRequiredDueToConfigChange(BaseTeXBotError, Exception): + """Exception class to raise when a restart is required to apply config changes.""" + + # noinspection PyMethodParameters,PyPep8Naming + @classproperty + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 + return "TeX-Bot requires a restart to apply configuration changes." + + @override + def __init__(self, message: str | None = None, changed_settings: Set[str] | None = None) -> None: # noqa: E501 + """Initialise an Exception to apply configuration changes.""" + self.changed_settings: Set[str] | None = ( + changed_settings if changed_settings else set() + ) + + super().__init__(message) diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index 5459f74cc..161708c10 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -11,6 +11,7 @@ "GuestRoleDoesNotExistError", "MemberRoleDoesNotExistError", "ArchivistRoleDoesNotExistError", + "ApplicantRoleDoesNotExistError", "ChannelDoesNotExistError", "RolesChannelDoesNotExistError", "GeneralChannelDoesNotExistError", @@ -18,7 +19,7 @@ import abc -from typing import Final +from typing import Final, override from classproperties import classproperty @@ -30,8 +31,8 @@ class RulesChannelDoesNotExistError(BaseTeXBotError, ValueError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "There is no channel marked as the rules channel." @@ -40,28 +41,29 @@ class GuildDoesNotExistError(BaseDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "Server with given ID does not exist or is not accessible to the bot." # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1011" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N802,N805 - """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 + @override + def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 return "guild" + @override def __init__(self, message: str | None = None, guild_id: int | None = None) -> None: - """Initialize a new DoesNotExist exception for a guild not existing.""" + """Initialise a new DoesNotExist exception for a guild not existing.""" self.guild_id: int | None = guild_id if guild_id and not message: - message = self.DEFAULT_MESSAGE.replace("given ID", f"ID \"{self.guild_id}\"") + message = self.DEFAULT_MESSAGE.replace("given ID", f"ID '{self.guild_id}'") super().__init__(message) @@ -71,26 +73,27 @@ class RoleDoesNotExistError(BaseDoesNotExistError, abc.ABC): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return f"Role with name \"{cls.ROLE_NAME}\" does not exist." # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N802,N805 - """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 + @override + def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 return "role" # noinspection PyMethodParameters,PyPep8Naming @classproperty @abc.abstractmethod - def ROLE_NAME(cls) -> str: # noqa: N802,N805 + def ROLE_NAME(cls) -> str: # noqa: N802, N805 """The name of the Discord role that does not exist.""" # noqa: D401 + @override def __init__(self, message: str | None = None) -> None: - """Initialize a new DoesNotExist exception for a role not existing.""" + """Initialise a new DoesNotExist exception for a role not existing.""" HAS_DEPENDANTS: Final[bool] = bool( - self.DEPENDENT_COMMANDS or self.DEPENDENT_TASKS or self.DEPENDENT_EVENTS, + self.DEPENDENT_COMMANDS or self.DEPENDENT_TASKS or self.DEPENDENT_EVENTS # noqa: COM812 ) if not message and HAS_DEPENDANTS: @@ -104,19 +107,14 @@ class CommitteeRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1021" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 - """ - The set of names of bot commands that require this Discord entity. - - This set being empty could mean that all bot commands require this Discord entity, - or no bot commands require this Discord entity. - """ # noqa: D401 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 # noinspection SpellCheckingInspection return frozenset( { @@ -133,32 +131,30 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802,N805 - """The name of the Discord role that does not exist.""" # noqa: D401 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 return "Committee" class CommitteeElectRoleDoesNotExistError(RoleDoesNotExistError): """Exception class to raise when the "Committee-Elect" Discord role is missing.""" + # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1026" + # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802, N805 - """ - The set of names of bot commands that require this Discord entity. - - This set being empty could mean that all bot commands require this Discord entity, - or none of them do. - """ # noqa: D401 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 return frozenset({"handover"}) + # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802, N805 - """The name of the Discord role that does not exist.""" # noqa: D401 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 return "Committee-Elect" @@ -167,38 +163,28 @@ class GuestRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1022" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 - """ - The set of names of bot commands that require this Discord entity. - - This set being empty could mean that all bot commands require this Discord entity, - or no bot commands require this Discord entity. - """ # noqa: D401 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 # noinspection SpellCheckingInspection return frozenset({"induct", "stats", "archive", "ensure-members-inducted"}) # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N802,N805 - """ - The set of names of bot tasks that require this Discord entity. - - This set being empty could mean that all bot tasks require this Discord entity, - or no bot tasks require this Discord entity. - """ # noqa: D401 + @override + def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N805 # noinspection SpellCheckingInspection return frozenset({"send_get_roles_reminders"}) # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802,N805 - """The name of the Discord role that does not exist.""" # noqa: D401 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 return "Guest" @@ -207,26 +193,21 @@ class MemberRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1023" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 - """ - The set of names of bot commands that require this Discord entity. - - This set being empty could mean that all bot commands require this Discord entity, - or no bot commands require this Discord entity. - """ # noqa: D401 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 # noinspection SpellCheckingInspection return frozenset({"makemember", "ensure-members-inducted"}) # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802,N805 - """The name of the Discord role that does not exist.""" # noqa: D401 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 return "Member" @@ -235,49 +216,43 @@ class ArchivistRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1024" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 - """ - The set of names of bot commands that require this Discord entity. - - This set being empty could mean that all bot commands require this Discord entity, - or no bot commands require this Discord entity. - """ # noqa: D401 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 # noinspection SpellCheckingInspection return frozenset({"archive"}) # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802,N805 - """The name of the Discord role that does not exist.""" # noqa: D401 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 return "Archivist" + class ApplicantRoleDoesNotExistError(RoleDoesNotExistError): """Exception class to raise when the "Applicant" Discord role is missing.""" + # noinspection PyMethodParameters @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802, N805 - """The unique error code for users to tell admins about an error that occured.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1025" + # noinspection PyMethodParameters @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802, N805 - """ - The set of names of bot commands that require this Discord entity. - - This set being empty could mean thta all bot commands require this entity, - or that none of them do. - """ # noqa: D401 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 return frozenset({"make_applicant"}) + # noinspection PyMethodParameters @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802, N805 - """The name of the Discord role that does not exist.""" # noqa: D401 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 return "Applicant" @@ -286,26 +261,27 @@ class ChannelDoesNotExistError(BaseDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return f"Channel with name \"{cls.CHANNEL_NAME}\" does not exist." # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N802,N805 - """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 + @override + def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 return "channel" # noinspection PyMethodParameters,PyPep8Naming @classproperty @abc.abstractmethod - def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 + def CHANNEL_NAME(cls) -> str: # noqa: N802, N805 """The name of the Discord channel that does not exist.""" # noqa: D401 + @override def __init__(self, message: str | None = None) -> None: - """Initialize a new DoesNotExist exception for a role not existing.""" + """Initialise a new DoesNotExist exception for a role not existing.""" HAS_DEPENDANTS: Final[bool] = bool( - self.DEPENDENT_COMMANDS or self.DEPENDENT_TASKS or self.DEPENDENT_EVENTS, + self.DEPENDENT_COMMANDS or self.DEPENDENT_TASKS or self.DEPENDENT_EVENTS # noqa: COM812 ) if not message and HAS_DEPENDANTS: @@ -321,26 +297,21 @@ class RolesChannelDoesNotExistError(ChannelDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1031" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 - """ - The set of names of bot commands that require this Discord entity. - - This set being empty could mean that all bot commands require this Discord entity, - or no bot commands require this Discord entity. - """ # noqa: D401 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 # noinspection SpellCheckingInspection return frozenset({"writeroles"}) # noinspection PyMethodParameters,PyPep8Naming @classproperty - def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 - """The name of the Discord channel that does not exist.""" # noqa: D401 + @override + def CHANNEL_NAME(cls) -> str: # noqa: N805 return "roles" @@ -349,24 +320,19 @@ class GeneralChannelDoesNotExistError(ChannelDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1032" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 - """ - The set of names of bot commands that require this Discord entity. - - This set being empty could mean that all bot commands require this Discord entity, - or no bot commands require this Discord entity. - """ # noqa: D401 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 # noinspection SpellCheckingInspection return frozenset({"induct"}) # noinspection PyMethodParameters,PyPep8Naming @classproperty - def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 - """The name of the Discord channel that does not exist.""" # noqa: D401 + @override + def CHANNEL_NAME(cls) -> str: # noqa: N805 return "general" diff --git a/exceptions/guild.py b/exceptions/guild.py index a0013945e..770322021 100644 --- a/exceptions/guild.py +++ b/exceptions/guild.py @@ -7,6 +7,9 @@ "EveryoneRoleCouldNotBeRetrievedError", ) + +from typing import override + from classproperties import classproperty from .base import BaseErrorWithErrorCode, BaseTeXBotError @@ -17,12 +20,13 @@ class DiscordMemberNotInMainGuildError(BaseTeXBotError, ValueError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "Given user ID does not represent any member of your group's Discord guild." + @override def __init__(self, message: str | None = None, user_id: int | None = None) -> None: - """Initialize a ValueError exception for a non-existent user ID.""" + """Initialise a ValueError exception for a non-existent user ID.""" self.user_id: int | None = user_id super().__init__(message) @@ -33,12 +37,12 @@ class EveryoneRoleCouldNotBeRetrievedError(BaseErrorWithErrorCode, ValueError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "The reference to the \"@everyone\" role could not be correctly retrieved." # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 return "E1042" diff --git a/exceptions/messages.py b/exceptions/messages.py index 23acfa2ef..049f55d9b 100644 --- a/exceptions/messages.py +++ b/exceptions/messages.py @@ -9,23 +9,25 @@ ) +from typing import override + from classproperties import classproperty -from .base import BaseTeXBotError from .config_changes import ImproperlyConfiguredError -class InvalidMessagesJSONFileError(BaseTeXBotError, ImproperlyConfiguredError): +class InvalidMessagesJSONFileError(ImproperlyConfiguredError): """Exception class to raise when the messages.json file has an invalid structure.""" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "The messages JSON file has an invalid structure at the given key." + @override def __init__(self, message: str | None = None, dict_key: str | None = None) -> None: - """Initialize an ImproperlyConfigured exception for an invalid messages.json file.""" + """Initialise an ImproperlyConfigured exception for an invalid messages.json file.""" self.dict_key: str | None = dict_key super().__init__(message) @@ -36,12 +38,13 @@ class MessagesJSONFileMissingKeyError(InvalidMessagesJSONFileError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "The messages JSON file is missing a required key." + @override def __init__(self, message: str | None = None, missing_key: str | None = None) -> None: - """Initialize a new InvalidMessagesJSONFile exception for a missing key.""" + """Initialise a new InvalidMessagesJSONFile exception for a missing key.""" super().__init__(message, dict_key=missing_key) @property @@ -59,13 +62,13 @@ class MessagesJSONFileValueError(InvalidMessagesJSONFileError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "The messages JSON file has an invalid value." + @override def __init__(self, message: str | None = None, dict_key: str | None = None, invalid_value: object | None = None) -> None: # noqa: E501 - """Initialize a new InvalidMessagesJSONFile exception for a key's invalid value.""" + """Initialise a new InvalidMessagesJSONFile exception for a key's invalid value.""" self.invalid_value: object | None = invalid_value super().__init__(message, dict_key) - diff --git a/exceptions/strike.py b/exceptions/strike.py index e4c0bc226..5624a4c90 100644 --- a/exceptions/strike.py +++ b/exceptions/strike.py @@ -7,6 +7,9 @@ "NoAuditLogsStrikeTrackingError", ) + +from typing import override + from classproperties import classproperty from .base import BaseTeXBotError @@ -22,8 +25,8 @@ class StrikeTrackingError(BaseTeXBotError, RuntimeError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "An error occurred while trying to track manually applied moderation actions." @@ -37,7 +40,6 @@ class NoAuditLogsStrikeTrackingError(BaseTeXBotError, RuntimeError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "Unable to retrieve audit log entry after possible manual moderation action." - diff --git a/main.py b/main.py index cb6d12c25..66d1b1509 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,19 @@ #!/usr/bin/env python """ -The main entrypoint into the running of the bot. +The main entrypoint into the running of TeX-Bot. It loads the settings values from the .env file/the environment variables, then ensures the Django database is correctly migrated to the latest version and finally begins -the asynchronous running process for the Discord bot. +the asynchronous running process for TeX-Bot. """ from collections.abc import Sequence -__all__: Sequence[str] = ("bot",) +__all__: Sequence[str] = ("tex_bot",) +from typing import NoReturn + import discord import config @@ -25,14 +27,19 @@ # noinspection PyDunderSlots,PyUnresolvedReferences intents.members = True - bot = TeXBot(intents=intents) + tex_bot: TeXBot = TeXBot(intents=intents) -bot.load_extension("cogs") + tex_bot.load_extension("cogs") -if __name__ == "__main__": - bot.run(settings["DISCORD_BOT_TOKEN"]) - if bot.EXIT_WAS_DUE_TO_KILL_COMMAND: +def _run_tex_bot() -> NoReturn: + tex_bot.run(settings["DISCORD_BOT_TOKEN"]) + + if tex_bot.EXIT_WAS_DUE_TO_KILL_COMMAND: raise SystemExit(0) raise SystemExit(1) + + +if __name__ == "__main__": + _run_tex_bot() diff --git a/poetry.lock b/poetry.lock index b6b9ff0fd..3cd5cd045 100644 --- a/poetry.lock +++ b/poetry.lock @@ -141,13 +141,13 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "asyncstdlib" -version = "3.12.3" +version = "3.12.4" description = "The missing async toolbox" optional = false python-versions = "~=3.8" files = [ - {file = "asyncstdlib-3.12.3-py3-none-any.whl", hash = "sha256:ef4660462338fa30746fef4166dd01df22b07ab072b7eb1f52f10b2a974c8cf8"}, - {file = "asyncstdlib-3.12.3.tar.gz", hash = "sha256:2acd0c04e205965cc2bb063b75370df92d207a3035bc4f83fb6b5686cffad7a0"}, + {file = "asyncstdlib-3.12.4-py3-none-any.whl", hash = "sha256:8e269c30906658faca35936d0348c1057aff4df1ee125f6ce564feeb72212d5e"}, + {file = "asyncstdlib-3.12.4.tar.gz", hash = "sha256:c87e2e2ebfea47d24af728e1caab2a4fb705228508679f30e34afdcbd0097a05"}, ] [package.extras] @@ -197,29 +197,29 @@ lxml = ["lxml"] [[package]] name = "ccft-pymarkdown" -version = "1.1.0" +version = "1.1.2" description = "A Python wrapper around jackdewinter's PyMarkdown linter to suppress errors, caused by custom-formatted tables in Markdown files" optional = false python-versions = "<4.0,>=3.12" files = [ - {file = "ccft_pymarkdown-1.1.0-py3-none-any.whl", hash = "sha256:9ce13b5888d0498c9fccf765332fc5ed62009fd759fa81261f9aeb4774b8a6cc"}, - {file = "ccft_pymarkdown-1.1.0.tar.gz", hash = "sha256:e158d5511ff91fbe38d7d96929112a8bf7bade13bc52118130ca31c653ea4fe3"}, + {file = "ccft_pymarkdown-1.1.2-py3-none-any.whl", hash = "sha256:ed81c80179205274d3d9e7e5551a4ba62ffbf79b2f44658fa098b64210080ce9"}, + {file = "ccft_pymarkdown-1.1.2.tar.gz", hash = "sha256:ebebfca022fa542c44ca78a4e6b62a1c7706ec124ce22c6e773040f09665a632"}, ] [package.dependencies] gitpython = ">=3.1,<4.0" pymarkdownlnt = ">=0.9,<0.10" -setuptools = ">=69.5,<70.0" +setuptools = ">=70.0,<71.0" [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -481,13 +481,13 @@ files = [ [[package]] name = "django" -version = "5.0.6" +version = "5.0.7" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.0.6-py3-none-any.whl", hash = "sha256:8363ac062bb4ef7c3f12d078f6fa5d154031d129a15170a1066412af49d30905"}, - {file = "Django-5.0.6.tar.gz", hash = "sha256:ff1b61005004e476e0aeea47c7f79b85864c70124030e95146315396f1e7951f"}, + {file = "Django-5.0.7-py3-none-any.whl", hash = "sha256:f216510ace3de5de01329463a315a629f33480e893a9024fc93d8c32c22913da"}, + {file = "Django-5.0.7.tar.gz", hash = "sha256:bd4505cae0b9bd642313e8fb71810893df5dc2ffcacaa67a33af2d5cd61888f2"}, ] [package.dependencies] @@ -501,36 +501,37 @@ bcrypt = ["bcrypt"] [[package]] name = "django-stubs" -version = "5.0.0" +version = "5.0.2" description = "Mypy stubs for Django" optional = false python-versions = ">=3.8" files = [ - {file = "django_stubs-5.0.0-py3-none-any.whl", hash = "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d"}, - {file = "django_stubs-5.0.0.tar.gz", hash = "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"}, + {file = "django_stubs-5.0.2-py3-none-any.whl", hash = "sha256:cb0c506cb5c54c64612e4a2ee8d6b913c6178560ec168009fe847c09747c304b"}, + {file = "django_stubs-5.0.2.tar.gz", hash = "sha256:236bc5606e5607cb968f92b648471f9edaa461a774bc013bf9e6bff8730f6bdf"}, ] [package.dependencies] asgiref = "*" django = "*" -django-stubs-ext = ">=5.0.0" +django-stubs-ext = ">=5.0.2" mypy = {version = ">=1.10.0,<1.11.0", optional = true, markers = "extra == \"compatible-mypy\""} types-PyYAML = "*" -typing-extensions = "*" +typing-extensions = ">=4.11.0" [package.extras] compatible-mypy = ["mypy (>=1.10.0,<1.11.0)"] +oracle = ["oracledb"] redis = ["redis"] [[package]] name = "django-stubs-ext" -version = "5.0.0" +version = "5.0.2" description = "Monkey-patching and extensions for django-stubs" optional = false python-versions = ">=3.8" files = [ - {file = "django_stubs_ext-5.0.0-py3-none-any.whl", hash = "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8"}, - {file = "django_stubs_ext-5.0.0.tar.gz", hash = "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115"}, + {file = "django_stubs_ext-5.0.2-py3-none-any.whl", hash = "sha256:8d8efec5a86241266bec94a528fe21258ad90d78c67307f3ae5f36e81de97f12"}, + {file = "django_stubs_ext-5.0.2.tar.gz", hash = "sha256:409c62585d7f996cef5c760e6e27ea3ff29f961c943747e67519c837422cad32"}, ] [package.dependencies] @@ -556,69 +557,69 @@ dev = ["coverage", "pytest (>=7.4.4)"] [[package]] name = "filelock" -version = "3.14.0" +version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "fonttools" -version = "4.51.0" +version = "4.53.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"}, - {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"}, - {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"}, - {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"}, - {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"}, - {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"}, - {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"}, - {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"}, - {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"}, - {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"}, - {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"}, - {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"}, - {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"}, - {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"}, - {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"}, - {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"}, - {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"}, - {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"}, - {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"}, - {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"}, - {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"}, - {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"}, - {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"}, - {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"}, - {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"}, - {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"}, - {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"}, - {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"}, - {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"}, - {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"}, - {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"}, - {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"}, - {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"}, - {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"}, - {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"}, - {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"}, - {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"}, - {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"}, - {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"}, - {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"}, - {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"}, - {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, + {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, + {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, + {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, + {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, + {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, + {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, + {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, + {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, + {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, + {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, + {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, + {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, ] [package.extras] @@ -755,13 +756,13 @@ test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", [[package]] name = "identify" -version = "2.5.36" +version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, - {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, ] [package.extras] @@ -904,40 +905,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.0" +version = "3.9.1" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, - {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, - {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, - {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, - {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, - {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, - {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, - {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, - {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, - {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, - {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, - {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, - {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, - {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, - {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, - {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, - {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, - {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, - {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, - {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, - {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, - {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, - {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, - {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, - {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, + {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, + {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, + {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, + {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, + {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, + {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, + {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, + {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, + {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, ] [package.dependencies] @@ -1069,38 +1070,38 @@ files = [ [[package]] name = "mypy" -version = "1.10.0" +version = "1.10.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, ] [package.dependencies] @@ -1126,72 +1127,78 @@ files = [ [[package]] name = "nodeenv" -version = "1.8.0" +version = "1.9.1" description = "Node.js virtual environment builder" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] -[package.dependencies] -setuptools = "*" - [[package]] name = "numpy" -version = "1.26.4" +version = "2.0.0" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787"}, + {file = "numpy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98"}, + {file = "numpy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f"}, + {file = "numpy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f"}, + {file = "numpy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"}, + {file = "numpy-2.0.0-cp312-cp312-win32.whl", hash = "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54"}, + {file = "numpy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44"}, + {file = "numpy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275"}, + {file = "numpy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9"}, + {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"}, ] [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1207,84 +1214,95 @@ files = [ [[package]] name = "pillow" -version = "10.3.0" +version = "10.4.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -1342,13 +1360,13 @@ virtualenv = ">=20.10.0" [[package]] name = "py-cord" -version = "2.5.0" +version = "2.6.0" description = "A Python wrapper for the Discord API" optional = false python-versions = ">=3.8" files = [ - {file = "py-cord-2.5.0.tar.gz", hash = "sha256:faf08af5da5eac2ed3d1c8a43d8307d5a1e3f01602def283330c9d2cde0b1162"}, - {file = "py_cord-2.5.0-py3-none-any.whl", hash = "sha256:9e5fc79feec5a48f53aa4c066b57dd75fe67d29021b042d12f378a513d308bbc"}, + {file = "py_cord-2.6.0-py3-none-any.whl", hash = "sha256:906cc077904a4d478af0264ae4374b0412ed9f9ab950d0162c4f31239a906d26"}, + {file = "py_cord-2.6.0.tar.gz", hash = "sha256:bbc0349542965d05e4b18cc4424136206430a8cc911fda12a0a57df6fdf9cd9c"}, ] [package.dependencies] @@ -1361,13 +1379,13 @@ voice = ["PyNaCl (>=1.3.0,<1.6)"] [[package]] name = "pymarkdownlnt" -version = "0.9.19" +version = "0.9.21" description = "A GitHub Flavored Markdown compliant Markdown linter." optional = false python-versions = ">=3.8.0" files = [ - {file = "pymarkdownlnt-0.9.19-py3-none-any.whl", hash = "sha256:f1584d1b559fef634f83bdfb004b9d446cccc14a8448d01ff66c67d5949e1b7d"}, - {file = "pymarkdownlnt-0.9.19.tar.gz", hash = "sha256:4ef7b1a2b1ab67e6c5e8859d773f5367638158bd699869f8fbdfdf36d68359d8"}, + {file = "pymarkdownlnt-0.9.21-py3-none-any.whl", hash = "sha256:5efc4092898469c92cdc33686c356f3d94afb99aca09addf79119ba58959f350"}, + {file = "pymarkdownlnt-0.9.21.tar.gz", hash = "sha256:f30f231f1fa36d98d646c7d2bedf2e8f3696a78b5e420d7a40a0d2f1c9159c0a"}, ] [package.dependencies] @@ -1391,13 +1409,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "8.2.1" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, - {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -1479,6 +1497,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1515,13 +1534,13 @@ files = [ [[package]] name = "requests" -version = "2.32.1" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.1-py3-none-any.whl", hash = "sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5"}, - {file = "requests-2.32.1.tar.gz", hash = "sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -1536,45 +1555,45 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.4" +version = "0.5.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, - {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, - {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, - {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, - {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, - {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, + {file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"}, + {file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"}, + {file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"}, + {file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"}, + {file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"}, + {file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"}, + {file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"}, ] [[package]] name = "setuptools" -version = "69.5.1" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, - {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -1684,13 +1703,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -1706,13 +1725,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -1723,24 +1742,24 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "validators" -version = "0.28.1" +version = "0.28.3" description = "Python Data Validation for Humans™" optional = false python-versions = ">=3.8" files = [ - {file = "validators-0.28.1-py3-none-any.whl", hash = "sha256:890c98789ad884037f059af6ea915ec2d667129d509180c2c590b8009a4c4219"}, - {file = "validators-0.28.1.tar.gz", hash = "sha256:5ac88e7916c3405f0ce38ac2ac82a477fcf4d90dbbeddd04c8193171fc17f7dc"}, + {file = "validators-0.28.3-py3-none-any.whl", hash = "sha256:53cafa854f13850156259d9cc479b864ee901f6a96e6b109e6fc33f98f37d99f"}, + {file = "validators-0.28.3.tar.gz", hash = "sha256:c6c79840bcde9ba77b19f6218f7738188115e27830cbaff43264bc4ed24c429d"}, ] [[package]] name = "virtualenv" -version = "20.26.2" +version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] [package.dependencies] @@ -1869,4 +1888,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "faae6fed029dbf7b339a5f496bb200e6e4ab0b2b7b9110c7fb586d4d844b7767" +content-hash = "bf047957d97d664f359a6d2699935aa87f32fb088d992ee026b90dc44fe126ee" diff --git a/pyproject.toml b/pyproject.toml index 380acc911..873cc33a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.12" -py-cord = "~2.5" +py-cord = "~2.6" python-dotenv = "^1.0" validators = "^0.28" beautifulsoup4 = "^4.12" @@ -41,15 +41,15 @@ mplcyberpunk = "^0.7" python-logging-discord-handler = "^0.1" classproperties = {git = "https://github.com/hottwaj/classproperties.git"} asyncstdlib = "~3.12" -setuptools = "^69.5" +setuptools = "^70.3" [tool.poetry.group.dev.dependencies] pre-commit = "^3.7" mypy = "~1.10" django-stubs = {extras = ["compatible-mypy"], version = "~5.0"} -types-beautifulsoup4 = "^4.12.0" +types-beautifulsoup4 = "^4.12" pytest = "^8.2" -ruff = "^0.4" +ruff = "^0.5" gitpython = "^3.1" pymarkdownlnt = "^0.9" ccft-pymarkdown = "^1.1" @@ -69,7 +69,7 @@ disallow_untyped_calls = true disallow_untyped_defs = true check_untyped_defs = true disallow_any_generics = true -disallow_any_unimported = true +disallow_any_unimported = false disallow_any_decorated = true disallow_any_explicit = true disallow_subclassing_any = true @@ -97,6 +97,7 @@ django_settings_module = "db._settings" [tool.ruff] +output-format = "concise" line-length = 95 target-version ="py312" extend-exclude = [ diff --git a/tests/test_utils.py b/tests/test_utils.py index b6dcfd8ea..a10ed97fa 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -87,15 +87,21 @@ def test_url_generates() -> None: DISCORD_BOT_APPLICATION_ID: Final[int] = random.randint( 10000000000000000, 99999999999999999999, ) - DISCORD_GUILD_ID: Final[int] = random.randint( + DISCORD_MAIN_GUILD_ID: Final[int] = random.randint( 10000000000000000, 99999999999999999999, ) invite_url: str = utils.generate_invite_url( - DISCORD_BOT_APPLICATION_ID, DISCORD_GUILD_ID, + DISCORD_BOT_APPLICATION_ID, DISCORD_MAIN_GUILD_ID, ) - assert re.match( - f"https://discord.com/.*={DISCORD_BOT_APPLICATION_ID}.*={DISCORD_GUILD_ID}", + assert re.fullmatch( + ( + r"\Ahttps://discord.com/.*=" + + str(DISCORD_BOT_APPLICATION_ID) + + r".*=" + + str(DISCORD_MAIN_GUILD_ID) + + r".*\Z" + ), invite_url, ) diff --git a/utils/__init__.py b/utils/__init__.py index fac494628..4f536d2d4 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -4,7 +4,7 @@ __all__: Sequence[str] = ( "CommandChecks", - "MessageSenderComponent", + "MessageSavingSenderComponent", "SuppressTraceback", "TeXBot", "TeXBotBaseCog", @@ -22,12 +22,12 @@ import discord -from utils.command_checks import CommandChecks -from utils.message_sender_components import MessageSenderComponent -from utils.suppress_traceback import SuppressTraceback -from utils.tex_bot import TeXBot -from utils.tex_bot_base_cog import TeXBotBaseCog -from utils.tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext +from .command_checks import CommandChecks +from .message_sender_components import MessageSavingSenderComponent +from .suppress_traceback import SuppressTraceback +from .tex_bot import TeXBot +from .tex_bot_base_cog import TeXBotBaseCog +from .tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext AllChannelTypes: TypeAlias = ( discord.VoiceChannel @@ -69,10 +69,10 @@ def is_member_inducted(member: discord.Member) -> bool: Util method to check if the supplied member has been inducted. Returns True if the member has any role other than "@News". - The set of ignored roles is a tuple, to make the set easily expandable. + The set of ignored roles is a tuple to make the set easily expandable. """ return any( - role.name.lower().strip().strip("@").strip() not in ("news",) for role in member.roles + role.name.lower().strip("@ \n\t") not in ("news",) for role in member.roles ) diff --git a/utils/command_checks.py b/utils/command_checks.py index 330d1f9c4..fc5bba769 100644 --- a/utils/command_checks.py +++ b/utils/command_checks.py @@ -23,7 +23,7 @@ class CommandChecks: @staticmethod async def _check_interaction_user_in_main_guild(ctx: TeXBotApplicationContext) -> bool: try: - await ctx.bot.get_main_guild_member(ctx.user) + await ctx.tex_bot.get_main_guild_member(ctx.user) except DiscordMemberNotInMainGuildError: return False return True @@ -38,7 +38,7 @@ async def _check_interaction_user_in_main_guild(ctx: TeXBotApplicationContext) - @staticmethod async def _check_interaction_user_has_committee_role(ctx: TeXBotApplicationContext) -> bool: # noqa: E501 - return await ctx.bot.check_user_has_committee_role(ctx.user) + return await ctx.tex_bot.check_user_has_committee_role(ctx.user) check_interaction_user_has_committee_role: Callable[[T], T] """ @@ -50,11 +50,13 @@ async def _check_interaction_user_has_committee_role(ctx: TeXBotApplicationConte @classmethod def is_interaction_user_in_main_guild_failure(cls, check: CheckFailure) -> bool: + # noinspection GrazieInspection """Whether check failed due to the interaction user not being in your Discord guild.""" return bool(check.__name__ == cls._check_interaction_user_in_main_guild.__name__) # type: ignore[attr-defined] @classmethod def is_interaction_user_has_committee_role_failure(cls, check: CheckFailure) -> bool: + # noinspection GrazieInspection """Whether check failed due to the interaction user not having the committee role.""" return bool(check.__name__ == cls._check_interaction_user_has_committee_role.__name__) # type: ignore[attr-defined] diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index d8dc35184..397fa0962 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -20,7 +20,8 @@ from typing import TYPE_CHECKING, Final, ParamSpec, TypeVar from exceptions import GuildDoesNotExistError, StrikeTrackingError -from utils.tex_bot_base_cog import TeXBotBaseCog + +from .tex_bot_base_cog import TeXBotBaseCog if TYPE_CHECKING: from typing import Concatenate, TypeAlias @@ -39,7 +40,7 @@ Callable[Concatenate[TeXBotBaseCog, P], Coroutine[object, object, T]] ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class ErrorCaptureDecorators: @@ -68,8 +69,7 @@ async def wrapper(self: TeXBotBaseCog, /, *args: P.args, **kwargs: P.kwargs) -> return await func(self, *args, **kwargs) except error_type as error: close_func(error) - await self.bot.close() - return None + await self.tex_bot.close() return wrapper # type: ignore[return-value] @staticmethod diff --git a/utils/message_sender_components.py b/utils/message_sender_components.py index 196bb0fe8..45f444468 100644 --- a/utils/message_sender_components.py +++ b/utils/message_sender_components.py @@ -3,7 +3,7 @@ from collections.abc import Sequence __all__: Sequence[str] = ( - "MessageSenderComponent", + "MessageSavingSenderComponent", "ChannelMessageSender", "ResponseMessageSender", ) @@ -15,12 +15,12 @@ import discord from discord.ui import View -from utils.tex_bot_contexts import TeXBotApplicationContext +from .tex_bot_contexts import TeXBotApplicationContext -class MessageSenderComponent(abc.ABC): +class MessageSavingSenderComponent(abc.ABC): """ - Abstract protocol definition of a sending component. + Abstract protocol definition of a sending component that saves the sent-message. Defines the way to send a provided message content & optional view to the defined endpoint. """ @@ -64,7 +64,7 @@ async def delete(self) -> None: await self.sent_message.delete_original_message() -class ChannelMessageSender(MessageSenderComponent): +class ChannelMessageSender(MessageSavingSenderComponent): """ Concrete definition of a channel sending component. @@ -100,7 +100,7 @@ class ChannelSendKwargs(_BaseChannelSendKwargs, total=False): return await self.channel.send(**send_kwargs) -class ResponseMessageSender(MessageSenderComponent): +class ResponseMessageSender(MessageSavingSenderComponent): """ Concrete definition of a context-based response sending component. diff --git a/utils/suppress_traceback.py b/utils/suppress_traceback.py index 81a671644..807cef9d2 100644 --- a/utils/suppress_traceback.py +++ b/utils/suppress_traceback.py @@ -11,6 +11,7 @@ import sys from types import TracebackType +from typing import override class SuppressTraceback: @@ -20,6 +21,7 @@ class SuppressTraceback: The previous traceback limit is returned when exiting the context manager. """ + @override def __init__(self) -> None: # noinspection SpellCheckingInspection """ diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 1d0cc594b..701733257 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -1,4 +1,4 @@ -"""Custom bot implementation to override the default bot class provided by Pycord.""" +"""Custom Pycord Bot class implementation.""" from collections.abc import Sequence @@ -8,7 +8,7 @@ import logging import re from logging import Logger -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, NoReturn, override import aiohttp import discord @@ -33,7 +33,7 @@ if TYPE_CHECKING: from utils import AllChannelTypes -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class TeXBot(discord.Bot): @@ -45,8 +45,9 @@ class TeXBot(discord.Bot): if these objects do not exist. """ + @override def __init__(self, *args: object, **options: object) -> None: - """Initialize a new discord.Bot subclass with empty shortcut accessors.""" + """Initialise a new Pycord Bot subclass with empty shortcut accessors.""" self._main_guild: discord.Guild | None = None self._committee_role: discord.Role | None = None self._committee_elect_role: discord.Role | None = None @@ -63,6 +64,15 @@ def __init__(self, *args: object, **options: object) -> None: super().__init__(*args, **options) # type: ignore[no-untyped-call] + @override + async def close(self) -> NoReturn: + await super().close() + + TEX_BOT_NOT_CLOSED_CORRECTLY_MESSAGE: Final[str] = ( + "TeX-Bot did not shutdown correctly." + ) + raise RuntimeError(TEX_BOT_NOT_CLOSED_CORRECTLY_MESSAGE) + # noinspection PyPep8Naming @property def EXIT_WAS_DUE_TO_KILL_COMMAND(self) -> bool: # noqa: N802 @@ -80,10 +90,14 @@ def main_guild(self) -> discord.Guild: Raises `GuildDoesNotExist` if the given ID does not link to a valid Discord guild. """ - if not self._main_guild or not self._bot_has_guild(settings["DISCORD_GUILD_ID"]): - raise GuildDoesNotExistError(guild_id=settings["DISCORD_GUILD_ID"]) + MAIN_GUILD_EXISTS: Final[bool] = bool( + self._main_guild + and self._tex_bot_has_guild(settings["_DISCORD_MAIN_GUILD_ID"]) # noqa: COM812 + ) + if not MAIN_GUILD_EXISTS: + raise GuildDoesNotExistError(guild_id=settings["_DISCORD_MAIN_GUILD_ID"]) - return self._main_guild + return self._main_guild # type: ignore[return-value] @property async def committee_role(self) -> discord.Role: @@ -185,7 +199,7 @@ async def archivist_role(self) -> discord.Role: Shortcut accessor to the archivist role. The archivist role is the one that allows members to see channels & categories - that are no longer in use, which are hidden to all other members. + that are no longer in use, which are hidden from all other members. Raises `ArchivistRoleDoesNotExist` if the role does not exist. """ @@ -277,8 +291,8 @@ def group_full_name(self) -> str: The full name of your community group. This is substituted into many error/welcome messages sent into your Discord guild, - by the bot. - The group-full-name is either retrieved from the provided environment variable, + by TeX-Bot. + The group-full-name is either retrieved from the provided environment variable or automatically identified from the name of your group's Discord guild. """ return ( # type: ignore[no-any-return] @@ -370,7 +384,7 @@ def group_moderation_contact(self) -> str: else "our community moderators" ) - def _bot_has_guild(self, guild_id: int) -> bool: + def _tex_bot_has_guild(self, guild_id: int) -> bool: return bool(discord.utils.get(self.guilds, id=guild_id)) def _guild_has_role(self, role: discord.Role) -> bool: @@ -394,7 +408,7 @@ async def _fetch_text_channel(self, name: str) -> discord.TextChannel | None: return text_channel - async def perform_kill_and_close(self, initiated_by_user: discord.User | discord.Member | None = None) -> None: # noqa: E501 + async def perform_kill_and_close(self, initiated_by_user: discord.User | discord.Member | None = None) -> NoReturn: # noqa: E501 """ Shutdown TeX-Bot by using the "/kill" command. @@ -433,13 +447,13 @@ async def check_user_has_committee_role(self, user: discord.Member | discord.Use def set_main_guild(self, main_guild: discord.Guild) -> None: """ - Set the main_guild value that the bot will reference in the future. + Set the main_guild value that TeX-Bot will reference in the future. This can only be set once. """ if self._main_guild_set: MAIN_GUILD_SET_MESSAGE: Final[str] = ( - "The bot's main_guild property has already been set, it cannot be changed." + "TeX-Bot's main_guild property has already been set, it cannot be changed." ) raise RuntimeError(MAIN_GUILD_SET_MESSAGE) @@ -466,9 +480,9 @@ async def get_member_from_str_id(self, str_member_id: str) -> discord.Member: """ str_member_id = str_member_id.replace("<@", "").replace(">", "") - if not re.match(r"\A\d{17,20}\Z", str_member_id): + if not re.fullmatch(r"\A\d{17,20}\Z", str_member_id): INVALID_USER_ID_MESSAGE: Final[str] = ( - f"\"{str_member_id}\" is not a valid user ID." + f"'{str_member_id}' is not a valid user ID." ) raise ValueError(INVALID_USER_ID_MESSAGE) diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index b59983ce9..669df4390 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -8,9 +8,9 @@ import contextlib import logging import re -from collections.abc import Mapping +from collections.abc import Mapping, Set from logging import Logger -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, override import discord from discord import Cog @@ -19,8 +19,9 @@ from exceptions.base import ( BaseDoesNotExistError, ) -from utils.tex_bot import TeXBot -from utils.tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext + +from .tex_bot import TeXBot +from .tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext if TYPE_CHECKING: from typing import TypeAlias @@ -29,11 +30,11 @@ if TYPE_CHECKING: MentionableMember: TypeAlias = discord.Member | discord.Role -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class TeXBotBaseCog(Cog): - """Base Cog subclass that stores a reference to the currently running bot.""" + """Base Cog subclass that stores a reference to the currently running TeXBot instance.""" ERROR_ACTIVITIES: Final[Mapping[str, str]] = { "archive": "archive the selected category", @@ -61,9 +62,14 @@ class TeXBotBaseCog(Cog): "write_roles": "send messages", } - def __init__(self, bot: TeXBot) -> None: - """Initialize a new cog instance, storing a reference to the bot object.""" - self.bot: TeXBot = bot + @override + def __init__(self, tex_bot: TeXBot) -> None: + """ + Initialise a new cog instance. + + During initialisation, a reference to the currently running TeXBot instance is stored. + """ + self.tex_bot: TeXBot = tex_bot async def command_send_error(self, ctx: TeXBotApplicationContext, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None) -> None: # noqa: E501 """ @@ -81,7 +87,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, error_code: st ) await self.send_error( - self.bot, + self.tex_bot, ctx.interaction, interaction_name=COMMAND_NAME, error_code=error_code, @@ -90,7 +96,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, error_code: st ) @classmethod - async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None) -> None: # noqa: E501 + async def send_error(cls, tex_bot: TeXBot, interaction: discord.Interaction, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None) -> None: # noqa: E501 """ Construct & format an error message from the given details. @@ -103,7 +109,7 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interac committee_mention: str = "committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_mention = (await bot.committee_role).mention + committee_mention = (await tex_bot.committee_role).mention construct_error_message = ( f"**Contact a {committee_mention} member, referencing error code: " @@ -148,29 +154,29 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interac ) @staticmethod - async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice]: # noqa: E501 + async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable channels. - The list of available selectable channels is unique to each member, and is used in any + The list of available selectable channels is unique to each member and is used in any slash-command options that have a channel input-type. """ if not ctx.interaction.user: return set() try: - main_guild: discord.Guild = ctx.bot.main_guild + main_guild: discord.Guild = ctx.tex_bot.main_guild # noinspection PyUnusedLocal - channel_permissions_limiter: MentionableMember = await ctx.bot.guest_role + channel_permissions_limiter: MentionableMember = await ctx.tex_bot.guest_role except BaseDoesNotExistError: return set() with contextlib.suppress(DiscordMemberNotInMainGuildError): - channel_permissions_limiter = await ctx.bot.get_main_guild_member( + channel_permissions_limiter = await ctx.tex_bot.get_main_guild_member( ctx.interaction.user, ) - if not ctx.value or re.match(r"\A#.*\Z", ctx.value): + if not ctx.value or re.fullmatch(r"\A#.*\Z", ctx.value): return { discord.OptionChoice(name=f"#{channel.name}", value=str(channel.id)) for channel diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 28170ecff..0a7c7c99a 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -1,7 +1,7 @@ """ Type-hinting classes that override the Pycord Context classes. -These custom overriden classes contain a reference to the custom bot class TeXBot, +These custom, overridden classes contain a reference to the custom bot class TeXBot, rather than Pycord's default Bot class. """ @@ -10,30 +10,51 @@ __all__: Sequence[str] = ("TeXBotAutocompleteContext", "TeXBotApplicationContext") +import abc +from typing import final, override + import discord -from utils.tex_bot import TeXBot +from .tex_bot import TeXBot + + +class _TeXBotContextMixin(abc.ABC): # noqa: B024 + @override + def __init__(self, tex_bot: TeXBot, interaction: discord.Interaction) -> None: + self._tex_bot: TeXBot = tex_bot + + super().__init__(tex_bot, interaction) # type: ignore[call-arg] + + @property + def tex_bot(self) -> TeXBot: + return self._tex_bot + @property # type: ignore[misc] + @final + def bot(self) -> discord.Bot: + raise DeprecationWarning -class TeXBotAutocompleteContext(discord.AutocompleteContext): + @bot.setter + @final + def bot(self, __value: discord.Bot, /) -> None: + raise DeprecationWarning + + +class TeXBotAutocompleteContext(_TeXBotContextMixin, discord.AutocompleteContext): # type: ignore[misc] """ Type-hinting class overriding AutocompleteContext's reference to the Bot class. - Pycord's default AutocompleteContext references the standard discord.Bot class, + Pycord's default AutocompleteContext references Pycord's standard Bot class, but cogs require a reference to the TeXBot class, so this AutocompleteContext subclass should be used in cogs instead. """ - bot: TeXBot - -class TeXBotApplicationContext(discord.ApplicationContext): +class TeXBotApplicationContext(_TeXBotContextMixin, discord.ApplicationContext): # type: ignore[misc] """ Type-hinting class overriding ApplicationContext's reference to the Bot class. - Pycord's default ApplicationContext references the standard discord.Bot class, + Pycord's default ApplicationContext references Pycord's standard Bot class, but cogs require a reference to the TeXBot class, so this ApplicationContext subclass should be used in cogs instead. """ - - bot: TeXBot