From 05fc775c9e5dabbe129879509de06e77f69d4e36 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Tue, 21 May 2024 20:14:10 +0100 Subject: [PATCH 001/128] Move config.py --- config.py => config/__init__.py | 0 config/settings.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename config.py => config/__init__.py (100%) create mode 100644 config/settings.py diff --git a/config.py b/config/__init__.py similarity index 100% rename from config.py rename to config/__init__.py diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 000000000..e69de29bb From c8d8ed3dfe2dde5cdbf58d6e1c13de2af37fca75 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 22 May 2024 03:00:08 +0100 Subject: [PATCH 002/128] Initial changes to declare YAML schema --- .gitignore | 12 + cogs/archive.py | 3 +- cogs/command_error.py | 3 +- cogs/kill.py | 2 +- cogs/make_member.py | 2 +- cogs/remind_me.py | 3 +- cogs/send_get_roles_reminders.py | 2 +- cogs/send_introduction_reminders.py | 2 +- cogs/startup.py | 3 +- config/__init__.py | 1502 ++++++++++++++------------- config/_settings.py | 89 ++ config/_yaml.py | 289 ++++++ config/constants.py | 51 + config/settings.py | 0 db/core/models/managers.py | 2 +- poetry.lock | 69 +- pyproject.toml | 9 +- utils/error_capture_decorators.py | 2 +- utils/tex_bot.py | 2 +- utils/tex_bot_base_cog.py | 2 +- 20 files changed, 1302 insertions(+), 747 deletions(-) create mode 100644 config/_settings.py create mode 100644 config/_yaml.py create mode 100644 config/constants.py delete mode 100644 config/settings.py diff --git a/.gitignore b/.gitignore index f4d92b6a9..6b3e3b37e 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,15 @@ local/ *.sqlite3.bak *.db.bak local_stubs/ +TeX-Bot-deployment.yaml +TeX-Bot-deployment.yml +tex-bot-deployment.yaml +tex-bot-deployment.yml +TeX-Bot-settings.yaml +TeX-Bot-settings.yml +tex-bot-settings.yaml +tex-bot-settings.yml +TeX-Bot-config.yaml +TeX-Bot-config.yml +tex-bot-config.yaml +tex-bot-config.yml diff --git a/cogs/archive.py b/cogs/archive.py index 761512d4f..27c45cc8f 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -8,6 +8,7 @@ import logging import re from logging import Logger +from typing import Final import discord @@ -19,7 +20,7 @@ TeXBotBaseCog, ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class ArchiveCommandCog(TeXBotBaseCog): diff --git a/cogs/command_error.py b/cogs/command_error.py index bf4e4ce66..8a5f5e814 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 utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class CommandErrorCog(TeXBotBaseCog): diff --git a/cogs/kill.py b/cogs/kill.py index 37ac34e5f..b61806743 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -16,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): diff --git a/cogs/make_member.py b/cogs/make_member.py index be6c681fe..b53ab4974 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -22,7 +22,7 @@ from exceptions import CommitteeRoleDoesNotExistError, GuestRoleDoesNotExistError 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"""{ diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 3505cef13..705d54e7d 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -27,7 +27,7 @@ from collections.abc import Iterator -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class RemindMeCommandCog(TeXBotBaseCog): @@ -312,6 +312,7 @@ async def clear_reminders_backlog(self) -> None: await reminder.adelete() continue + # noinspection PyUnresolvedReferences channel: discord.PartialMessageable = self.bot.get_partial_messageable( reminder.channel_id, type=( diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 8d713c3ec..617e0def9 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: import datetime -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class SendGetRolesRemindersTaskCog(TeXBotBaseCog): diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index daf717011..b9e1f50f5 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -30,7 +30,7 @@ capture_guild_does_not_exist_error, ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class SendIntroductionRemindersTaskCog(TeXBotBaseCog): diff --git a/cogs/startup.py b/cogs/startup.py index 53c9d8ef3..981211b01 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): diff --git a/config/__init__.py b/config/__init__.py index f1238577d..0940b00c7 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -8,744 +8,82 @@ from collections.abc import Sequence __all__: Sequence[str] = ( - "TRUE_VALUES", - "FALSE_VALUES", - "VALID_SEND_INTRODUCTION_REMINDERS_VALUES", - "DEFAULT_STATISTICS_ROLES", - "LOG_LEVEL_CHOICES", + "PROJECT_ROOT", "run_setup", "settings", + "check_for_deprecated_environment_variables", ) -import abc -import functools import importlib -import json import logging import os -import re -from collections.abc import Iterable, Mapping -from datetime import timedelta +from collections.abc import Iterable from logging import Logger from pathlib import Path -from re import Match -from typing import IO, Any, ClassVar, Final, final - -import dotenv -import validators - -from exceptions import ( - ImproperlyConfiguredError, - MessagesJSONFileMissingKeyError, - MessagesJSONFileValueError, -) - -PROJECT_ROOT: Final[Path] = Path(__file__).parent.resolve() - -TRUE_VALUES: Final[frozenset[str]] = frozenset({"true", "1", "t", "y", "yes", "on"}) -FALSE_VALUES: Final[frozenset[str]] = frozenset({"false", "0", "f", "n", "no", "off"}) -VALID_SEND_INTRODUCTION_REMINDERS_VALUES: Final[frozenset[str]] = frozenset( - {"once"} | TRUE_VALUES | FALSE_VALUES, -) -DEFAULT_STATISTICS_ROLES: Final[frozenset[str]] = frozenset( - { - "Committee", - "Committee-Elect", - "Student Rep", - "Member", - "Guest", - "Server Booster", - "Foundation Year", - "First Year", - "Second Year", - "Final Year", - "Year In Industry", - "Year Abroad", - "PGT", - "PGR", - "Alumnus/Alumna", - "Postdoc", - "Quiz Victor", - }, -) -LOG_LEVEL_CHOICES: Final[Sequence[str]] = ( - "DEBUG", - "INFO", - "WARNING", - "ERROR", - "CRITICAL", -) - -logger: Logger = logging.getLogger("TeX-Bot") - - -class Settings(abc.ABC): - """ - Settings class that provides access to all settings values. - - Settings values can be accessed via key (like a dictionary) or via class attribute. - """ - - _is_env_variables_setup: ClassVar[bool] - _settings: ClassVar[dict[str, object]] - - @classmethod - def get_invalid_settings_key_message(cls, item: str) -> str: - """Return the message to state that the given settings key is invalid.""" - return f"{item!r} is not a valid settings key." - - def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 - """Retrieve settings value by attribute lookup.""" - MISSING_ATTRIBUTE_MESSAGE: Final[str] = ( - f"{type(self).__name__!r} object has no attribute {item!r}" - ) - - if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values - raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) - - if not self._is_env_variables_setup: - self._setup_env_variables() - - if item in self._settings: - return self._settings[item] - - if re.match(r"\A[A-Z](?:[A-Z_]*[A-Z])?\Z", item): - INVALID_SETTINGS_KEY_MESSAGE: Final[str] = self.get_invalid_settings_key_message( - item, - ) - raise AttributeError(INVALID_SETTINGS_KEY_MESSAGE) - - raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) - - def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 - """Retrieve settings value by key lookup.""" - attribute_not_exist_error: AttributeError - try: - return getattr(self, item) - except AttributeError as attribute_not_exist_error: - key_error_message: str = item - - if self.get_invalid_settings_key_message(item) in str(attribute_not_exist_error): - key_error_message = str(attribute_not_exist_error) - - raise KeyError(key_error_message) from None - - @staticmethod - def _setup_logging() -> None: - raw_console_log_level: str = str(os.getenv("CONSOLE_LOG_LEVEL", "INFO")).upper() - - if raw_console_log_level not in LOG_LEVEL_CHOICES: - INVALID_LOG_LEVEL_MESSAGE: Final[str] = f"""LOG_LEVEL must be one of { - ",".join(f"{log_level_choice!r}" - for log_level_choice - in LOG_LEVEL_CHOICES[:-1]) - } or {LOG_LEVEL_CHOICES[-1]!r}.""" - raise ImproperlyConfiguredError(INVALID_LOG_LEVEL_MESSAGE) - - logger.setLevel(getattr(logging, raw_console_log_level)) - - console_logging_handler: logging.Handler = logging.StreamHandler() - # noinspection SpellCheckingInspection - console_logging_handler.setFormatter( - logging.Formatter("{asctime} | {name} | {levelname:^8} - {message}", style="{"), - ) - - logger.addHandler(console_logging_handler) - logger.propagate = False - - @classmethod - def _setup_discord_bot_token(cls) -> None: - raw_discord_bot_token: str | None = os.getenv("DISCORD_BOT_TOKEN") - - DISCORD_BOT_TOKEN_IS_VALID: Final[bool] = bool( - raw_discord_bot_token - and re.match( - r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", - raw_discord_bot_token, - ), - ) - if not DISCORD_BOT_TOKEN_IS_VALID: - INVALID_DISCORD_BOT_TOKEN_MESSAGE: Final[str] = ( - "DISCORD_BOT_TOKEN must be a valid Discord bot token " - "(see https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts)." - ) - raise ImproperlyConfiguredError(INVALID_DISCORD_BOT_TOKEN_MESSAGE) - - cls._settings["DISCORD_BOT_TOKEN"] = raw_discord_bot_token - - @classmethod - def _setup_discord_log_channel_webhook_url(cls) -> None: - raw_discord_log_channel_webhook_url: str = os.getenv( - "DISCORD_LOG_CHANNEL_WEBHOOK_URL", - "", - ) - - DISCORD_LOG_CHANNEL_WEBHOOK_URL_IS_VALID: Final[bool] = bool( - not raw_discord_log_channel_webhook_url - or ( - validators.url(raw_discord_log_channel_webhook_url) - and raw_discord_log_channel_webhook_url.startswith( - "https://discord.com/api/webhooks/", - ) +from typing import Final + +from ._settings import SettingsAccessor +from .constants import PROJECT_ROOT + +logger: Final[Logger] = logging.getLogger("TeX-Bot") + +settings: Final[SettingsAccessor] = SettingsAccessor() + + +def _get_settings_file_path() -> Path: + settings_file_not_found_message: str = ( + "No settings file was found. " + "Please make sure you have created a `TeX-Bot-deployment.yaml` file." + ) + + raw_settings_file_path: str | None = ( + os.getenv("TEX_BOT_SETTINGS_FILE_PATH", None) + or os.getenv("TEX_BOT_SETTINGS_FILE", None) + or os.getenv("TEX_BOT_SETTINGS_PATH", None) + or os.getenv("TEX_BOT_SETTINGS", None) + or os.getenv("TEX_BOT_CONFIG_FILE_PATH", None) + or os.getenv("TEX_BOT_CONFIG_FILE", None) + or os.getenv("TEX_BOT_CONFIG_PATH", None) + or os.getenv("TEX_BOT_CONFIG", None) + or os.getenv("TEX_BOT_DEPLOYMENT_FILE_PATH", None) + or os.getenv("TEX_BOT_DEPLOYMENT_FILE", None) + or os.getenv("TEX_BOT_DEPLOYMENT_PATH", None) + or os.getenv("TEX_BOT_DEPLOYMENT", None) + ) + + if raw_settings_file_path: + settings_file_not_found_message = ( + "A path to the settings file location was provided by environment variable, " + "however this path does not refer to an existing file." + ) + else: + logger.debug( + ( + "Settings file location not supplied by environment variable, " + "falling back to `Tex-Bot-deployment.yaml`." ), ) - if not DISCORD_LOG_CHANNEL_WEBHOOK_URL_IS_VALID: - INVALID_DISCORD_LOG_CHANNEL_WEBHOOK_URL_MESSAGE: Final[str] = ( - "DISCORD_LOG_CHANNEL_WEBHOOK_URL must be a valid webhook URL " - "that points to a discord channel where logs should be displayed." - ) - raise ImproperlyConfiguredError(INVALID_DISCORD_LOG_CHANNEL_WEBHOOK_URL_MESSAGE) - - cls._settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"] = raw_discord_log_channel_webhook_url - - @classmethod - def _setup_discord_guild_id(cls) -> None: - raw_discord_guild_id: str | None = os.getenv("DISCORD_GUILD_ID") - - 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), - ) - if not DISCORD_GUILD_ID_IS_VALID: - INVALID_DISCORD_GUILD_ID_MESSAGE: Final[str] = ( - "DISCORD_GUILD_ID must be a valid Discord guild ID " - "(see https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id)." - ) - raise ImproperlyConfiguredError(INVALID_DISCORD_GUILD_ID_MESSAGE) - - cls._settings["DISCORD_GUILD_ID"] = int(raw_discord_guild_id) # type: ignore[arg-type] - - @classmethod - def _setup_group_full_name(cls) -> None: - raw_group_full_name: str | None = os.getenv("GROUP_NAME") - - 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), - ) - if not GROUP_FULL_NAME_IS_VALID: - INVALID_GROUP_FULL_NAME: Final[str] = ( - "GROUP_NAME must not contain any invalid characters." - ) - raise ImproperlyConfiguredError(INVALID_GROUP_FULL_NAME) - cls._settings["_GROUP_FULL_NAME"] = raw_group_full_name - - @classmethod - def _setup_group_short_name(cls) -> None: - raw_group_short_name: str | None = os.getenv("GROUP_SHORT_NAME") - - 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), - ) - if not GROUP_SHORT_NAME_IS_VALID: - INVALID_GROUP_SHORT_NAME: Final[str] = ( - "GROUP_SHORT_NAME must not contain any invalid characters." - ) - raise ImproperlyConfiguredError(INVALID_GROUP_SHORT_NAME) - cls._settings["_GROUP_SHORT_NAME"] = raw_group_short_name - - @classmethod - def _setup_purchase_membership_url(cls) -> None: - raw_purchase_membership_url: str | None = os.getenv("PURCHASE_MEMBERSHIP_URL") - - PURCHASE_MEMBERSHIP_URL_IS_VALID: Final[bool] = bool( - not raw_purchase_membership_url - or validators.url(raw_purchase_membership_url), - ) - if not PURCHASE_MEMBERSHIP_URL_IS_VALID: - INVALID_PURCHASE_MEMBERSHIP_URL_MESSAGE: Final[str] = ( - "PURCHASE_MEMBERSHIP_URL must be a valid URL." - ) - raise ImproperlyConfiguredError(INVALID_PURCHASE_MEMBERSHIP_URL_MESSAGE) - - cls._settings["PURCHASE_MEMBERSHIP_URL"] = raw_purchase_membership_url - - @classmethod - def _setup_membership_perks_url(cls) -> None: - raw_membership_perks_url: str | None = os.getenv("MEMBERSHIP_PERKS_URL") - - MEMBERSHIP_PERKS_URL_IS_VALID: Final[bool] = bool( - not raw_membership_perks_url - or validators.url(raw_membership_perks_url), - ) - if not MEMBERSHIP_PERKS_URL_IS_VALID: - INVALID_MEMBERSHIP_PERKS_URL_MESSAGE: Final[str] = ( - "MEMBERSHIP_PERKS_URL must be a valid URL." - ) - raise ImproperlyConfiguredError(INVALID_MEMBERSHIP_PERKS_URL_MESSAGE) - - cls._settings["MEMBERSHIP_PERKS_URL"] = raw_membership_perks_url - - @classmethod - def _setup_ping_command_easter_egg_probability(cls) -> None: - INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE: Final[str] = ( - "PING_COMMAND_EASTER_EGG_PROBABILITY must be a float between & including 1 & 0." - ) - - e: ValueError - try: - raw_ping_command_easter_egg_probability: float = 100 * float( - os.getenv("PING_COMMAND_EASTER_EGG_PROBABILITY", "0.01"), - ) - except ValueError as e: - raise ( - ImproperlyConfiguredError(INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE) - ) from e - - if not 0 <= raw_ping_command_easter_egg_probability <= 100: - raise ImproperlyConfiguredError( - INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE, - ) - - cls._settings["PING_COMMAND_EASTER_EGG_PROBABILITY"] = ( - raw_ping_command_easter_egg_probability - ) - - @classmethod - @functools.lru_cache(maxsize=5) - def _get_messages_dict(cls, raw_messages_file_path: str | None) -> Mapping[str, object]: - JSON_DECODING_ERROR_MESSAGE: Final[str] = ( - "Messages JSON file must contain a JSON string that can be decoded " - "into a Python dict object." - ) - - messages_file_path: Path = ( - Path(raw_messages_file_path) - if raw_messages_file_path - else PROJECT_ROOT / Path("messages.json") - ) - - if not messages_file_path.is_file(): - MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE: Final[str] = ( - "MESSAGES_FILE_PATH must be a path to a file that exists." - ) - raise ImproperlyConfiguredError(MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE) - - messages_file: IO[str] - with messages_file_path.open(encoding="utf8") as messages_file: - e: json.JSONDecodeError - try: - messages_dict: object = json.load(messages_file) - except json.JSONDecodeError as e: - raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) from e - - if not isinstance(messages_dict, Mapping): - raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) - - return messages_dict - - @classmethod - def _setup_welcome_messages(cls) -> None: - messages_dict: Mapping[str, object] = cls._get_messages_dict( - os.getenv("MESSAGES_FILE_PATH"), - ) - - if "welcome_messages" not in messages_dict: - raise MessagesJSONFileMissingKeyError(missing_key="welcome_messages") - - WELCOME_MESSAGES_KEY_IS_VALID: Final[bool] = bool( - isinstance(messages_dict["welcome_messages"], Iterable) - and messages_dict["welcome_messages"], - ) - if not WELCOME_MESSAGES_KEY_IS_VALID: - raise MessagesJSONFileValueError( - dict_key="welcome_messages", - invalid_value=messages_dict["welcome_messages"], - ) - - cls._settings["WELCOME_MESSAGES"] = set(messages_dict["welcome_messages"]) # type: ignore[call-overload] - - @classmethod - def _setup_roles_messages(cls) -> None: - messages_dict: Mapping[str, object] = cls._get_messages_dict( - os.getenv("MESSAGES_FILE_PATH"), - ) - - if "roles_messages" not in messages_dict: - raise MessagesJSONFileMissingKeyError(missing_key="roles_messages") - - ROLES_MESSAGES_KEY_IS_VALID: Final[bool] = ( - isinstance(messages_dict["roles_messages"], Iterable) - and bool(messages_dict["roles_messages"]) - ) - if not ROLES_MESSAGES_KEY_IS_VALID: - raise MessagesJSONFileValueError( - dict_key="roles_messages", - invalid_value=messages_dict["roles_messages"], - ) - cls._settings["ROLES_MESSAGES"] = set(messages_dict["roles_messages"]) # type: ignore[call-overload] - - @classmethod - def _setup_members_list_url(cls) -> None: - raw_members_list_url: str | None = os.getenv("MEMBERS_LIST_URL") - - MEMBERS_LIST_URL_IS_VALID: Final[bool] = bool( - raw_members_list_url - and validators.url(raw_members_list_url), - ) - if not MEMBERS_LIST_URL_IS_VALID: - INVALID_MEMBERS_LIST_URL_MESSAGE: Final[str] = ( - "MEMBERS_LIST_URL must be a valid URL." - ) - raise ImproperlyConfiguredError(INVALID_MEMBERS_LIST_URL_MESSAGE) - - 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( - "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), - ) - if not MEMBERS_LIST_URL_SESSION_COOKIE_IS_VALID: - INVALID_MEMBERS_LIST_URL_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) - - cls._settings["MEMBERS_LIST_URL_SESSION_COOKIE"] = raw_members_list_url_session_cookie - - @classmethod - def _setup_send_introduction_reminders(cls) -> None: - raw_send_introduction_reminders: str | bool = str( - os.getenv("SEND_INTRODUCTION_REMINDERS", "Once"), - ).lower() + raw_settings_file_path = "TeX-Bot-deployment.yaml" + if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): + raw_settings_file_path = "TeX-Bot-settings.yaml" - if raw_send_introduction_reminders not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: - INVALID_SEND_INTRODUCTION_REMINDERS_MESSAGE: Final[str] = ( - "SEND_INTRODUCTION_REMINDERS must be one of: " - "\"Once\", \"Interval\" or \"False\"." - ) - raise ImproperlyConfiguredError(INVALID_SEND_INTRODUCTION_REMINDERS_MESSAGE) + if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): + raw_settings_file_path = "TeX-Bot-config.yaml" - if raw_send_introduction_reminders in TRUE_VALUES: - raw_send_introduction_reminders = "once" + settings_file_path: Path = Path(raw_settings_file_path) - elif raw_send_introduction_reminders not in ("once", "interval"): - raw_send_introduction_reminders = False - - cls._settings["SEND_INTRODUCTION_REMINDERS"] = raw_send_introduction_reminders - - @classmethod - def _setup_send_introduction_reminders_delay(cls) -> None: - if "SEND_INTRODUCTION_REMINDERS" not in cls._settings: - INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( - "Invalid setup order: SEND_INTRODUCTION_REMINDERS must be set up " - "before SEND_INTRODUCTION_REMINDERS_DELAY can be set up." - ) - raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - - raw_send_introduction_reminders_delay: Match[str] | None = re.match( - 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")), - ) - - raw_timedelta_send_introduction_reminders_delay: timedelta = timedelta() - - if cls._settings["SEND_INTRODUCTION_REMINDERS"]: - if not raw_send_introduction_reminders_delay: - INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_INTRODUCTION_REMINDERS_DELAY must contain the delay " - "in any combination of seconds, minutes, hours, days or weeks." - ) - raise ImproperlyConfiguredError( - INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, - ) - - raw_timedelta_send_introduction_reminders_delay = timedelta( - **{ - key: float(value) - for key, value - in raw_send_introduction_reminders_delay.groupdict().items() - if value - }, - ) - - if raw_timedelta_send_introduction_reminders_delay < timedelta(days=1): - TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_INTRODUCTION_REMINDERS_DELAY must be longer than or equal to 1 day " - "(in any allowed format)." - ) - raise ImproperlyConfiguredError( - TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, - ) - - cls._settings["SEND_INTRODUCTION_REMINDERS_DELAY"] = ( - raw_timedelta_send_introduction_reminders_delay - ) + if not settings_file_path.is_file(): + raise FileNotFoundError(settings_file_not_found_message) - @classmethod - def _setup_send_introduction_reminders_interval(cls) -> None: - if "SEND_INTRODUCTION_REMINDERS" not in cls._settings: - INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( - "Invalid setup order: SEND_INTRODUCTION_REMINDERS must be set up " - "before SEND_INTRODUCTION_REMINDERS_INTERVAL can be set up." - ) - raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - - raw_send_introduction_reminders_interval: Match[str] | None = re.match( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - str(os.getenv("SEND_INTRODUCTION_REMINDERS_INTERVAL", "6h")), - ) - - raw_timedelta_details_send_introduction_reminders_interval: Mapping[str, float] = { - "hours": 6, - } - - if cls._settings["SEND_INTRODUCTION_REMINDERS"]: - if not raw_send_introduction_reminders_interval: - INVALID_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE: Final[str] = ( - "SEND_INTRODUCTION_REMINDERS_INTERVAL must contain the interval " - "in any combination of seconds, minutes or hours." - ) - raise ImproperlyConfiguredError( - INVALID_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE, - ) - - raw_timedelta_details_send_introduction_reminders_interval = { - key: float(value) - for key, value - in raw_send_introduction_reminders_interval.groupdict().items() - if value - } - - cls._settings["SEND_INTRODUCTION_REMINDERS_INTERVAL"] = ( - raw_timedelta_details_send_introduction_reminders_interval - ) - - @classmethod - def _setup_send_get_roles_reminders(cls) -> None: - raw_send_get_roles_reminders: str = str( - os.getenv("SEND_GET_ROLES_REMINDERS", "True"), - ).lower() - - if raw_send_get_roles_reminders not in TRUE_VALUES | FALSE_VALUES: - INVALID_SEND_GET_ROLES_REMINDERS_MESSAGE: Final[str] = ( - "SEND_GET_ROLES_REMINDERS must be a boolean value." - ) - raise ImproperlyConfiguredError(INVALID_SEND_GET_ROLES_REMINDERS_MESSAGE) - - cls._settings["SEND_GET_ROLES_REMINDERS"] = ( - raw_send_get_roles_reminders in TRUE_VALUES - ) - - @classmethod - def _setup_send_get_roles_reminders_delay(cls) -> None: - if "SEND_GET_ROLES_REMINDERS" not in cls._settings: - INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( - "Invalid setup order: SEND_GET_ROLES_REMINDERS must be set up " - "before SEND_GET_ROLES_REMINDERS_DELAY can be set up." - ) - raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - - raw_send_get_roles_reminders_delay: Match[str] | None = re.match( - 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")), - ) - - raw_timedelta_send_get_roles_reminders_delay: timedelta = timedelta() - - if cls._settings["SEND_GET_ROLES_REMINDERS"]: - if not raw_send_get_roles_reminders_delay: - INVALID_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_GET_ROLES_REMINDERS_DELAY must contain the delay " - "in any combination of seconds, minutes, hours, days or weeks." - ) - raise ImproperlyConfiguredError( - INVALID_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE, - ) - - raw_timedelta_send_get_roles_reminders_delay = timedelta( - **{ - key: float(value) - for key, value - in raw_send_get_roles_reminders_delay.groupdict().items() - if value - }, - ) - - if raw_timedelta_send_get_roles_reminders_delay < timedelta(days=1): - TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_SEND_GET_ROLES_REMINDERS_DELAY " - "must be longer than or equal to 1 day (in any allowed format)." - ) - raise ImproperlyConfiguredError( - TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE, - ) - - cls._settings["SEND_GET_ROLES_REMINDERS_DELAY"] = ( - raw_timedelta_send_get_roles_reminders_delay - ) - - @classmethod - def _setup_advanced_send_get_roles_reminders_interval(cls) -> None: - if "SEND_GET_ROLES_REMINDERS" not in cls._settings: - INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( - "Invalid setup order: SEND_GET_ROLES_REMINDERS must be set up " - "before ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL can be set up." - ) - raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - - raw_advanced_send_get_roles_reminders_interval: Match[str] | None = re.match( - 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")), - ) - - raw_timedelta_details_advanced_send_get_roles_reminders_interval: Mapping[str, float] = { # noqa: E501 - "hours": 24, - } - - if cls._settings["SEND_GET_ROLES_REMINDERS"]: - if not raw_advanced_send_get_roles_reminders_interval: - INVALID_ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL_MESSAGE: Final[str] = ( - "ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL must contain the interval " - "in any combination of seconds, minutes or hours." - ) - raise ImproperlyConfiguredError( - INVALID_ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL_MESSAGE, - ) - - raw_timedelta_details_advanced_send_get_roles_reminders_interval = { - key: float(value) - for key, value - in raw_advanced_send_get_roles_reminders_interval.groupdict().items() - if value - } - - cls._settings["ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL"] = ( - raw_timedelta_details_advanced_send_get_roles_reminders_interval - ) - - @classmethod - def _setup_statistics_days(cls) -> None: - e: ValueError - try: - raw_statistics_days: float = float(os.getenv("STATISTICS_DAYS", "30")) - except ValueError as e: - INVALID_STATISTICS_DAYS_MESSAGE: Final[str] = ( - "STATISTICS_DAYS must contain the statistics period in days." - ) - raise ImproperlyConfiguredError(INVALID_STATISTICS_DAYS_MESSAGE) from e - - cls._settings["STATISTICS_DAYS"] = timedelta(days=raw_statistics_days) - - @classmethod - def _setup_statistics_roles(cls) -> None: - raw_statistics_roles: str | None = os.getenv("STATISTICS_ROLES") - - if not raw_statistics_roles: - cls._settings["STATISTICS_ROLES"] = DEFAULT_STATISTICS_ROLES - - else: - cls._settings["STATISTICS_ROLES"] = { - raw_statistics_role - for raw_statistics_role - in raw_statistics_roles.split(",") - if raw_statistics_role - } - - @classmethod - def _setup_moderation_document_url(cls) -> None: - raw_moderation_document_url: str | None = os.getenv("MODERATION_DOCUMENT_URL") - - MODERATION_DOCUMENT_URL_IS_VALID: Final[bool] = bool( - raw_moderation_document_url - and validators.url(raw_moderation_document_url), - ) - if not MODERATION_DOCUMENT_URL_IS_VALID: - MODERATION_DOCUMENT_URL_MESSAGE: Final[str] = ( - "MODERATION_DOCUMENT_URL must be a valid URL." - ) - raise ImproperlyConfiguredError(MODERATION_DOCUMENT_URL_MESSAGE) - - 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( - "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", - "DM", - ) - if not raw_manual_moderation_warning_message_location: - MANUAL_MODERATION_WARNING_MESSAGE_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) - - cls._settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"] = ( - raw_manual_moderation_warning_message_location - ) - - @classmethod - def _setup_env_variables(cls) -> None: - """ - Load environment values into the settings dictionary. - - Environment values are loaded from the .env file/the current environment variables and - are only stored after the input values have been validated. - """ - if cls._is_env_variables_setup: - logging.warning("Environment variables have already been set up.") - return - - dotenv.load_dotenv() - - cls._setup_logging() - cls._setup_discord_bot_token() - cls._setup_discord_log_channel_webhook_url() - cls._setup_discord_guild_id() - cls._setup_group_full_name() - cls._setup_group_short_name() - cls._setup_ping_command_easter_egg_probability() - cls._setup_welcome_messages() - cls._setup_roles_messages() - cls._setup_members_list_url() - cls._setup_members_list_url_session_cookie() - cls._setup_membership_perks_url() - cls._setup_purchase_membership_url() - cls._setup_send_introduction_reminders() - cls._setup_send_introduction_reminders_delay() - cls._setup_send_introduction_reminders_interval() - cls._setup_send_get_roles_reminders() - cls._setup_send_get_roles_reminders_delay() - cls._setup_advanced_send_get_roles_reminders_interval() - cls._setup_statistics_days() - cls._setup_statistics_roles() - cls._setup_moderation_document_url() - cls._setup_manual_moderation_warning_message_location() - - cls._is_env_variables_setup = True - - -def _settings_class_factory() -> type[Settings]: - @final - class RuntimeSettings(Settings): - """ - Settings class that provides access to all settings values. - - Settings values can be accessed via key (like a dictionary) or via class attribute. - """ - - _is_env_variables_setup: ClassVar[bool] = False - _settings: ClassVar[dict[str, object]] = {} - - return RuntimeSettings - - -settings: Final[Settings] = _settings_class_factory()() + return settings_file_path def run_setup() -> None: """Execute the setup functions required, before other modules can be run.""" - # noinspection PyProtectedMember - settings._setup_env_variables() # noqa: SLF001 + check_for_deprecated_environment_variables() + + settings.reload(_get_settings_file_path()) logger.debug("Begin database setup") @@ -755,3 +93,731 @@ def run_setup() -> None: management.call_command("migrate") logger.debug("Database setup completed") + + +def check_for_deprecated_environment_variables() -> None: + CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR: Final[DeprecationWarning] = ( + DeprecationWarning( + ( + "Configuration using environment variables is deprecated. " + "Use a `TeX-Bot-deployment.yaml` file instead." + ), + ) + ) + + if (PROJECT_ROOT / ".env").exists(): + raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR + + DEPRECATED_ENVIRONMENT_VARIABLE_NAMES: Final[Iterable[str]] = ( + "DISCORD_BOT_TOKEN", + "DISCORD_GUILD_ID", + "DISCORD_LOG_CHANNEL_WEBHOOK_URL", + "GROUP_NAME", + "GROUP_SHORT_NAME", + "PURCHASE_MEMBERSHIP_URL", + "CONSOLE_LOG_LEVEL", + "MEMBERS_LIST_URL", + "MEMBERS_LIST_URL_SESSION_COOKIE", + "PING_COMMAND_EASTER_EGG_PROBABILITY", + "MESSAGES_FILE_PATH", + "SEND_INTRODUCTION_REMINDERS", + "SEND_INTRODUCTION_REMINDERS_DELAY", + "SEND_INTRODUCTION_REMINDERS_INTERVAL", + "SEND_GET_ROLES_REMINDERS", + "SEND_GET_ROLES_REMINDERS_DELAY", + "SEND_GET_ROLES_REMINDERS_INTERVAL", + "ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL", + "STATISTICS_DAYS", + "STATISTICS_ROLES", + "MODERATION_DOCUMENT_URL", + "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", + ) + deprecated_environment_variable_name: str + for deprecated_environment_variable_name in DEPRECATED_ENVIRONMENT_VARIABLE_NAMES: + deprecated_environment_variable_found: bool = bool( + ( + deprecated_environment_variable_name.upper() in os.environ + or deprecated_environment_variable_name.lower() in os.environ + or f"TEX_BOT_{deprecated_environment_variable_name}".upper() in os.environ + or f"TEX_BOT_{deprecated_environment_variable_name}".lower() in os.environ + ), + ) + if deprecated_environment_variable_found: + raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR + + +# VALID_SEND_INTRODUCTION_REMINDERS_VALUES: Final[frozenset[str]] = frozenset( +# {"once"} | TRUE_VALUES | FALSE_VALUES, +# ) +# DEFAULT_STATISTICS_ROLES: Final[frozenset[str]] = frozenset( +# { +# "Committee", +# "Committee-Elect", +# "Student Rep", +# "Member", +# "Guest", +# "Server Booster", +# "Foundation Year", +# "First Year", +# "Second Year", +# "Final Year", +# "Year In Industry", +# "Year Abroad", +# "PGT", +# "PGR", +# "Alumnus/Alumna", +# "Postdoc", +# "Quiz Victor", +# }, +# ) +# LOG_LEVEL_CHOICES: Final[Sequence[str]] = ( +# "DEBUG", +# "INFO", +# "WARNING", +# "ERROR", +# "CRITICAL", +# ) +# +# +# class Settings(abc.ABC): +# """ +# Settings class that provides access to all settings values. +# +# Settings values can be accessed via key (like a dictionary) or via class attribute. +# """ +# +# _is_env_variables_setup: ClassVar[bool] +# _settings: ClassVar[dict[str, object]] +# +# @classmethod +# def get_invalid_settings_key_message(cls, item: str) -> str: +# """Return the message to state that the given settings key is invalid.""" +# return f"{item!r} is not a valid settings key." +# +# def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 +# """Retrieve settings value by attribute lookup.""" +# MISSING_ATTRIBUTE_MESSAGE: Final[str] = ( +# f"{type(self).__name__!r} object has no attribute {item!r}" +# ) +# +# if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values +# raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) +# +# if not self._is_env_variables_setup: +# self._setup_env_variables() +# +# if item in self._settings: +# return self._settings[item] +# +# if re.match(r"\A[A-Z](?:[A-Z_]*[A-Z])?\Z", item): +# INVALID_SETTINGS_KEY_MESSAGE: Final[str] = self.get_invalid_settings_key_message( +# item, +# ) +# raise AttributeError(INVALID_SETTINGS_KEY_MESSAGE) +# +# raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) +# +# def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 +# """Retrieve settings value by key lookup.""" +# attribute_not_exist_error: AttributeError +# try: +# return getattr(self, item) +# except AttributeError as attribute_not_exist_error: +# key_error_message: str = item +# +# if self.get_invalid_settings_key_message(item) in str(attribute_not_exist_error): +# key_error_message = str(attribute_not_exist_error) +# +# raise KeyError(key_error_message) from None +# +# @staticmethod +# def _setup_logging() -> None: +# raw_console_log_level: str = str(os.getenv("CONSOLE_LOG_LEVEL", "INFO")).upper() +# +# if raw_console_log_level not in LOG_LEVEL_CHOICES: +# INVALID_LOG_LEVEL_MESSAGE: Final[str] = f"""LOG_LEVEL must be one of { +# ",".join(f"{log_level_choice!r}" +# for log_level_choice +# in LOG_LEVEL_CHOICES[:-1]) +# } or {LOG_LEVEL_CHOICES[-1]!r}.""" +# raise ImproperlyConfiguredError(INVALID_LOG_LEVEL_MESSAGE) +# +# logger.setLevel(getattr(logging, raw_console_log_level)) +# +# console_logging_handler: logging.Handler = logging.StreamHandler() +# # noinspection SpellCheckingInspection +# console_logging_handler.setFormatter( +# logging.Formatter("{asctime} | {name} | {levelname:^8} - {message}", style="{"), +# ) +# +# logger.addHandler(console_logging_handler) +# logger.propagate = False +# +# @classmethod +# def _setup_discord_bot_token(cls) -> None: +# raw_discord_bot_token: str | None = os.getenv("DISCORD_BOT_TOKEN") +# +# DISCORD_BOT_TOKEN_IS_VALID: Final[bool] = bool( +# raw_discord_bot_token +# and re.match( +# r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", +# raw_discord_bot_token, +# ), +# ) +# if not DISCORD_BOT_TOKEN_IS_VALID: +# INVALID_DISCORD_BOT_TOKEN_MESSAGE: Final[str] = ( +# "DISCORD_BOT_TOKEN must be a valid Discord bot token " +# "(see https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts)." +# ) +# raise ImproperlyConfiguredError(INVALID_DISCORD_BOT_TOKEN_MESSAGE) +# +# cls._settings["DISCORD_BOT_TOKEN"] = raw_discord_bot_token +# +# @classmethod +# def _setup_discord_log_channel_webhook_url(cls) -> None: +# raw_discord_log_channel_webhook_url: str = os.getenv( +# "DISCORD_LOG_CHANNEL_WEBHOOK_URL", +# "", +# ) +# +# DISCORD_LOG_CHANNEL_WEBHOOK_URL_IS_VALID: Final[bool] = bool( +# not raw_discord_log_channel_webhook_url +# or ( +# validators.url(raw_discord_log_channel_webhook_url) +# and raw_discord_log_channel_webhook_url.startswith( +# "https://discord.com/api/webhooks/", +# ) +# ), +# ) +# if not DISCORD_LOG_CHANNEL_WEBHOOK_URL_IS_VALID: +# INVALID_DISCORD_LOG_CHANNEL_WEBHOOK_URL_MESSAGE: Final[str] = ( +# "DISCORD_LOG_CHANNEL_WEBHOOK_URL must be a valid webhook URL " +# "that points to a discord channel where logs should be displayed." +# ) +# raise ImproperlyConfiguredError(INVALID_DISCORD_LOG_CHANNEL_WEBHOOK_URL_MESSAGE) +# +# cls._settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"] = raw_discord_log_channel_webhook_url +# +# @classmethod +# def _setup_discord_guild_id(cls) -> None: +# raw_discord_guild_id: str | None = os.getenv("DISCORD_GUILD_ID") +# +# 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), +# ) +# if not DISCORD_GUILD_ID_IS_VALID: +# INVALID_DISCORD_GUILD_ID_MESSAGE: Final[str] = ( +# "DISCORD_GUILD_ID must be a valid Discord guild ID " +# "(see https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id)." +# ) +# raise ImproperlyConfiguredError(INVALID_DISCORD_GUILD_ID_MESSAGE) +# +# cls._settings["DISCORD_GUILD_ID"] = int(raw_discord_guild_id) # type: ignore[arg-type] +# +# @classmethod +# def _setup_group_full_name(cls) -> None: +# raw_group_full_name: str | None = os.getenv("GROUP_NAME") +# +# 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), +# ) +# if not GROUP_FULL_NAME_IS_VALID: +# INVALID_GROUP_FULL_NAME: Final[str] = ( +# "GROUP_NAME must not contain any invalid characters." +# ) +# raise ImproperlyConfiguredError(INVALID_GROUP_FULL_NAME) +# cls._settings["_GROUP_FULL_NAME"] = raw_group_full_name +# +# @classmethod +# def _setup_group_short_name(cls) -> None: +# raw_group_short_name: str | None = os.getenv("GROUP_SHORT_NAME") +# +# 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), +# ) +# if not GROUP_SHORT_NAME_IS_VALID: +# INVALID_GROUP_SHORT_NAME: Final[str] = ( +# "GROUP_SHORT_NAME must not contain any invalid characters." +# ) +# raise ImproperlyConfiguredError(INVALID_GROUP_SHORT_NAME) +# cls._settings["_GROUP_SHORT_NAME"] = raw_group_short_name +# +# @classmethod +# def _setup_purchase_membership_url(cls) -> None: +# raw_purchase_membership_url: str | None = os.getenv("PURCHASE_MEMBERSHIP_URL") +# +# PURCHASE_MEMBERSHIP_URL_IS_VALID: Final[bool] = bool( +# not raw_purchase_membership_url +# or validators.url(raw_purchase_membership_url), +# ) +# if not PURCHASE_MEMBERSHIP_URL_IS_VALID: +# INVALID_PURCHASE_MEMBERSHIP_URL_MESSAGE: Final[str] = ( +# "PURCHASE_MEMBERSHIP_URL must be a valid URL." +# ) +# raise ImproperlyConfiguredError(INVALID_PURCHASE_MEMBERSHIP_URL_MESSAGE) +# +# cls._settings["PURCHASE_MEMBERSHIP_URL"] = raw_purchase_membership_url +# +# @classmethod +# def _setup_membership_perks_url(cls) -> None: +# raw_membership_perks_url: str | None = os.getenv("MEMBERSHIP_PERKS_URL") +# +# MEMBERSHIP_PERKS_URL_IS_VALID: Final[bool] = bool( +# not raw_membership_perks_url +# or validators.url(raw_membership_perks_url), +# ) +# if not MEMBERSHIP_PERKS_URL_IS_VALID: +# INVALID_MEMBERSHIP_PERKS_URL_MESSAGE: Final[str] = ( +# "MEMBERSHIP_PERKS_URL must be a valid URL." +# ) +# raise ImproperlyConfiguredError(INVALID_MEMBERSHIP_PERKS_URL_MESSAGE) +# +# cls._settings["MEMBERSHIP_PERKS_URL"] = raw_membership_perks_url +# +# @classmethod +# def _setup_ping_command_easter_egg_probability(cls) -> None: +# INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE: Final[str] = ( +# "PING_COMMAND_EASTER_EGG_PROBABILITY must be a float between & including 1 & 0." +# ) +# +# e: ValueError +# try: +# raw_ping_command_easter_egg_probability: float = 100 * float( +# os.getenv("PING_COMMAND_EASTER_EGG_PROBABILITY", "0.01"), +# ) +# except ValueError as e: +# raise ( +# ImproperlyConfiguredError(INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE) +# ) from e +# +# if not 0 <= raw_ping_command_easter_egg_probability <= 100: +# raise ImproperlyConfiguredError( +# INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE, +# ) +# +# cls._settings["PING_COMMAND_EASTER_EGG_PROBABILITY"] = ( +# raw_ping_command_easter_egg_probability +# ) +# +# @classmethod +# @functools.lru_cache(maxsize=5) +# def _get_messages_dict(cls, raw_messages_file_path: str | None) -> Mapping[str, object]: +# JSON_DECODING_ERROR_MESSAGE: Final[str] = ( +# "Messages JSON file must contain a JSON string that can be decoded " +# "into a Python dict object." +# ) +# +# messages_file_path: Path = ( +# Path(raw_messages_file_path) +# if raw_messages_file_path +# else PROJECT_ROOT / Path("messages.json") +# ) +# +# if not messages_file_path.is_file(): +# MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE: Final[str] = ( +# "MESSAGES_FILE_PATH must be a path to a file that exists." +# ) +# raise ImproperlyConfiguredError(MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE) +# +# messages_file: IO[str] +# with messages_file_path.open(encoding="utf8") as messages_file: +# e: json.JSONDecodeError +# try: +# messages_dict: object = json.load(messages_file) +# except json.JSONDecodeError as e: +# raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) from e +# +# if not isinstance(messages_dict, Mapping): +# raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) +# +# return messages_dict +# +# @classmethod +# def _setup_welcome_messages(cls) -> None: +# messages_dict: Mapping[str, object] = cls._get_messages_dict( +# os.getenv("MESSAGES_FILE_PATH"), +# ) +# +# if "welcome_messages" not in messages_dict: +# raise MessagesJSONFileMissingKeyError(missing_key="welcome_messages") +# +# WELCOME_MESSAGES_KEY_IS_VALID: Final[bool] = bool( +# isinstance(messages_dict["welcome_messages"], Iterable) +# and messages_dict["welcome_messages"], +# ) +# if not WELCOME_MESSAGES_KEY_IS_VALID: +# raise MessagesJSONFileValueError( +# dict_key="welcome_messages", +# invalid_value=messages_dict["welcome_messages"], +# ) +# +# cls._settings["WELCOME_MESSAGES"] = set(messages_dict["welcome_messages"]) # type: ignore[call-overload] +# +# @classmethod +# def _setup_roles_messages(cls) -> None: +# messages_dict: Mapping[str, object] = cls._get_messages_dict( +# os.getenv("MESSAGES_FILE_PATH"), +# ) +# +# if "roles_messages" not in messages_dict: +# raise MessagesJSONFileMissingKeyError(missing_key="roles_messages") +# +# ROLES_MESSAGES_KEY_IS_VALID: Final[bool] = ( +# isinstance(messages_dict["roles_messages"], Iterable) +# and bool(messages_dict["roles_messages"]) +# ) +# if not ROLES_MESSAGES_KEY_IS_VALID: +# raise MessagesJSONFileValueError( +# dict_key="roles_messages", +# invalid_value=messages_dict["roles_messages"], +# ) +# cls._settings["ROLES_MESSAGES"] = set(messages_dict["roles_messages"]) # type: ignore[call-overload] +# +# @classmethod +# def _setup_members_list_url(cls) -> None: +# raw_members_list_url: str | None = os.getenv("MEMBERS_LIST_URL") +# +# MEMBERS_LIST_URL_IS_VALID: Final[bool] = bool( +# raw_members_list_url +# and validators.url(raw_members_list_url), +# ) +# if not MEMBERS_LIST_URL_IS_VALID: +# INVALID_MEMBERS_LIST_URL_MESSAGE: Final[str] = ( +# "MEMBERS_LIST_URL must be a valid URL." +# ) +# raise ImproperlyConfiguredError(INVALID_MEMBERS_LIST_URL_MESSAGE) +# +# 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( +# "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), +# ) +# if not MEMBERS_LIST_URL_SESSION_COOKIE_IS_VALID: +# INVALID_MEMBERS_LIST_URL_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) +# +# cls._settings["MEMBERS_LIST_URL_SESSION_COOKIE"] = raw_members_list_url_session_cookie +# +# @classmethod +# def _setup_send_introduction_reminders(cls) -> None: +# raw_send_introduction_reminders: str | bool = str( +# os.getenv("SEND_INTRODUCTION_REMINDERS", "Once"), +# ).lower() +# +# if raw_send_introduction_reminders not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: +# INVALID_SEND_INTRODUCTION_REMINDERS_MESSAGE: Final[str] = ( +# "SEND_INTRODUCTION_REMINDERS must be one of: " +# "\"Once\", \"Interval\" or \"False\"." +# ) +# raise ImproperlyConfiguredError(INVALID_SEND_INTRODUCTION_REMINDERS_MESSAGE) +# +# if raw_send_introduction_reminders in TRUE_VALUES: +# raw_send_introduction_reminders = "once" +# +# elif raw_send_introduction_reminders not in ("once", "interval"): +# raw_send_introduction_reminders = False +# +# cls._settings["SEND_INTRODUCTION_REMINDERS"] = raw_send_introduction_reminders +# +# @classmethod +# def _setup_send_introduction_reminders_delay(cls) -> None: +# if "SEND_INTRODUCTION_REMINDERS" not in cls._settings: +# INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( +# "Invalid setup order: SEND_INTRODUCTION_REMINDERS must be set up " +# "before SEND_INTRODUCTION_REMINDERS_DELAY can be set up." +# ) +# raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) +# +# raw_send_introduction_reminders_delay: Match[str] | None = re.match( +# 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")), +# ) +# +# raw_timedelta_send_introduction_reminders_delay: timedelta = timedelta() +# +# if cls._settings["SEND_INTRODUCTION_REMINDERS"]: +# if not raw_send_introduction_reminders_delay: +# INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( +# "SEND_INTRODUCTION_REMINDERS_DELAY must contain the delay " +# "in any combination of seconds, minutes, hours, days or weeks." +# ) +# raise ImproperlyConfiguredError( +# INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, +# ) +# +# raw_timedelta_send_introduction_reminders_delay = timedelta( +# **{ +# key: float(value) +# for key, value +# in raw_send_introduction_reminders_delay.groupdict().items() +# if value +# }, +# ) +# +# if raw_timedelta_send_introduction_reminders_delay < timedelta(days=1): +# TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( +# "SEND_INTRODUCTION_REMINDERS_DELAY must be longer than or equal to 1 day " +# "(in any allowed format)." +# ) +# raise ImproperlyConfiguredError( +# TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, +# ) +# +# cls._settings["SEND_INTRODUCTION_REMINDERS_DELAY"] = ( +# raw_timedelta_send_introduction_reminders_delay +# ) +# +# @classmethod +# def _setup_send_introduction_reminders_interval(cls) -> None: +# if "SEND_INTRODUCTION_REMINDERS" not in cls._settings: +# INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( +# "Invalid setup order: SEND_INTRODUCTION_REMINDERS must be set up " +# "before SEND_INTRODUCTION_REMINDERS_INTERVAL can be set up." +# ) +# raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) +# +# raw_send_introduction_reminders_interval: Match[str] | None = re.match( +# r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", +# str(os.getenv("SEND_INTRODUCTION_REMINDERS_INTERVAL", "6h")), +# ) +# +# raw_timedelta_details_send_introduction_reminders_interval: Mapping[str, float] = { +# "hours": 6, +# } +# +# if cls._settings["SEND_INTRODUCTION_REMINDERS"]: +# if not raw_send_introduction_reminders_interval: +# INVALID_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE: Final[str] = ( +# "SEND_INTRODUCTION_REMINDERS_INTERVAL must contain the interval " +# "in any combination of seconds, minutes or hours." +# ) +# raise ImproperlyConfiguredError( +# INVALID_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE, +# ) +# +# raw_timedelta_details_send_introduction_reminders_interval = { +# key: float(value) +# for key, value +# in raw_send_introduction_reminders_interval.groupdict().items() +# if value +# } +# +# cls._settings["SEND_INTRODUCTION_REMINDERS_INTERVAL"] = ( +# raw_timedelta_details_send_introduction_reminders_interval +# ) +# +# @classmethod +# def _setup_send_get_roles_reminders(cls) -> None: +# raw_send_get_roles_reminders: str = str( +# os.getenv("SEND_GET_ROLES_REMINDERS", "True"), +# ).lower() +# +# if raw_send_get_roles_reminders not in TRUE_VALUES | FALSE_VALUES: +# INVALID_SEND_GET_ROLES_REMINDERS_MESSAGE: Final[str] = ( +# "SEND_GET_ROLES_REMINDERS must be a boolean value." +# ) +# raise ImproperlyConfiguredError(INVALID_SEND_GET_ROLES_REMINDERS_MESSAGE) +# +# cls._settings["SEND_GET_ROLES_REMINDERS"] = ( +# raw_send_get_roles_reminders in TRUE_VALUES +# ) +# +# @classmethod +# def _setup_send_get_roles_reminders_delay(cls) -> None: +# if "SEND_GET_ROLES_REMINDERS" not in cls._settings: +# INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( +# "Invalid setup order: SEND_GET_ROLES_REMINDERS must be set up " +# "before SEND_GET_ROLES_REMINDERS_DELAY can be set up." +# ) +# raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) +# +# raw_send_get_roles_reminders_delay: Match[str] | None = re.match( +# 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")), +# ) +# +# raw_timedelta_send_get_roles_reminders_delay: timedelta = timedelta() +# +# if cls._settings["SEND_GET_ROLES_REMINDERS"]: +# if not raw_send_get_roles_reminders_delay: +# INVALID_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE: Final[str] = ( +# "SEND_GET_ROLES_REMINDERS_DELAY must contain the delay " +# "in any combination of seconds, minutes, hours, days or weeks." +# ) +# raise ImproperlyConfiguredError( +# INVALID_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE, +# ) +# +# raw_timedelta_send_get_roles_reminders_delay = timedelta( +# **{ +# key: float(value) +# for key, value +# in raw_send_get_roles_reminders_delay.groupdict().items() +# if value +# }, +# ) +# +# if raw_timedelta_send_get_roles_reminders_delay < timedelta(days=1): +# TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE: Final[str] = ( +# "SEND_SEND_GET_ROLES_REMINDERS_DELAY " +# "must be longer than or equal to 1 day (in any allowed format)." +# ) +# raise ImproperlyConfiguredError( +# TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE, +# ) +# +# cls._settings["SEND_GET_ROLES_REMINDERS_DELAY"] = ( +# raw_timedelta_send_get_roles_reminders_delay +# ) +# +# @classmethod +# def _setup_advanced_send_get_roles_reminders_interval(cls) -> None: +# if "SEND_GET_ROLES_REMINDERS" not in cls._settings: +# INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( +# "Invalid setup order: SEND_GET_ROLES_REMINDERS must be set up " +# "before ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL can be set up." +# ) +# raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) +# +# raw_advanced_send_get_roles_reminders_interval: Match[str] | None = re.match( +# 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")), +# ) +# +# raw_timedelta_details_advanced_send_get_roles_reminders_interval: Mapping[str, float] = { # noqa: E501 +# "hours": 24, +# } +# +# if cls._settings["SEND_GET_ROLES_REMINDERS"]: +# if not raw_advanced_send_get_roles_reminders_interval: +# INVALID_ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL_MESSAGE: Final[str] = ( +# "ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL must contain the interval " +# "in any combination of seconds, minutes or hours." +# ) +# raise ImproperlyConfiguredError( +# INVALID_ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL_MESSAGE, +# ) +# +# raw_timedelta_details_advanced_send_get_roles_reminders_interval = { +# key: float(value) +# for key, value +# in raw_advanced_send_get_roles_reminders_interval.groupdict().items() +# if value +# } +# +# cls._settings["ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL"] = ( +# raw_timedelta_details_advanced_send_get_roles_reminders_interval +# ) +# +# @classmethod +# def _setup_statistics_days(cls) -> None: +# e: ValueError +# try: +# raw_statistics_days: float = float(os.getenv("STATISTICS_DAYS", "30")) +# except ValueError as e: +# INVALID_STATISTICS_DAYS_MESSAGE: Final[str] = ( +# "STATISTICS_DAYS must contain the statistics period in days." +# ) +# raise ImproperlyConfiguredError(INVALID_STATISTICS_DAYS_MESSAGE) from e +# +# cls._settings["STATISTICS_DAYS"] = timedelta(days=raw_statistics_days) +# +# @classmethod +# def _setup_statistics_roles(cls) -> None: +# raw_statistics_roles: str | None = os.getenv("STATISTICS_ROLES") +# +# if not raw_statistics_roles: +# cls._settings["STATISTICS_ROLES"] = DEFAULT_STATISTICS_ROLES +# +# else: +# cls._settings["STATISTICS_ROLES"] = { +# raw_statistics_role +# for raw_statistics_role +# in raw_statistics_roles.split(",") +# if raw_statistics_role +# } +# +# @classmethod +# def _setup_moderation_document_url(cls) -> None: +# raw_moderation_document_url: str | None = os.getenv("MODERATION_DOCUMENT_URL") +# +# MODERATION_DOCUMENT_URL_IS_VALID: Final[bool] = bool( +# raw_moderation_document_url +# and validators.url(raw_moderation_document_url), +# ) +# if not MODERATION_DOCUMENT_URL_IS_VALID: +# MODERATION_DOCUMENT_URL_MESSAGE: Final[str] = ( +# "MODERATION_DOCUMENT_URL must be a valid URL." +# ) +# raise ImproperlyConfiguredError(MODERATION_DOCUMENT_URL_MESSAGE) +# +# 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( +# "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", +# "DM", +# ) +# if not raw_manual_moderation_warning_message_location: +# MANUAL_MODERATION_WARNING_MESSAGE_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) +# +# cls._settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"] = ( +# raw_manual_moderation_warning_message_location +# ) +# +# @classmethod +# def _setup_env_variables(cls) -> None: +# """ +# Load environment values into the settings dictionary. +# +# Environment values are loaded from the .env file/the current environment variables and +# are only stored after the input values have been validated. +# """ +# if cls._is_env_variables_setup: +# logging.warning("Environment variables have already been set up.") +# return +# +# dotenv.load_dotenv() +# +# cls._setup_logging() +# cls._setup_discord_bot_token() +# cls._setup_discord_log_channel_webhook_url() +# cls._setup_discord_guild_id() +# cls._setup_group_full_name() +# cls._setup_group_short_name() +# cls._setup_ping_command_easter_egg_probability() +# cls._setup_welcome_messages() +# cls._setup_roles_messages() +# cls._setup_members_list_url() +# cls._setup_members_list_url_session_cookie() +# cls._setup_membership_perks_url() +# cls._setup_purchase_membership_url() +# cls._setup_send_introduction_reminders() +# cls._setup_send_introduction_reminders_delay() +# cls._setup_send_introduction_reminders_interval() +# cls._setup_send_get_roles_reminders() +# cls._setup_send_get_roles_reminders_delay() +# cls._setup_advanced_send_get_roles_reminders_interval() +# cls._setup_statistics_days() +# cls._setup_statistics_roles() +# cls._setup_moderation_document_url() +# cls._setup_manual_moderation_warning_message_location() +# +# cls._is_env_variables_setup = True diff --git a/config/_settings.py b/config/_settings.py new file mode 100644 index 000000000..ba2f89fc0 --- /dev/null +++ b/config/_settings.py @@ -0,0 +1,89 @@ +""" +Contains settings values and setup functions. + +Settings values are imported from the tex-bot-deployment.yaml file. +These values are used to configure the functionality of the bot at run-time. +""" + +from collections.abc import Sequence + +__all__: Sequence[str] = ("SettingsAccessor",) + + +import logging +import re +from logging import Logger +from pathlib import Path +from typing import Any, ClassVar, Final + +import strictyaml +from strictyaml import YAML + +from ._yaml import SETTINGS_YAML_SCHEMA + +logger: Final[Logger] = logging.getLogger("TeX-Bot") + + +class SettingsAccessor: + """ + Settings class that provides access to all settings values. + + Settings values can be accessed via key (like a dictionary) or via class attribute. + """ + + _settings: ClassVar[dict[str, object]] = {} + _most_recent_yaml: ClassVar[YAML | None] = None # type: ignore[no-any-unimported] + + @classmethod + def format_invalid_settings_key_message(cls, item: str) -> str: + """Return the message to state that the given settings key is invalid.""" + return f"{item!r} is not a valid settings key." + + def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 + """Retrieve settings value by attribute lookup.""" + MISSING_ATTRIBUTE_MESSAGE: Final[str] = ( + f"{type(self).__name__!r} object has no attribute {item!r}" + ) + + if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values + raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) + + if not self._is_env_variables_setup: + self._setup_env_variables() + + if item in self._settings: + return self._settings[item] + + if re.match(r"\A[A-Z](?:[A-Z_]*[A-Z])?\Z", item): + INVALID_SETTINGS_KEY_MESSAGE: Final[str] = ( + self.format_invalid_settings_key_message(item) + ) + raise AttributeError(INVALID_SETTINGS_KEY_MESSAGE) + + raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) + + def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 + """Retrieve settings value by key lookup.""" + attribute_not_exist_error: AttributeError + try: + return getattr(self, item) + except AttributeError as attribute_not_exist_error: + key_error_message: str = item + + ERROR_WAS_FROM_INVALID_KEY_NAME: Final[bool] = ( + self.format_invalid_settings_key_message(item) in str( + attribute_not_exist_error, + ) + ) + if ERROR_WAS_FROM_INVALID_KEY_NAME: + key_error_message = str(attribute_not_exist_error) + + raise KeyError(key_error_message) from None + + @classmethod + def reload(cls, settings_file_path: Path) -> None: + current_yaml: YAML = strictyaml.load( # type: ignore[no-any-unimported] + settings_file_path.read_text(), + SETTINGS_YAML_SCHEMA, + ) + # TODO: Revalidate session cookie based upon URL diff --git a/config/_yaml.py b/config/_yaml.py new file mode 100644 index 000000000..be0ec7a4e --- /dev/null +++ b/config/_yaml.py @@ -0,0 +1,289 @@ +from collections.abc import Sequence + +__all__: Sequence[str] = ( + "SlugValidator", + "DiscordWebhookURLValidator", + "SETTINGS_YAML_SCHEMA", +) + +import math +import re +from typing import Final, Literal, TypeAlias, override + +import slugify +import strictyaml +from strictyaml import constants as strictyaml_constants +from strictyaml import utils as strictyaml_utils +from strictyaml.exceptions import YAMLSerializationError +from strictyaml.yamllocation import YAMLChunk + +from .constants import ( + DEFAULT_STATISTICS_ROLES, + LOG_LEVELS, + TRANSLATED_MESSAGES_LOCALE_CODES, + VALID_SEND_INTRODUCTION_REMINDERS_VALUES, +) + +SendIntroductionRemindersFlagType: TypeAlias = Literal["once", "interval", False] +LogLevelType: TypeAlias = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + +class SlugValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] + @override + def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimported,misc] + return slugify.slugify(str(chunk.contents)) + + +class LogLevelValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] + @override + def validate_scalar(self, chunk: YAMLChunk) -> LogLevelType: # type: ignore[no-any-unimported,misc] + val: str = str(chunk.contents).upper() + + if val not in LOG_LEVELS: + chunk.expecting_but_found( + ( + "when expecting a valid log-level " + f"(one of: \"{"\", \"".join(LOG_LEVELS)}\")" + ), + ) + raise RuntimeError + + return val # type: ignore[return-value] + + # noinspection PyOverrides + @override + def to_yaml(self, data: object) -> str: # type: ignore[misc] + str_data: str = str(data).upper() + + if str_data.upper() not in LOG_LEVELS: + raise YAMLSerializationError( + f"Got {data} when expecting one of: \"{"\", \"".join(LOG_LEVELS)}\".", + ) + + return str_data + + +class DiscordWebhookURLValidator(strictyaml.Url): # type: ignore[no-any-unimported,misc] + @override + def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimported,misc] + CHUNK_IS_VALID: Final[bool] = bool( + ( + self.__is_absolute_url(chunk.contents) + and chunk.contents.startswith("https://discord.com/api/webhooks/") + ), + ) + if not CHUNK_IS_VALID: + chunk.expecting_but_found("when expecting a Discord webhook URL") + raise RuntimeError + + return chunk.contents # type: ignore[no-any-return] + + @override + def to_yaml(self, data: object) -> str: # type: ignore[misc] + self.should_be_string(data, "expected a URL,") + + if not isinstance(data, str): + raise TypeError + + DATA_IS_VALID: Final[bool] = bool( + ( + self.__is_absolute_url(data) + and data.startswith("https://discord.com/api/webhooks/") + ), + ) + if not DATA_IS_VALID: + raise YAMLSerializationError(f"'{data}' is not a Discord webhook URL.") + + return data + + +class DiscordSnowflakeValidator(strictyaml.Int): # type: ignore[no-any-unimported,misc] + @override + def validate_scalar(self, chunk: YAMLChunk) -> int: # type: ignore[no-any-unimported,misc] + val: int = super().validate_scalar(chunk) + + if not re.match(r"\A\d{17,20}\Z", str(val)): + chunk.expecting_but_found("when expecting a Discord snowflake ID") + raise RuntimeError + + return val + + @override + def to_yaml(self, data: object) -> str: # type: ignore[misc] + DATA_IS_VALID: Final[bool] = bool( + ( + (strictyaml_utils.is_string(data) or isinstance(data, int)) + and strictyaml_utils.is_integer(str(data)) + and re.match(r"\A\d{17,20}\Z", str(data)) + ), + ) + if not DATA_IS_VALID: + raise YAMLSerializationError(f"'{data}' is not a Discord snowflake ID.") + + return str(data) + + +class ProbabilityValidator(strictyaml.Float): # type: ignore[no-any-unimported,misc] + @override + def validate_scalar(self, chunk: YAMLChunk) -> float: # type: ignore[no-any-unimported,misc] + val: float = 100 * super().validate_scalar(chunk) + + if not 0 <= val <= 100: + chunk.expecting_but_found("when expecting a probability") + raise RuntimeError + + return val + + @override + def to_yaml(self, data: object) -> str: # type: ignore[misc] + YAML_SERIALIZATION_ERROR: Final[YAMLSerializationError] = YAMLSerializationError( # type: ignore[no-any-unimported] + f"'{data}' is not a probability.", + ) + + if strictyaml_utils.has_number_type(data): + if not isinstance(data, float): + raise TypeError + + if not 0 <= data <= 100: + raise YAML_SERIALIZATION_ERROR + + if math.isnan(data): + return "nan" + if data == float("inf"): + return "inf" + if data == float("-inf"): + return "-inf" + + return str(data / 100) + + if strictyaml_utils.is_string(data) and strictyaml_utils.is_decimal(data): + if not isinstance(data, str): + raise TypeError + + float_data: float = float(data) + + if not 0 <= float_data <= 100: + raise YAML_SERIALIZATION_ERROR + + return str(float_data / 100) + + raise YAML_SERIALIZATION_ERROR + + +class SendIntroductionRemindersFlagValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] + @override + def validate_scalar(self, chunk: YAMLChunk) -> SendIntroductionRemindersFlagType: # type: ignore[no-any-unimported,misc] + val: str = str(chunk.contents).lower() + + if val not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: + chunk.expecting_but_found( + ( + "when expecting a send-introduction-reminders-flag " + "(one of: \"Once\", \"Interval\" or \"False\")" + ), + ) + raise RuntimeError + + if val in strictyaml_constants.TRUE_VALUES: + return "once" + + if val not in ("once", "interval"): + return False + + return val # type: ignore[return-value] + + # noinspection PyOverrides + @override + def to_yaml(self, data: object) -> str: # type: ignore[misc] + if isinstance(data, bool): + return "Once" if data else "False" + + if not isinstance(data, str) or data not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: + raise YAMLSerializationError( + f"Got {data} when expecting one of: \"Once\", \"Interval\" or \"False\".", + ) + + if data in strictyaml_constants.TRUE_VALUES: + return "Once" + + if data in strictyaml_constants.FALSE_VALUES: + return "False" + + return data.title() + + +SETTINGS_YAML_SCHEMA: Final[strictyaml.Map] = strictyaml.Map( # type: ignore[no-any-unimported] + { + strictyaml.Optional("console-log-level", default="INFO"): LogLevelValidator(), + strictyaml.Optional("discord-log-channel-log-level", default="WARNING"): ( + LogLevelValidator() + ), + "discord-bot-token": strictyaml.Regex( + r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", + ), + strictyaml.Optional("discord-log-channel-webhook-url"): DiscordWebhookURLValidator(), + "discord-guild-id": DiscordSnowflakeValidator(), + strictyaml.Optional("group-full-name"): strictyaml.Regex( + r"\A[A-Za-z0-9 '&!?:,.#%\"-]+\Z", + ), + strictyaml.Optional("group-short-name"): strictyaml.Regex( + r"\A[A-Za-z0-9'&!?:,.#%\"-]+\Z", + ), + strictyaml.Optional("purchase-membership-url"): strictyaml.Url(), + strictyaml.Optional("membership-perks-url"): strictyaml.Url(), + strictyaml.Optional("ping-command-easter-egg-probability", default=0.01): ( + ProbabilityValidator() + ), + strictyaml.Optional("messages-language", default="en-GB"): strictyaml.Enum( + TRANSLATED_MESSAGES_LOCALE_CODES, + ), + "members-list-url": strictyaml.Url(), + "members-list-url-session-cookie": strictyaml.Str(), + strictyaml.Optional("send-introduction-reminders", default="once"): ( + SendIntroductionRemindersFlagValidator() + ), + strictyaml.Optional("send-introduction-reminders-delay", default="40h"): ( + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + ) + ), + strictyaml.Optional("send-introduction-reminders-interval", default="6h"): ( + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", + ) + ), + strictyaml.Optional("send-get-roles-reminders", default=True): strictyaml.Bool(), + strictyaml.Optional("send-get-roles-reminders-delay", default="40h"): ( + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + ) + ), + strictyaml.Optional("statistics-days", default=30.0): strictyaml.Float(), + strictyaml.Optional("statistics-roles", default=list(DEFAULT_STATISTICS_ROLES)): ( + strictyaml.UniqueSeq(strictyaml.Str()) + ), + "moderation-document-url": strictyaml.Url(), + strictyaml.Optional("manual-moderation-warning-message-location", default="DM"): ( + strictyaml.Str() + ), + strictyaml.Optional("strike-timeout-duration", default="24h"): strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + ), + strictyaml.Optional("group-member-id-format", default=r"\A\d{7}\Z"): ( + strictyaml.Str() + ), + strictyaml.Optional("advanced", default={}): strictyaml.EmptyDict() | ( + strictyaml.Map( + { + strictyaml.Optional("send-get-roles-reminders-interval", default="6h"): ( + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", + ) + ), + }, + key_validator=SlugValidator(), + ) + ), + }, + key_validator=SlugValidator(), +) diff --git a/config/constants.py b/config/constants.py new file mode 100644 index 000000000..379638f11 --- /dev/null +++ b/config/constants.py @@ -0,0 +1,51 @@ +"""Constant values that are defined for quick access.""" + +from collections.abc import Sequence + +__all__: Sequence[str] = ( + "PROJECT_ROOT", + "VALID_SEND_INTRODUCTION_REMINDERS_VALUES", + "TRANSLATED_MESSAGES_LOCALE_CODES", + "DEFAULT_STATISTICS_ROLES", + "LOG_LEVELS", +) + + +from pathlib import Path +from typing import Final + +from strictyaml import constants as strictyaml_constants + +PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.resolve() +VALID_SEND_INTRODUCTION_REMINDERS_VALUES: Final[frozenset[str]] = frozenset( + ({"once", "interval"} | set(strictyaml_constants.BOOL_VALUES)), +) +TRANSLATED_MESSAGES_LOCALE_CODES: Final[frozenset[str]] = frozenset({"en-GB"}) +DEFAULT_STATISTICS_ROLES: Final[frozenset[str]] = frozenset( + { + "Committee", + "Committee-Elect", + "Student Rep", + "Member", + "Guest", + "Server Booster", + "Foundation Year", + "First Year", + "Second Year", + "Final Year", + "Year In Industry", + "Year Abroad", + "PGT", + "PGR", + "Alumnus/Alumna", + "Postdoc", + "Quiz Victor", + }, +) +LOG_LEVELS: Final[Sequence[str]] = ( + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL", +) diff --git a/config/settings.py b/config/settings.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/db/core/models/managers.py b/db/core/models/managers.py index dc95cfaac..b9d6025f1 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): diff --git a/poetry.lock b/poetry.lock index b6b9ff0fd..c27f52768 100644 --- a/poetry.lock +++ b/poetry.lock @@ -197,19 +197,19 @@ lxml = ["lxml"] [[package]] name = "ccft-pymarkdown" -version = "1.1.0" +version = "1.1.1" 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.1-py3-none-any.whl", hash = "sha256:adeb27e94ef67ac368be7d24eac712480a9a6f7294e368253f0b3c516e6e1e8f"}, + {file = "ccft_pymarkdown-1.1.1.tar.gz", hash = "sha256:1cd3c5c797da87ad33ed40502a85703104f80f520f5ba33b0477cc171cff201f"}, ] [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" @@ -1454,6 +1454,23 @@ discord-webhook = ">=1.0.0,<2.0.0" [package.extras] docs = ["Sphinx (>=4.4.0,<5.0.0)", "sphinx-autodoc-typehints[docs] (>=1.16.0,<2.0.0)", "sphinx-rtd-theme (>=1.0.0,<2.0.0)", "sphinx-sitemap (>=2.2.0,<3.0.0)"] +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1515,13 +1532,13 @@ files = [ [[package]] name = "requests" -version = "2.32.1" +version = "2.32.2" 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.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, + {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, ] [package.dependencies] @@ -1562,19 +1579,18 @@ files = [ [[package]] name = "setuptools" -version = "69.5.1" +version = "70.0.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.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, + {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, ] [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"] +docs = ["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"] +testing = ["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)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.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-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -1624,6 +1640,31 @@ files = [ dev = ["build", "hatch"] doc = ["sphinx"] +[[package]] +name = "strictyaml" +version = "1.7.3" +description = "Strict, typed YAML parser" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "strictyaml-1.7.3-py3-none-any.whl", hash = "sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7"}, + {file = "strictyaml-1.7.3.tar.gz", hash = "sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407"}, +] + +[package.dependencies] +python-dateutil = ">=2.6.0" + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -1869,4 +1910,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "faae6fed029dbf7b339a5f496bb200e6e4ab0b2b7b9110c7fb586d4d844b7767" +content-hash = "2d775c0e23515319c499b9121a2e9df19bada25b35641735bf9bb5d9099112bc" diff --git a/pyproject.toml b/pyproject.toml index 380acc911..03a25d31e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,9 @@ 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.0" +strictyaml = "^1.7.3" +python-slugify = "^8.0" [tool.poetry.group.dev.dependencies] pre-commit = "^3.7" @@ -50,8 +52,6 @@ django-stubs = {extras = ["compatible-mypy"], version = "~5.0"} types-beautifulsoup4 = "^4.12.0" pytest = "^8.2" ruff = "^0.4" -gitpython = "^3.1" -pymarkdownlnt = "^0.9" ccft-pymarkdown = "^1.1" @@ -88,6 +88,9 @@ module = [ "discord_logging.handler", "parsedatetime", "validators", + "strictyaml", + "strictyaml.exceptions", + "strictyaml.yamllocation", ] ignore_missing_imports = true diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index d8dc35184..7c7793f2d 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -39,7 +39,7 @@ Callable[Concatenate[TeXBotBaseCog, P], Coroutine[object, object, T]] ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class ErrorCaptureDecorators: diff --git a/utils/tex_bot.py b/utils/tex_bot.py index cf1965835..bf84ef5c1 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -35,7 +35,7 @@ | None ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class TeXBot(discord.Bot): diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index d64a33639..f16172682 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -30,7 +30,7 @@ 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): From 533c179f183db34e0105a92f8bc9fd18ef612a5d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 22 May 2024 03:02:56 +0100 Subject: [PATCH 003/128] Fix not copying changed directory in `Dockerfile` --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7ce8a3d6a..6ab73d46e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,11 +35,12 @@ COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} WORKDIR /app -COPY LICENSE .en[v] config.py exceptions.py main.py messages.json ./ +COPY LICENSE .en[v] exceptions.py main.py messages.json ./ RUN chmod +x main.py -COPY cogs/ ./cogs/ -COPY db/ ./db/ COPY utils/ ./utils/ +COPY db/ ./db/ +COPY config/ ./config/ +COPY cogs/ ./cogs/ ENTRYPOINT ["python", "-m", "main"] From c797cf828800b3f9de3d4d127ab00779adcf4bd8 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 23 May 2024 02:06:03 +0100 Subject: [PATCH 004/128] Use improved exit reasons --- main.py | 21 +++++++++++++----- utils/__init__.py | 3 ++- utils/tex_bot.py | 56 +++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/main.py b/main.py index cb6d12c25..add478762 100644 --- a/main.py +++ b/main.py @@ -15,8 +15,10 @@ import discord import config +import utils from config import settings -from utils import SuppressTraceback, TeXBot +from typing import NoReturn +from utils import SuppressTraceback, TeXBot, TeXBotExitReason with SuppressTraceback(): config.run_setup() @@ -29,10 +31,19 @@ bot.load_extension("cogs") -if __name__ == "__main__": + +def _run_bot() -> NoReturn: bot.run(settings["DISCORD_BOT_TOKEN"]) + assert not utils.is_running_in_async() + + if bot.EXIT_REASON is TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG: + bot.reset_exit_reason() + config.run_setup() + bot.reload_extension("cogs") + _run_bot() - if bot.EXIT_WAS_DUE_TO_KILL_COMMAND: - raise SystemExit(0) + raise SystemExit(bot.EXIT_REASON.value) - raise SystemExit(1) + +if __name__ == "__main__": + _run_bot() diff --git a/utils/__init__.py b/utils/__init__.py index 60caa0be0..8eca5640e 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -7,6 +7,7 @@ "MessageSenderComponent", "SuppressTraceback", "TeXBot", + "TeXBotExitReason", "TeXBotBaseCog", "TeXBotApplicationContext", "TeXBotAutocompleteContext", @@ -23,7 +24,7 @@ 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 import TeXBot, TeXBotExitReason from utils.tex_bot_base_cog import TeXBotBaseCog from utils.tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext diff --git a/utils/tex_bot.py b/utils/tex_bot.py index bf84ef5c1..f65871770 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -2,16 +2,19 @@ from collections.abc import Sequence -__all__: Sequence[str] = ("TeXBot",) +__all__: Sequence[str] = ("TeXBot", "TeXBotExitReason") import logging import re +from collections.abc import Collection +from enum import IntEnum from logging import Logger from typing import Final, TypeAlias import discord +import utils from config import settings from exceptions import ( ArchivistRoleDoesNotExistError, @@ -38,6 +41,12 @@ logger: Final[Logger] = logging.getLogger("TeX-Bot") +class TeXBotExitReason(IntEnum): + UNKNOWN_ERROR = -1 + KILL_COMMAND_USED = 0 + RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG = 1 + + class TeXBot(discord.Bot): """ Subclass of the default Bot class provided by Pycord. @@ -58,7 +67,7 @@ def __init__(self, *args: object, **options: object) -> None: self._roles_channel: discord.TextChannel | None = None self._general_channel: discord.TextChannel | None = None self._rules_channel: discord.TextChannel | None = None - self._exit_was_due_to_kill_command: bool = False + self._exit_reason: TeXBotExitReason = TeXBotExitReason.UNKNOWN_ERROR self._main_guild_set: bool = False @@ -66,9 +75,9 @@ def __init__(self, *args: object, **options: object) -> None: # noinspection PyPep8Naming @property - def EXIT_WAS_DUE_TO_KILL_COMMAND(self) -> bool: # noqa: N802 - """Return whether the TeX-Bot exited due to the kill command being used.""" - return self._exit_was_due_to_kill_command + def EXIT_REASON(self) -> TeXBotExitReason: # noqa: N802 + """Return the reason for TeX-Bot's last exit.""" + return self._exit_reason @property def main_guild(self) -> discord.Guild: @@ -358,18 +367,47 @@ async def perform_kill_and_close(self, initiated_by_user: discord.User | discord A log message will also be sent, announcing the user that requested the shutdown. """ - if self.EXIT_WAS_DUE_TO_KILL_COMMAND: - EXIT_FLAG_ALREADY_SET_MESSAGE: Final[str] = ( + if self.EXIT_REASON is not TeXBotExitReason.UNKNOWN_ERROR: + EXIT_REASON_ALREADY_SET_MESSAGE: Final[str] = ( "The kill & close command has already been used. Invalid state." ) - raise RuntimeError(EXIT_FLAG_ALREADY_SET_MESSAGE) + raise RuntimeError(EXIT_REASON_ALREADY_SET_MESSAGE) if initiated_by_user: logger.info("Manual shutdown initiated by %s.", initiated_by_user) - self._exit_was_due_to_kill_command = True + self._exit_reason = TeXBotExitReason.KILL_COMMAND_USED + await self.close() + + async def perform_restart_after_config_changes(self) -> None: # noqa: E501 + """Restart TeX-Bot after the config changes.""" + if self.EXIT_REASON is not TeXBotExitReason.UNKNOWN_ERROR: + EXIT_REASON_ALREADY_SET_MESSAGE: Final[str] = ( + "TeX-Bot cannot be restarted as the exit reason has already been set. " + "Invalid state." + ) + raise RuntimeError(EXIT_REASON_ALREADY_SET_MESSAGE) + + self._exit_reason = TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG await self.close() + def reset_exit_reason(self) -> None: + """Reset the exit reason of TeX-Bot back to `UNKNOWN_ERROR`.""" + if utils.is_running_in_async(): + raise RuntimeError("Cannot reset exit reason when TeX-Bot is currently running.") + + RESETABLE_EXIT_REASONS: Collection[TeXBotExitReason] = ( + TeXBotExitReason.UNKNOWN_ERROR, + TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG, + ) + if self.EXIT_REASON not in RESETABLE_EXIT_REASONS: + raise RuntimeError( + "Cannot reset exit reason, due to incorrect current exit reason. " + "Invalid state." + ) + + self._exit_reason = TeXBotExitReason.UNKNOWN_ERROR + async def get_everyone_role(self) -> discord.Role: """ Util method to retrieve the "@everyone" role from your group's Discord guild. From f47fd8c7ee8ce52cbcee4ae7caef3186aceaf1c6 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 23 May 2024 02:07:35 +0100 Subject: [PATCH 005/128] Improve YAML loading --- config/__init__.py | 53 ++++----------------------------------------- config/_yaml.py | 15 ++++++++++++- config/constants.py | 3 +++ 3 files changed, 21 insertions(+), 50 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index 0940b00c7..96d1b49e9 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -15,6 +15,7 @@ ) +import contextlib import importlib import logging import os @@ -23,6 +24,7 @@ from pathlib import Path from typing import Final +from exceptions import BotRequiresRestartAfterConfigChange from ._settings import SettingsAccessor from .constants import PROJECT_ROOT @@ -31,59 +33,12 @@ settings: Final[SettingsAccessor] = SettingsAccessor() -def _get_settings_file_path() -> Path: - settings_file_not_found_message: str = ( - "No settings file was found. " - "Please make sure you have created a `TeX-Bot-deployment.yaml` file." - ) - - raw_settings_file_path: str | None = ( - os.getenv("TEX_BOT_SETTINGS_FILE_PATH", None) - or os.getenv("TEX_BOT_SETTINGS_FILE", None) - or os.getenv("TEX_BOT_SETTINGS_PATH", None) - or os.getenv("TEX_BOT_SETTINGS", None) - or os.getenv("TEX_BOT_CONFIG_FILE_PATH", None) - or os.getenv("TEX_BOT_CONFIG_FILE", None) - or os.getenv("TEX_BOT_CONFIG_PATH", None) - or os.getenv("TEX_BOT_CONFIG", None) - or os.getenv("TEX_BOT_DEPLOYMENT_FILE_PATH", None) - or os.getenv("TEX_BOT_DEPLOYMENT_FILE", None) - or os.getenv("TEX_BOT_DEPLOYMENT_PATH", None) - or os.getenv("TEX_BOT_DEPLOYMENT", None) - ) - - if raw_settings_file_path: - settings_file_not_found_message = ( - "A path to the settings file location was provided by environment variable, " - "however this path does not refer to an existing file." - ) - else: - logger.debug( - ( - "Settings file location not supplied by environment variable, " - "falling back to `Tex-Bot-deployment.yaml`." - ), - ) - raw_settings_file_path = "TeX-Bot-deployment.yaml" - if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): - raw_settings_file_path = "TeX-Bot-settings.yaml" - - if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): - raw_settings_file_path = "TeX-Bot-config.yaml" - - settings_file_path: Path = Path(raw_settings_file_path) - - if not settings_file_path.is_file(): - raise FileNotFoundError(settings_file_not_found_message) - - return settings_file_path - - def run_setup() -> None: """Execute the setup functions required, before other modules can be run.""" check_for_deprecated_environment_variables() - settings.reload(_get_settings_file_path()) + with contextlib.suppress(BotRequiresRestartAfterConfigChange): + settings.reload() logger.debug("Begin database setup") diff --git a/config/_yaml.py b/config/_yaml.py index be0ec7a4e..aa8bfee37 100644 --- a/config/_yaml.py +++ b/config/_yaml.py @@ -4,6 +4,7 @@ "SlugValidator", "DiscordWebhookURLValidator", "SETTINGS_YAML_SCHEMA", + "load_yaml", ) import math @@ -12,7 +13,7 @@ import slugify import strictyaml -from strictyaml import constants as strictyaml_constants +from strictyaml import YAML, constants as strictyaml_constants from strictyaml import utils as strictyaml_utils from strictyaml.exceptions import YAMLSerializationError from strictyaml.yamllocation import YAMLChunk @@ -287,3 +288,15 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] }, key_validator=SlugValidator(), ) + + +def load_yaml(raw_yaml: str) -> YAML: + parsed_yaml: YAML = strictyaml.load(raw_yaml, SETTINGS_YAML_SCHEMA) # type: ignore[no-any-unimported] + + # noinspection SpellCheckingInspection + if "guildofstudents" in parsed_yaml["members-list-url"]: + parsed_yaml["members-list-url-session-cookie"].revalidate( + strictyaml.Regex(r"\A[A-Fa-f\d]{128,256}\Z"), + ) + + return parsed_yaml diff --git a/config/constants.py b/config/constants.py index 379638f11..597cc0ab0 100644 --- a/config/constants.py +++ b/config/constants.py @@ -17,10 +17,13 @@ from strictyaml import constants as strictyaml_constants PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.resolve() + VALID_SEND_INTRODUCTION_REMINDERS_VALUES: Final[frozenset[str]] = frozenset( ({"once", "interval"} | set(strictyaml_constants.BOOL_VALUES)), ) + TRANSLATED_MESSAGES_LOCALE_CODES: Final[frozenset[str]] = frozenset({"en-GB"}) + DEFAULT_STATISTICS_ROLES: Final[frozenset[str]] = frozenset( { "Committee", From e36c1c6910800f2a66e9bd71f5b02ece870125ae Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 23 May 2024 02:08:12 +0100 Subject: [PATCH 006/128] Add restart after config change functionality --- config/_settings.py | 113 ++++++++++++++++++++++++++++++++++++++++---- config/constants.py | 15 ++++++ exceptions.py | 15 ++++++ 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/config/_settings.py b/config/_settings.py index ba2f89fc0..5b05ca74f 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -10,20 +10,71 @@ __all__: Sequence[str] = ("SettingsAccessor",) +import contextlib import logging +import os import re from logging import Logger from pathlib import Path from typing import Any, ClassVar, Final -import strictyaml from strictyaml import YAML -from ._yaml import SETTINGS_YAML_SCHEMA +from exceptions import BotRequiresRestartAfterConfigChange +from ._yaml import load_yaml +from .constants import REQUIRES_RESTART_SETTINGS_KEYS, PROJECT_ROOT logger: Final[Logger] = logging.getLogger("TeX-Bot") +def _get_settings_file_path() -> Path: + settings_file_not_found_message: str = ( + "No settings file was found. " + "Please make sure you have created a `TeX-Bot-deployment.yaml` file." + ) + + raw_settings_file_path: str | None = ( + os.getenv("TEX_BOT_SETTINGS_FILE_PATH", None) + or os.getenv("TEX_BOT_SETTINGS_FILE", None) + or os.getenv("TEX_BOT_SETTINGS_PATH", None) + or os.getenv("TEX_BOT_SETTINGS", None) + or os.getenv("TEX_BOT_CONFIG_FILE_PATH", None) + or os.getenv("TEX_BOT_CONFIG_FILE", None) + or os.getenv("TEX_BOT_CONFIG_PATH", None) + or os.getenv("TEX_BOT_CONFIG", None) + or os.getenv("TEX_BOT_DEPLOYMENT_FILE_PATH", None) + or os.getenv("TEX_BOT_DEPLOYMENT_FILE", None) + or os.getenv("TEX_BOT_DEPLOYMENT_PATH", None) + or os.getenv("TEX_BOT_DEPLOYMENT", None) + ) + + if raw_settings_file_path: + settings_file_not_found_message = ( + "A path to the settings file location was provided by environment variable, " + "however this path does not refer to an existing file." + ) + else: + logger.debug( + ( + "Settings file location not supplied by environment variable, " + "falling back to `Tex-Bot-deployment.yaml`." + ), + ) + raw_settings_file_path = "TeX-Bot-deployment.yaml" + if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): + raw_settings_file_path = "TeX-Bot-settings.yaml" + + if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): + raw_settings_file_path = "TeX-Bot-config.yaml" + + settings_file_path: Path = Path(raw_settings_file_path) + + if not settings_file_path.is_file(): + raise FileNotFoundError(settings_file_not_found_message) + + return settings_file_path + + class SettingsAccessor: """ Settings class that provides access to all settings values. @@ -48,8 +99,9 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) - if not self._is_env_variables_setup: - self._setup_env_variables() + if self._most_recent_yaml is None: + with contextlib.suppress(BotRequiresRestartAfterConfigChange): + self.reload() if item in self._settings: return self._settings[item] @@ -81,9 +133,52 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 raise KeyError(key_error_message) from None @classmethod - def reload(cls, settings_file_path: Path) -> None: - current_yaml: YAML = strictyaml.load( # type: ignore[no-any-unimported] - settings_file_path.read_text(), - SETTINGS_YAML_SCHEMA, + def reload(cls) -> None: + current_yaml: YAML = load_yaml(_get_settings_file_path().read_text()) # type: ignore[no-any-unimported] + + if current_yaml == cls._most_recent_yaml: + return + + changed_settings_keys: set[str] = set() + + if cls._reload_console_logging(current_yaml["console-log-level"]): + changed_settings_keys.add("console-log-level") + + if cls._reload_discord_log_channel_log_level(current_yaml["discord-log-channel-log-level"]): + changed_settings_keys.add("discord-log-channel-log-level") + + cls._most_recent_yaml = current_yaml + + if changed_settings_keys & REQUIRES_RESTART_SETTINGS_KEYS: + raise BotRequiresRestartAfterConfigChange(changed_settings=changed_settings_keys) + + @classmethod + def _reload_console_logging(cls, console_log_level: str) -> bool: + CONSOLE_LOG_LEVEL_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or console_log_level != cls._most_recent_yaml["console-log-level"] + ) + if not CONSOLE_LOG_LEVEL_CHANGED: + return False + + logger.setLevel(getattr(logging, console_log_level)) + + logger.handlers.clear() + + console_logging_handler: logging.Handler = logging.StreamHandler() + # noinspection SpellCheckingInspection + console_logging_handler.setFormatter( + logging.Formatter( + "{asctime} | {name} | {levelname:^8} - {message}", + style="{", + ), ) - # TODO: Revalidate session cookie based upon URL + logger.addHandler(console_logging_handler) + + logger.propagate = False + + return True + + @classmethod + def _reload_discord_log_channel_log_level(cls, discord_log_channel_log_level: str) -> bool: + raise NotImplementedError diff --git a/config/constants.py b/config/constants.py index 597cc0ab0..379060954 100644 --- a/config/constants.py +++ b/config/constants.py @@ -8,6 +8,7 @@ "TRANSLATED_MESSAGES_LOCALE_CODES", "DEFAULT_STATISTICS_ROLES", "LOG_LEVELS", + "REQUIRES_RESTART_SETTINGS_KEYS", ) @@ -45,6 +46,7 @@ "Quiz Victor", }, ) + LOG_LEVELS: Final[Sequence[str]] = ( "DEBUG", "INFO", @@ -52,3 +54,16 @@ "ERROR", "CRITICAL", ) + +REQUIRES_RESTART_SETTINGS_KEYS: Final[frozenset[str]] = frozenset( + { + "discord-bot-token", + "discord-guild-id", + "send-introduction-reminders", + "send-introduction-reminders-delay", + "send-introduction-reminders-interval", + "send-get-roles-reminders", + "send-get-roles-reminders-delay", + "send-get-roles-reminders-interval", + }, +) diff --git a/exceptions.py b/exceptions.py index e4509a9f5..89278fb4b 100644 --- a/exceptions.py +++ b/exceptions.py @@ -24,6 +24,7 @@ "ChannelDoesNotExistError", "RolesChannelDoesNotExistError", "GeneralChannelDoesNotExistError", + "BotRequiresRestartAfterConfigChange", ) @@ -72,6 +73,20 @@ def __repr__(self) -> str: return formatted +class BotRequiresRestartAfterConfigChange(BaseTeXBotError): + # 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 + return "TeX-Bot requires a restart due to configuration changes." + + def __init__(self, message: str | None = None, changed_settings: set[str] | None = None) -> None: # noqa: E501 + """Initialize a ValueError exception for a non-existent user ID.""" + self.changed_settings: set[str] | None = changed_settings + + super().__init__(message) + + class BaseErrorWithErrorCode(BaseTeXBotError, abc.ABC): """Base class for exception errors that have an error code.""" From 80a8b9c9aa9f70c28d8a7eb6d33cd64a26a6d77f Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 23 May 2024 02:53:37 +0100 Subject: [PATCH 007/128] Add more config settings loading --- config/__init__.py | 2 +- config/_settings.py | 53 +++++++++++++++++++++++++++++++++++++++------ config/_yaml.py | 8 ++++--- config/constants.py | 3 +++ main.py | 5 ++--- tests/test_utils.py | 4 ---- utils/tex_bot.py | 2 +- 7 files changed, 58 insertions(+), 19 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index 96d1b49e9..4ba6a6f76 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -21,10 +21,10 @@ import os from collections.abc import Iterable from logging import Logger -from pathlib import Path from typing import Final from exceptions import BotRequiresRestartAfterConfigChange + from ._settings import SettingsAccessor from .constants import PROJECT_ROOT diff --git a/config/_settings.py b/config/_settings.py index 5b05ca74f..dae8aa810 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -21,8 +21,13 @@ from strictyaml import YAML from exceptions import BotRequiresRestartAfterConfigChange + from ._yaml import load_yaml -from .constants import REQUIRES_RESTART_SETTINGS_KEYS, PROJECT_ROOT +from .constants import ( + DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL, + PROJECT_ROOT, + REQUIRES_RESTART_SETTINGS_KEYS, +) logger: Final[Logger] = logging.getLogger("TeX-Bot") @@ -84,6 +89,7 @@ class SettingsAccessor: _settings: ClassVar[dict[str, object]] = {} _most_recent_yaml: ClassVar[YAML | None] = None # type: ignore[no-any-unimported] + _discord_log_channel_log_level: str = DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL @classmethod def format_invalid_settings_key_message(cls, item: str) -> str: @@ -144,7 +150,12 @@ def reload(cls) -> None: if cls._reload_console_logging(current_yaml["console-log-level"]): changed_settings_keys.add("console-log-level") - if cls._reload_discord_log_channel_log_level(current_yaml["discord-log-channel-log-level"]): + DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = ( + cls._reload_discord_log_channel_log_level( + current_yaml["discord-log-channel-log-level"], + ) + ) + if DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: changed_settings_keys.add("discord-log-channel-log-level") cls._most_recent_yaml = current_yaml @@ -161,11 +172,26 @@ def _reload_console_logging(cls, console_log_level: str) -> bool: if not CONSOLE_LOG_LEVEL_CHANGED: return False - logger.setLevel(getattr(logging, console_log_level)) + logger.setLevel(1) + + console_logging_handler: logging.StreamHandler = logging.StreamHandler() - logger.handlers.clear() + if logger.hasHandlers(): + stream_handlers: set[logging.StreamHandler] = { + handler + for handler + in logger.handlers + if isinstance(handler, logging.StreamHandler) + } + if len(stream_handlers) > 1: + raise ValueError("Cannot determine which logging stream-handler to update.") + + if len(stream_handlers) == 1: + console_logging_handler = stream_handlers.pop() + + else: + logger.addHandler(console_logging_handler) - console_logging_handler: logging.Handler = logging.StreamHandler() # noinspection SpellCheckingInspection console_logging_handler.setFormatter( logging.Formatter( @@ -173,7 +199,7 @@ def _reload_console_logging(cls, console_log_level: str) -> bool: style="{", ), ) - logger.addHandler(console_logging_handler) + console_logging_handler.setLevel(getattr(logging, console_log_level)) logger.propagate = False @@ -181,4 +207,17 @@ def _reload_console_logging(cls, console_log_level: str) -> bool: @classmethod def _reload_discord_log_channel_log_level(cls, discord_log_channel_log_level: str) -> bool: - raise NotImplementedError + DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or discord_log_channel_log_level != cls._most_recent_yaml[ + "discord-log-channel-log-level" + ] + ) + if not DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: + return False + + cls._discord_log_channel_log_level = discord_log_channel_log_level + + return True + + # TODO: Load more config settings diff --git a/config/_yaml.py b/config/_yaml.py index aa8bfee37..0832087d7 100644 --- a/config/_yaml.py +++ b/config/_yaml.py @@ -13,12 +13,14 @@ import slugify import strictyaml -from strictyaml import YAML, constants as strictyaml_constants +from strictyaml import YAML +from strictyaml import constants as strictyaml_constants from strictyaml import utils as strictyaml_utils from strictyaml.exceptions import YAMLSerializationError from strictyaml.yamllocation import YAMLChunk from .constants import ( + DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL, DEFAULT_STATISTICS_ROLES, LOG_LEVELS, TRANSLATED_MESSAGES_LOCALE_CODES, @@ -216,13 +218,13 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] SETTINGS_YAML_SCHEMA: Final[strictyaml.Map] = strictyaml.Map( # type: ignore[no-any-unimported] { strictyaml.Optional("console-log-level", default="INFO"): LogLevelValidator(), - strictyaml.Optional("discord-log-channel-log-level", default="WARNING"): ( + strictyaml.Optional("discord-log-channel-log-level", default=DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL): ( # noqa: E501 LogLevelValidator() ), + strictyaml.Optional("discord-log-channel-webhook-url"): DiscordWebhookURLValidator(), "discord-bot-token": strictyaml.Regex( r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", ), - strictyaml.Optional("discord-log-channel-webhook-url"): DiscordWebhookURLValidator(), "discord-guild-id": DiscordSnowflakeValidator(), strictyaml.Optional("group-full-name"): strictyaml.Regex( r"\A[A-Za-z0-9 '&!?:,.#%\"-]+\Z", diff --git a/config/constants.py b/config/constants.py index 379060954..508c140cf 100644 --- a/config/constants.py +++ b/config/constants.py @@ -9,6 +9,7 @@ "DEFAULT_STATISTICS_ROLES", "LOG_LEVELS", "REQUIRES_RESTART_SETTINGS_KEYS", + "DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL", ) @@ -67,3 +68,5 @@ "send-get-roles-reminders-interval", }, ) + +DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL: str = "WARNING" diff --git a/main.py b/main.py index add478762..020074c1f 100644 --- a/main.py +++ b/main.py @@ -12,12 +12,12 @@ __all__: Sequence[str] = ("bot",) +from typing import NoReturn + import discord import config -import utils from config import settings -from typing import NoReturn from utils import SuppressTraceback, TeXBot, TeXBotExitReason with SuppressTraceback(): @@ -34,7 +34,6 @@ def _run_bot() -> NoReturn: bot.run(settings["DISCORD_BOT_TOKEN"]) - assert not utils.is_running_in_async() if bot.EXIT_REASON is TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG: bot.reset_exit_reason() diff --git a/tests/test_utils.py b/tests/test_utils.py index b6dcfd8ea..cda4ad8f7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -11,8 +11,6 @@ import utils -# TODO(CarrotManMatt): Move to stats_tests # noqa: FIX002 -# https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/57 # class TestPlotBarChart: # """Test case to unit-test the plot_bar_chart function.""" # @@ -36,8 +34,6 @@ # assert bool(bar_chart_image.fp.read()) is True # noqa: ERA001 -# TODO(CarrotManMatt): Move to stats_tests # noqa: FIX002 -# https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/57 # class TestAmountOfTimeFormatter: # """Test case to unit-test the amount_of_time_formatter function.""" # diff --git a/utils/tex_bot.py b/utils/tex_bot.py index f65871770..7d5567f6e 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -379,7 +379,7 @@ async def perform_kill_and_close(self, initiated_by_user: discord.User | discord self._exit_reason = TeXBotExitReason.KILL_COMMAND_USED await self.close() - async def perform_restart_after_config_changes(self) -> None: # noqa: E501 + async def perform_restart_after_config_changes(self) -> None: """Restart TeX-Bot after the config changes.""" if self.EXIT_REASON is not TeXBotExitReason.UNKNOWN_ERROR: EXIT_REASON_ALREADY_SET_MESSAGE: Final[str] = ( From 14e881f18520f35e72f2eb6629c324efc1afc09c Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 24 May 2024 01:56:25 +0100 Subject: [PATCH 008/128] Add more config loading --- .env.example | 108 -------- cogs/startup.py | 100 +++++--- config/__init__.py | 10 +- config/_pre_startup_utils.py | 16 ++ config/_settings.py | 240 ++++++++++++++---- config/_yaml/__init__.py | 221 ++++++++++++++++ config/_yaml/custom_schema_utils.py | 25 ++ .../{_yaml.py => _yaml/custom_validators.py} | 117 +-------- config/constants.py | 24 +- utils/__init__.py | 26 +- 10 files changed, 560 insertions(+), 327 deletions(-) delete mode 100644 .env.example create mode 100644 config/_pre_startup_utils.py create mode 100644 config/_yaml/__init__.py create mode 100644 config/_yaml/custom_schema_utils.py rename config/{_yaml.py => _yaml/custom_validators.py} (56%) diff --git a/.env.example b/.env.example deleted file mode 100644 index fbf9482ee..000000000 --- a/.env.example +++ /dev/null @@ -1,108 +0,0 @@ -# !!REQUIRED!! -# The Discord token for the bot you created (available on your bot page in the developer portal: https://discord.com/developers/applications)) -# Must be a valid Discord bot token (see https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts) -DISCORD_BOT_TOKEN=[Replace with your Discord bot token] - -# !!REQUIRED!! -# The ID of the your Discord guild -# Must be a valid Discord guild ID (see https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id) -DISCORD_GUILD_ID=[Replace with the ID of the your Discord guild] - -# The webhook URL of the Discord text channel where error logs should be sent -# Error logs will always be sent to the console, this setting allows them to also be sent to a Discord log channel -# Must be a valid Discord channel webhook URL (see https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) -DISCORD_LOG_CHANNEL_WEBHOOK_URL=[Replace with your Discord log channel webhook URL] - -# The full name of your community group, do NOT use an abbreviation. -# This is substituted into many error/welcome messages sent into your Discord guild, by the bot. -# If this is not set the group-full-name will be retrieved from the name of your group's Discord guild -GROUP_NAME=[Replace with the full name of your community group (not an abbreviation)] - -# The short colloquial name of your community group, it is recommended that you set this to be an abbreviation of your group's name. -# If this is not set the group-short-name will be determined from your group's full name -GROUP_SHORT_NAME=[Replace with the short colloquial name of your community group] - -# The URL of the page where guests can purchase a full membership to join your community group -# Must be a valid URL -PURCHASE_MEMBERSHIP_URL=[Replace with your group's purchase=membership URL] - - -# The minimum level that logs must meet in order to be logged to the console output stream -# One of: DEBUG, INFO, WARNING, ERROR, CRITICAL -CONSOLE_LOG_LEVEL=INFO - - -# !!REQUIRED!! -# The URL to retrieve the list of IDs of people that have purchased a membership to your community group -# Ensure that all members are visible without pagination. For example, if your members-list is found on the UoB Guild of Students website, ensure the URL includes the "sort by groups" option -# Must be a valid URL -MEMBERS_LIST_URL=[Replace with your group's members-list URL] - -# !!REQUIRED!! -# The members-list URL session cookie -# If your group's members-list is stored at a URL that requires authentication, this session cookie should authenticate the bot to view your group's members-list, as if it were logged in to the website as a Committee member -# This can be extracted from your web-browser, after logging in to view your members-list yourself. It will probably be listed as a cookie named `.ASPXAUTH` -MEMBERS_LIST_URL_SESSION_COOKIE=[Replace with your .ASPXAUTH cookie] - - -# The probability that the more rare ping command response will be sent instead of the normal one -# Must be a float between & including 1 & 0 -PING_COMMAND_EASTER_EGG_PROBABILITY=0.01 - - -# The path to the messages JSON file that contains the common messages sent by the bot -# Must be a path to a JSON file that exists, that contains a JSON string that can be decoded into a Python dict object -MESSAGES_FILE_PATH=messages.json - - -# Whether introduction reminders will be sent to Discord members that are not inducted, saying that they need to send an introduction to be allowed access -# One of: Once, Interval, False -SEND_INTRODUCTION_REMINDERS=Once - -# How long to wait after a user joins your guild before sending them the first/only message remind them to send an introduction -# Is ignored if SEND_INTRODUCTION_REMINDERS=False -# Must be a string of the seconds, minutes, hours, days or weeks before the first/only reminder is sent (format: "smhdw") -# The delay must be longer than or equal to 1 day (in any allowed format) -SEND_INTRODUCTION_REMINDERS_DELAY=40h - -# The interval of time between sending out reminders to Discord members that are not inducted, saying that they need to send an introduction to be allowed access -# Is ignored if SEND_INTRODUCTION_REMINDERS=Once or SEND_INTRODUCTION_REMINDERS=False -# Must be a string of the seconds, minutes or hours between reminders (format: "smh") -SEND_INTRODUCTION_REMINDERS_INTERVAL=6h - -# Whether reminders will be sent to Discord members that have been inducted, saying that they can get opt-in roles. (This message will be only sent once per Discord member) -# Must be a boolean (True or False) -SEND_GET_ROLES_REMINDERS=True - -# How long to wait after a user is inducted before sending them the message to get some opt-in roles -# Is ignored if SEND_GET_ROLES_REMINDERS=False -# Must be a string of the seconds, minutes, hours, days or weeks before a reminder is sent (format: "smhdw") -# The delay must be longer than or equal to 1 day (in any allowed format) -SEND_GET_ROLES_REMINDERS_DELAY=40h - -# !!This is an advanced configuration variable, so is unlikely to need to be changed from its default value!! -# The interval of time between sending out reminders to Discord members that have been inducted, saying that they can get opt-in roles. (This message will be only sent once, the interval is just how often the check for new guests occurs) -# Is ignored if SEND_GET_ROLES_REMINDERS=False -# Must be a string of the seconds, minutes or hours between reminders (format: "smh") -ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL=24h - - -# The number of days to look over messages sent, to generate statistics data -# Must be a float representing the number of days to look back through -STATISTICS_DAYS=30 - -# The names of the roles to gather statistics about, to display in bar chart graphs -# Must be a comma seperated list of strings of role names -STATISTICS_ROLES=Committee,Committee-Elect,Student Rep,Member,Guest,Server Booster,Foundation Year,First Year,Second Year,Final Year,Year In Industry,Year Abroad,PGT,PGR,Alumnus/Alumna,Postdoc,Quiz Victor - - -# !!REQUIRED!! -# The URL of the your group's Discord guild moderation document -# Must be a valid URL -MODERATION_DOCUMENT_URL=[Replace with your group's moderation document URL] - - -# The name of the channel, that warning messages will be sent to when a committee-member manually applies a moderation action (instead of using the `/strike` command) -# Must be the name of a Discord channel in your group's Discord guild, or the value "DM" (which indicates that the messages will be sent in the committee-member's DMs) -# This can be the name of ANY Discord channel (so the offending person *will* be able to see these messages if a public channel is chosen) -MANUAL_MODERATION_WARNING_MESSAGE_LOCATION=DM diff --git a/cogs/startup.py b/cogs/startup.py index 981211b01..9198c00c4 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -14,6 +14,7 @@ import utils from config import settings +from config.constants import DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME from exceptions import ( ArchivistRoleDoesNotExistError, CommitteeRoleDoesNotExistError, @@ -31,41 +32,65 @@ class StartupCog(TeXBotBaseCog): """Cog class that defines additional code to execute upon startup.""" - @TeXBotBaseCog.listener() - async def on_ready(self) -> None: - """ - Populate the shortcut accessors of the bot after initialisation. + def _setup_discord_log_channel(self) -> None: + NO_DISCORD_LOG_CHANNEL_SET_MESSAGE: Final[str] = ( + "Discord log-channel webhook-URL was not set, " + "so error logs will not be sent to the Discord log-channel." + ) - Shortcut accessors should only be populated once the 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", - settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"], - avatar_url=( - self.bot.user.avatar.url - if self.bot.user and self.bot.user.avatar - else None - ), - ) - discord_logging_handler.setLevel(logging.WARNING) - # noinspection SpellCheckingInspection - discord_logging_handler.setFormatter( - logging.Formatter("{levelname} | {message}", style="{"), + discord_logging_handlers: set[DiscordHandler] = { + handler for handler in logger.handlers if isinstance(handler, DiscordHandler) + } + + if len(discord_logging_handlers) > 1: + raise ValueError( + "Cannot determine which logging Discord-webhook-handler to update." ) - logger.addHandler(discord_logging_handler) + elif len(discord_logging_handlers) == 1: + existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() + + logger.removeHandler(existing_discord_logging_handler) + + if settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"]: + new_discord_logging_handler: DiscordHandler = DiscordHandler( + ( + existing_discord_logging_handler.name + if existing_discord_logging_handler.name != DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME # noqa: E501 + else ( + self.bot.user.name + if self.bot.user + else DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME) + ), + settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"], + avatar_url=( + self.bot.user.avatar.url + if self.bot.user and self.bot.user.avatar + else None + ), + ) + new_discord_logging_handler.setLevel(existing_discord_logging_handler.level) + new_discord_logging_handler.setFormatter( + existing_discord_logging_handler.formatter + ) + new_discord_logging_handler.avatar_url = new_discord_logging_handler.avatar_url + + logger.addHandler(new_discord_logging_handler) + + else: + logger.warning(NO_DISCORD_LOG_CHANNEL_SET_MESSAGE) + + elif len(discord_logging_handlers) == 0 or not settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"]: # noqa: E501 + logger.warning(NO_DISCORD_LOG_CHANNEL_SET_MESSAGE) else: - logger.warning( - "DISCORD_LOG_CHANNEL_WEBHOOK_URL was not set, " - "so error logs will not be sent to the Discord log channel.", - ) + raise ValueError + async def _get_main_guild(self) -> discord.Guild: try: main_guild: discord.Guild | None = self.bot.main_guild except GuildDoesNotExistError: - main_guild = self.bot.get_guild(settings["DISCORD_GUILD_ID"]) + main_guild = self.bot.get_guild(settings["DISCORD_MAIN_GUILD_ID"]) if main_guild: self.bot.set_main_guild(main_guild) @@ -75,21 +100,34 @@ async def on_ready(self) -> None: "Invite URL: %s", utils.generate_invite_url( self.bot.application_id, - settings["DISCORD_GUILD_ID"]), + settings["DISCORD_MAIN_GUILD_ID"]), ) - logger.critical(GuildDoesNotExistError(guild_id=settings["DISCORD_GUILD_ID"])) + logger.critical(GuildDoesNotExistError(guild_id=settings["DISCORD_MAIN_GUILD_ID"])) await self.bot.close() - return + raise RuntimeError + + return main_guild + + @TeXBotBaseCog.listener() + async def on_ready(self) -> None: + """ + Populate the shortcut accessors of the bot after initialisation. + + Shortcut accessors should only be populated once the bot is ready to make API requests. + """ + self._setup_discord_log_channel() + + main_guild: discord.Guild = await self._get_main_guild() if self.bot.application_id: logger.debug( "Invite URL: %s", utils.generate_invite_url( self.bot.application_id, - settings["DISCORD_GUILD_ID"]), + settings["DISCORD_MAIN_GUILD_ID"]), ) - if not discord.utils.get(main_guild.roles, name="Committee"): + if not discord.utils.get(main_guild.roles, name="Committee"): # TODO: Move to separate functions logger.warning(CommitteeRoleDoesNotExistError()) if not discord.utils.get(main_guild.roles, name="Guest"): diff --git a/config/__init__.py b/config/__init__.py index 4ba6a6f76..9cfc52271 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -9,6 +9,9 @@ __all__: Sequence[str] = ( "PROJECT_ROOT", + "TRANSLATED_MESSAGES_LOCALE_CODES", + "DEFAULT_STATISTICS_ROLES", + "LOG_LEVELS", "run_setup", "settings", "check_for_deprecated_environment_variables", @@ -26,7 +29,12 @@ from exceptions import BotRequiresRestartAfterConfigChange from ._settings import SettingsAccessor -from .constants import PROJECT_ROOT +from .constants import ( + PROJECT_ROOT, + TRANSLATED_MESSAGES_LOCALE_CODES, + DEFAULT_STATISTICS_ROLES, + LOG_LEVELS, +) logger: Final[Logger] = logging.getLogger("TeX-Bot") diff --git a/config/_pre_startup_utils.py b/config/_pre_startup_utils.py new file mode 100644 index 000000000..574dfb600 --- /dev/null +++ b/config/_pre_startup_utils.py @@ -0,0 +1,16 @@ +from collections.abc import Sequence + +__all__: Sequence[str] = ("is_running_in_async",) + + +import asyncio + + +def is_running_in_async() -> bool: + """Determine whether the current context is asynchronous or not.""" + try: + asyncio.get_running_loop() + except RuntimeError: + return False + else: + return True diff --git a/config/_settings.py b/config/_settings.py index dae8aa810..5c9961b9f 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -18,15 +18,17 @@ from pathlib import Path from typing import Any, ClassVar, Final +from discord_logging.handler import DiscordHandler from strictyaml import YAML +from ._pre_startup_utils import is_running_in_async from exceptions import BotRequiresRestartAfterConfigChange from ._yaml import load_yaml from .constants import ( - DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL, + DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME, PROJECT_ROOT, - REQUIRES_RESTART_SETTINGS_KEYS, + REQUIRES_RESTART_SETTINGS, ) logger: Final[Logger] = logging.getLogger("TeX-Bot") @@ -89,7 +91,6 @@ class SettingsAccessor: _settings: ClassVar[dict[str, object]] = {} _most_recent_yaml: ClassVar[YAML | None] = None # type: ignore[no-any-unimported] - _discord_log_channel_log_level: str = DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL @classmethod def format_invalid_settings_key_message(cls, item: str) -> str: @@ -110,6 +111,14 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 self.reload() if item in self._settings: + ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: Final[bool] = bool( + "bot" in item.lower() + and "token" in item.lower() + and is_running_in_async() + ) + if ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: + raise RuntimeError(f"Cannot access {item!r} when TeX-Bot is already running.") + return self._settings[item] if re.match(r"\A[A-Z](?:[A-Z_]*[A-Z])?\Z", item): @@ -147,77 +156,210 @@ def reload(cls) -> None: changed_settings_keys: set[str] = set() - if cls._reload_console_logging(current_yaml["console-log-level"]): - changed_settings_keys.add("console-log-level") - - DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = ( - cls._reload_discord_log_channel_log_level( - current_yaml["discord-log-channel-log-level"], - ) + changed_settings_keys.update( + cls._reload_console_logging(current_yaml["logging"]["console"]), + cls._reload_discord_log_channel_logging( + current_yaml["logging"].get("discord-channel", None), + ), + cls._reload_discord_bot_token(current_yaml["discord"]["bot-token"]), + cls._reload_discord_main_guild_id(current_yaml["discord"]["main-guild-id"]), ) - if DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: - changed_settings_keys.add("discord-log-channel-log-level") cls._most_recent_yaml = current_yaml - if changed_settings_keys & REQUIRES_RESTART_SETTINGS_KEYS: + if changed_settings_keys & REQUIRES_RESTART_SETTINGS: raise BotRequiresRestartAfterConfigChange(changed_settings=changed_settings_keys) @classmethod - def _reload_console_logging(cls, console_log_level: str) -> bool: - CONSOLE_LOG_LEVEL_CHANGED: Final[bool] = bool( + def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: + """ + Reload the console logging configuration with the new given log level. + + Returns the set of settings keys that have been changed. + """ + CONSOLE_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or console_log_level != cls._most_recent_yaml["console-log-level"] + or console_logging_settings != cls._most_recent_yaml["logging"]["console"] ) - if not CONSOLE_LOG_LEVEL_CHANGED: - return False - - logger.setLevel(1) + if not CONSOLE_LOGGING_SETTINGS_CHANGED: + return set() + + stream_handlers: set[logging.StreamHandler] = { + handler + for handler + in logger.handlers + if isinstance(handler, logging.StreamHandler) + } + if len(stream_handlers) > 1: + raise ValueError("Cannot determine which logging stream-handler to update.") console_logging_handler: logging.StreamHandler = logging.StreamHandler() - if logger.hasHandlers(): - stream_handlers: set[logging.StreamHandler] = { - handler - for handler - in logger.handlers - if isinstance(handler, logging.StreamHandler) - } - if len(stream_handlers) > 1: - raise ValueError("Cannot determine which logging stream-handler to update.") + if len(stream_handlers) == 0: + # noinspection SpellCheckingInspection + console_logging_handler.setFormatter( + logging.Formatter( + "{asctime} | {name} | {levelname:^8} - {message}", + style="{", + ), + ) + logger.setLevel(1) + logger.addHandler(console_logging_handler) + logger.propagate = False - if len(stream_handlers) == 1: - console_logging_handler = stream_handlers.pop() + elif len(stream_handlers) == 1: + console_logging_handler = stream_handlers.pop() else: - logger.addHandler(console_logging_handler) + raise ValueError + + console_logging_handler.setLevel( + getattr(logging, console_logging_settings["log-level"].data) + ) + + return {"logging:console:log-level"} + + @classmethod + def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: YAML | None) -> set[str]: # noqa: E501 + """ + Reload the Discord log channel logging configuration. + + Returns the set of settings keys that have been changed. + """ + DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or discord_channel_logging_settings != cls._most_recent_yaml["logging"].get( + "discord-channel", + None, + ) + or "DISCORD_LOG_CHANNEL_WEBHOOK_URL" not in cls._settings + ) + if not DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: + return set() + + if discord_channel_logging_settings is None: + cls._settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"] = None + else: + cls._settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"] = ( + discord_channel_logging_settings["webhook_url"].data + ) + + discord_logging_handlers: set[DiscordHandler] = { + handler for handler in logger.handlers if isinstance(handler, DiscordHandler) + } + if len(discord_logging_handlers) > 1: + raise ValueError( + "Cannot determine which logging Discord-webhook-handler to update." + ) + + discord_logging_handler_display_name: str = ( + DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME + ) + discord_logging_handler_avatar_url: str | None = None + + if len(discord_logging_handlers) == 1: + existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() + + ONLY_DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = bool( + discord_channel_logging_settings is not None + and cls._most_recent_yaml["logging"].get("discord-channel", None) is not None + and all( + value == cls._most_recent_yaml["logging"]["discord-channel"].get(key, None) + for key, value + in discord_channel_logging_settings.items() + if key != "log-level" + ) + ) + if ONLY_DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: + DISCORD_LOG_CHANNEL_LOG_LEVEL_IS_SAME: Final[bool] = bool( + discord_channel_logging_settings["log-level"] == cls._most_recent_yaml[ + "logging" + ]["discord-channel"]["log-level"] + ) + if DISCORD_LOG_CHANNEL_LOG_LEVEL_IS_SAME: + raise ValueError( + "Assumed Discord log channel log level had changed, but it hadn't." + ) + + existing_discord_logging_handler.setLevel( + getattr(logging, discord_channel_logging_settings["log-level"].data) + ) + return {"logging:discord-channel:log-level"} + + discord_logging_handler_display_name = existing_discord_logging_handler.name + discord_logging_handler_avatar_url = existing_discord_logging_handler.avatar_url + logger.removeHandler(existing_discord_logging_handler) + if discord_channel_logging_settings is None: + return {"logging:discord-channel:webhook-url"} + + elif len(discord_logging_handlers) == 0 and discord_channel_logging_settings is None: + return set() + + discord_logging_handler: logging.Handler = DiscordHandler( + discord_logging_handler_display_name, + discord_channel_logging_settings["webhook-url"], + avatar_url=discord_logging_handler_avatar_url, + ) + discord_logging_handler.setLevel( + getattr(logging, discord_channel_logging_settings["log-level"].data) + ) # noinspection SpellCheckingInspection - console_logging_handler.setFormatter( - logging.Formatter( - "{asctime} | {name} | {levelname:^8} - {message}", - style="{", - ), + discord_logging_handler.setFormatter( + logging.Formatter("{levelname} | {message}", style="{"), ) - console_logging_handler.setLevel(getattr(logging, console_log_level)) - logger.propagate = False + logger.addHandler(discord_logging_handler) - return True + changed_settings: set[str] = {"logging:discord-channel:webhook-url"} - @classmethod - def _reload_discord_log_channel_log_level(cls, discord_log_channel_log_level: str) -> bool: DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = bool( + cls._most_recent_yaml["logging"].get("discord-channel", None) is None + or discord_channel_logging_settings["log-level"] != cls._most_recent_yaml[ + "logging" + ]["discord-channel"]["log-level"] + ) + if DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: + changed_settings.add("logging:discord-channel:log-level") + + return changed_settings + + @classmethod + def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: + """ + Reload the Discord bot-token. + + Returns the set of settings keys that have been changed. + """ + DISCORD_BOT_TOKEN_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or discord_bot_token != cls._most_recent_yaml["discord"]["bot-token"] + or "DISCORD_BOT_TOKEN" not in cls._settings + ) + if not DISCORD_BOT_TOKEN_CHANGED: + return set() + + cls._settings["DISCORD_BOT_TOKEN"] = discord_bot_token.data + + return {"discord:bot-token"} + + @classmethod + def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: + """ + Reload the Discord main-guild ID. + + Returns the set of settings keys that have been changed. + """ + DISCORD_MAIN_GUILD_ID_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or discord_log_channel_log_level != cls._most_recent_yaml[ - "discord-log-channel-log-level" - ] + or discord_main_guild_id != cls._most_recent_yaml["discord"]["main-guild-id"] + or "DISCORD_MAIN_GUILD_ID" not in cls._settings ) - if not DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: - return False + if not DISCORD_MAIN_GUILD_ID_CHANGED: + return set() - cls._discord_log_channel_log_level = discord_log_channel_log_level + cls._settings["DISCORD_MAIN_GUILD_ID"] = discord_main_guild_id.data - return True + return {"discord:main-guild-id"} # TODO: Load more config settings diff --git a/config/_yaml/__init__.py b/config/_yaml/__init__.py new file mode 100644 index 000000000..49fb43ef0 --- /dev/null +++ b/config/_yaml/__init__.py @@ -0,0 +1,221 @@ +from collections.abc import Sequence + +__all__: Sequence[str] = ( + "DiscordWebhookURLValidator", + "LogLevelValidator", + "DiscordSnowflakeValidator", + "ProbabilityValidator", + "SendIntroductionRemindersFlagValidator", + "SETTINGS_YAML_SCHEMA", + "load_yaml", +) + +from collections.abc import Mapping +from typing import Final + +import strictyaml +from strictyaml import YAML + +from ..constants import DEFAULT_STATISTICS_ROLES, TRANSLATED_MESSAGES_LOCALE_CODES +from .custom_validators import ( + DiscordWebhookURLValidator, + LogLevelValidator, + DiscordSnowflakeValidator, + ProbabilityValidator, + SendIntroductionRemindersFlagValidator, + SendIntroductionRemindersFlagType, + LogLevelType, +) +from .custom_schema_utils import SlugKeyMap + + +_DEFAULT_CONSOLE_LOG_LEVEL: Final[LogLevelType] = "INFO" +_DEFAULT_CONSOLE_LOGGING_SETTINGS: Final[Mapping[str, str]] = { + "log-level": _DEFAULT_CONSOLE_LOG_LEVEL, +} +_DEFAULT_LOGGING_SETTINGS: Final[Mapping[str, Mapping[str, str]]] = { + "console": _DEFAULT_CONSOLE_LOGGING_SETTINGS, +} +_DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY: Final[float] = 0.01 +_DEFAULT_PING_COMMAND_SETTINGS: Final[Mapping[str, float]] = { + "easter-egg-probability": _DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY, +} +_DEFAULT_STATS_COMMAND_LOOKBACK_DAYS: Final[float] = 30.0 +_DEFAULT_STATS_COMMAND_DISPLAYED_ROLES: Final[Sequence[str]] = list(DEFAULT_STATISTICS_ROLES) +_DEFAULT_STATS_COMMAND_SETTINGS: Final[Mapping[str, float | Sequence[str]]] = { + "lookback-days": _DEFAULT_STATS_COMMAND_LOOKBACK_DAYS, + "displayed-roles": _DEFAULT_STATS_COMMAND_DISPLAYED_ROLES, +} +_DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION: Final[str] = "24h" +_DEFAULT_STRIKE_COMMAND_MANUAL_USE_WARNING_LOCATION: Final[str] = "DM" +_DEFAULT_STRIKE_COMMAND_SETTINGS: Final[Mapping[str, str]] = { + "timeout-duration": _DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, + "manual-use-warning-location": _DEFAULT_STRIKE_COMMAND_MANUAL_USE_WARNING_LOCATION, +} +_DEFAULT_COMMANDS_SETTINGS: Final[Mapping[str, Mapping[str, float]]] = { + "ping": _DEFAULT_PING_COMMAND_SETTINGS, + "stats": _DEFAULT_STATS_COMMAND_SETTINGS, + "strike": _DEFAULT_STRIKE_COMMAND_SETTINGS, +} +_DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY: Final[str] = "40h" +_DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL: Final[str] = "6h" +_DEFAULT_SEND_INTRODUCTION_REMINDERS_SETTINGS: Final[Mapping[str, SendIntroductionRemindersFlagType | str]] = { + "enable": "once", + "delay": _DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, + "interval": _DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, +} +_DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY: Final[str] = "40h" +_DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL: Final[str] = "6h" +_DEFAULT_SEND_GET_ROLES_REMINDERS_SETTINGS: Final[Mapping[str, bool | str]] = { + "enable": True, + "delay": _DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, + "interval": _DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, +} +_DEFAULT_REMINDERS_SETTINGS: Final[Mapping[str, Mapping[str, bool | str] | Mapping[str, SendIntroductionRemindersFlagType | str]]] = { # noqa: E501 + "send-introduction-reminders": _DEFAULT_SEND_INTRODUCTION_REMINDERS_SETTINGS, + "send-get-roles-reminders": _DEFAULT_SEND_GET_ROLES_REMINDERS_SETTINGS, +} + +SETTINGS_YAML_SCHEMA: Final[strictyaml.Validator] = SlugKeyMap( # type: ignore[no-any-unimported] + { + strictyaml.Optional("logging", default=_DEFAULT_LOGGING_SETTINGS): SlugKeyMap( + { + strictyaml.Optional("console", default=_DEFAULT_CONSOLE_LOGGING_SETTINGS): SlugKeyMap( # noqa: E501 + { + strictyaml.Optional("log-level", default=_DEFAULT_CONSOLE_LOG_LEVEL): ( + LogLevelValidator() + ), + }, + ), + strictyaml.Optional("discord-channel"): SlugKeyMap( + { + "webhook-url": DiscordWebhookURLValidator(), + strictyaml.Optional("log-level", default="WARNING"): ( + LogLevelValidator() + ), + }, + ), + }, + ), + "discord": SlugKeyMap( + { + "bot-token": strictyaml.Regex( + r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", + ), + "main-guild-id": DiscordSnowflakeValidator(), + }, + ), + "community-group": SlugKeyMap( + { + strictyaml.Optional("full-name"): strictyaml.Regex( + r"\A[A-Za-z0-9 '&!?:,.#%\"-]+\Z", + ), + strictyaml.Optional("short-name"): strictyaml.Regex( + r"\A[A-Za-z0-9'&!?:,.#%\"-]+\Z", + ), + strictyaml.Optional("links"): SlugKeyMap( + { + strictyaml.Optional("purchase-membership"): strictyaml.Url(), + strictyaml.Optional("membership-perks"): strictyaml.Url(), + strictyaml.Optional("moderation-document"): strictyaml.Url(), + } + ), + "members-list": SlugKeyMap( + { + "url": strictyaml.Url(), + "auth-session-cookie": strictyaml.Str(), + strictyaml.Optional("id-format", default=r"\A\d{6,7}\Z"): ( + strictyaml.Str() + ), + }, + ), + }, + ), + strictyaml.Optional("commands", default=_DEFAULT_COMMANDS_SETTINGS): SlugKeyMap( + { + strictyaml.Optional("ping", default=_DEFAULT_PING_COMMAND_SETTINGS): SlugKeyMap( # noqa: E501 + { + strictyaml.Optional("easter-egg-probability", default=_DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY): ( # noqa: E501 + ProbabilityValidator() + ), + }, + ), + strictyaml.Optional("stats", default=_DEFAULT_STATS_COMMAND_SETTINGS): ( + SlugKeyMap( + { + strictyaml.Optional("lookback-days", default=_DEFAULT_STATS_COMMAND_LOOKBACK_DAYS): ( # noqa: E501 + strictyaml.Float() + ), + strictyaml.Optional("displayed-roles", default=_DEFAULT_STATS_COMMAND_DISPLAYED_ROLES): ( # noqa: E501 + strictyaml.UniqueSeq(strictyaml.Str()) + ), + }, + ) + ), + strictyaml.Optional("strike", default=_DEFAULT_STRIKE_COMMAND_SETTINGS): ( + SlugKeyMap( + { + strictyaml.Optional("timeout-duration", default=_DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION): ( # noqa: E501 + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + ) + ), + strictyaml.Optional("manual-use-warning-location", default=_DEFAULT_STRIKE_COMMAND_MANUAL_USE_WARNING_LOCATION): ( # noqa: E501 + strictyaml.Str() + ), + }, + ) + ), + }, + ), + strictyaml.Optional("messages-language", default="en-GB"): strictyaml.Enum( + TRANSLATED_MESSAGES_LOCALE_CODES, + ), + strictyaml.Optional("reminders", default=_DEFAULT_REMINDERS_SETTINGS): SlugKeyMap( + { + strictyaml.Optional("send-introduction-reminders", default=_DEFAULT_SEND_INTRODUCTION_REMINDERS_SETTINGS): SlugKeyMap( # noqa: E501 + { + "enable": SendIntroductionRemindersFlagValidator(), + strictyaml.Optional("delay", default=_DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY): ( # noqa: E501 + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + ) + ), + strictyaml.Optional("interval", default=_DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL): ( # noqa: E501 + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", + ) + ), + }, + ), + strictyaml.Optional("send-get-roles-reminders", default=_DEFAULT_SEND_GET_ROLES_REMINDERS_SETTINGS): SlugKeyMap( # noqa: E501 + { + "enable": strictyaml.Bool(), + strictyaml.Optional("delay", default=_DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY): ( # noqa: E501 + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + ) + ), + strictyaml.Optional("interval", default=_DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL): ( # noqa: E501 + strictyaml.Regex( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", + ) + ), + }, + ), + }, + ), + }, +) + + +def load_yaml(raw_yaml: str) -> YAML: + parsed_yaml: YAML = strictyaml.load(raw_yaml, SETTINGS_YAML_SCHEMA) # type: ignore[no-any-unimported] + + # noinspection SpellCheckingInspection + if "guildofstudents" in parsed_yaml["community-group"]["members-list"]["url"]: + parsed_yaml["community-group"]["members-list"]["auth-session-cookie"].revalidate( + strictyaml.Regex(r"\A[A-Fa-f\d]{128,256}\Z"), + ) + + return parsed_yaml diff --git a/config/_yaml/custom_schema_utils.py b/config/_yaml/custom_schema_utils.py new file mode 100644 index 000000000..3a5b722bd --- /dev/null +++ b/config/_yaml/custom_schema_utils.py @@ -0,0 +1,25 @@ +from collections.abc import Sequence + +__all__: Sequence[str] = ("SlugKeyValidator", "SlugKeyMap") + +from typing import override + +import slugify +import strictyaml + +from strictyaml.yamllocation import YAMLChunk + + +class SlugKeyValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] + @override + def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimported,misc] + return slugify.slugify(str(chunk.contents)) + + +class SlugKeyMap(strictyaml.Map): + @override + def __init__(self, validator: dict[object, object], key_validator: strictyaml.Validator | None = None) -> None: # noqa: E501 + super().__init__( + validator=validator, + key_validator=key_validator if key_validator is not None else SlugKeyValidator() + ) diff --git a/config/_yaml.py b/config/_yaml/custom_validators.py similarity index 56% rename from config/_yaml.py rename to config/_yaml/custom_validators.py index 0832087d7..c4dd5e18e 100644 --- a/config/_yaml.py +++ b/config/_yaml/custom_validators.py @@ -1,42 +1,32 @@ from collections.abc import Sequence __all__: Sequence[str] = ( - "SlugValidator", "DiscordWebhookURLValidator", - "SETTINGS_YAML_SCHEMA", - "load_yaml", + "LogLevelValidator", + "DiscordSnowflakeValidator", + "ProbabilityValidator", + "SendIntroductionRemindersFlagValidator", + "SendIntroductionRemindersFlagType", + "LogLevelType", ) import math import re -from typing import Final, Literal, TypeAlias, override +from typing import Final, override, Literal, TypeAlias -import slugify import strictyaml -from strictyaml import YAML from strictyaml import constants as strictyaml_constants from strictyaml import utils as strictyaml_utils from strictyaml.exceptions import YAMLSerializationError from strictyaml.yamllocation import YAMLChunk -from .constants import ( - DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL, - DEFAULT_STATISTICS_ROLES, - LOG_LEVELS, - TRANSLATED_MESSAGES_LOCALE_CODES, - VALID_SEND_INTRODUCTION_REMINDERS_VALUES, -) +from ..constants import LOG_LEVELS, VALID_SEND_INTRODUCTION_REMINDERS_VALUES + SendIntroductionRemindersFlagType: TypeAlias = Literal["once", "interval", False] LogLevelType: TypeAlias = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] -class SlugValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] - @override - def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimported,misc] - return slugify.slugify(str(chunk.contents)) - - class LogLevelValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] @override def validate_scalar(self, chunk: YAMLChunk) -> LogLevelType: # type: ignore[no-any-unimported,misc] @@ -213,92 +203,3 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return "False" return data.title() - - -SETTINGS_YAML_SCHEMA: Final[strictyaml.Map] = strictyaml.Map( # type: ignore[no-any-unimported] - { - strictyaml.Optional("console-log-level", default="INFO"): LogLevelValidator(), - strictyaml.Optional("discord-log-channel-log-level", default=DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL): ( # noqa: E501 - LogLevelValidator() - ), - strictyaml.Optional("discord-log-channel-webhook-url"): DiscordWebhookURLValidator(), - "discord-bot-token": strictyaml.Regex( - r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", - ), - "discord-guild-id": DiscordSnowflakeValidator(), - strictyaml.Optional("group-full-name"): strictyaml.Regex( - r"\A[A-Za-z0-9 '&!?:,.#%\"-]+\Z", - ), - strictyaml.Optional("group-short-name"): strictyaml.Regex( - r"\A[A-Za-z0-9'&!?:,.#%\"-]+\Z", - ), - strictyaml.Optional("purchase-membership-url"): strictyaml.Url(), - strictyaml.Optional("membership-perks-url"): strictyaml.Url(), - strictyaml.Optional("ping-command-easter-egg-probability", default=0.01): ( - ProbabilityValidator() - ), - strictyaml.Optional("messages-language", default="en-GB"): strictyaml.Enum( - TRANSLATED_MESSAGES_LOCALE_CODES, - ), - "members-list-url": strictyaml.Url(), - "members-list-url-session-cookie": strictyaml.Str(), - strictyaml.Optional("send-introduction-reminders", default="once"): ( - SendIntroductionRemindersFlagValidator() - ), - strictyaml.Optional("send-introduction-reminders-delay", default="40h"): ( - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - ) - ), - strictyaml.Optional("send-introduction-reminders-interval", default="6h"): ( - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - ) - ), - strictyaml.Optional("send-get-roles-reminders", default=True): strictyaml.Bool(), - strictyaml.Optional("send-get-roles-reminders-delay", default="40h"): ( - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - ) - ), - strictyaml.Optional("statistics-days", default=30.0): strictyaml.Float(), - strictyaml.Optional("statistics-roles", default=list(DEFAULT_STATISTICS_ROLES)): ( - strictyaml.UniqueSeq(strictyaml.Str()) - ), - "moderation-document-url": strictyaml.Url(), - strictyaml.Optional("manual-moderation-warning-message-location", default="DM"): ( - strictyaml.Str() - ), - strictyaml.Optional("strike-timeout-duration", default="24h"): strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - ), - strictyaml.Optional("group-member-id-format", default=r"\A\d{7}\Z"): ( - strictyaml.Str() - ), - strictyaml.Optional("advanced", default={}): strictyaml.EmptyDict() | ( - strictyaml.Map( - { - strictyaml.Optional("send-get-roles-reminders-interval", default="6h"): ( - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - ) - ), - }, - key_validator=SlugValidator(), - ) - ), - }, - key_validator=SlugValidator(), -) - - -def load_yaml(raw_yaml: str) -> YAML: - parsed_yaml: YAML = strictyaml.load(raw_yaml, SETTINGS_YAML_SCHEMA) # type: ignore[no-any-unimported] - - # noinspection SpellCheckingInspection - if "guildofstudents" in parsed_yaml["members-list-url"]: - parsed_yaml["members-list-url-session-cookie"].revalidate( - strictyaml.Regex(r"\A[A-Fa-f\d]{128,256}\Z"), - ) - - return parsed_yaml diff --git a/config/constants.py b/config/constants.py index 508c140cf..ae71d600e 100644 --- a/config/constants.py +++ b/config/constants.py @@ -8,8 +8,8 @@ "TRANSLATED_MESSAGES_LOCALE_CODES", "DEFAULT_STATISTICS_ROLES", "LOG_LEVELS", - "REQUIRES_RESTART_SETTINGS_KEYS", - "DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL", + "REQUIRES_RESTART_SETTINGS", + "DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME", ) @@ -56,17 +56,17 @@ "CRITICAL", ) -REQUIRES_RESTART_SETTINGS_KEYS: Final[frozenset[str]] = frozenset( +REQUIRES_RESTART_SETTINGS: Final[frozenset[str]] = frozenset( { - "discord-bot-token", - "discord-guild-id", - "send-introduction-reminders", - "send-introduction-reminders-delay", - "send-introduction-reminders-interval", - "send-get-roles-reminders", - "send-get-roles-reminders-delay", - "send-get-roles-reminders-interval", + "discord:bot-token", + "discord:guild-id", + "reminders:send-introduction-reminders:enable", + "reminders:send-introduction-reminders:delay", + "reminders:send-introduction-reminders:interval", + "reminders:send-get-roles-reminders:enable", + "reminders:send-get-roles-reminders:delay", + "reminders:send-get-roles-reminders:interval", }, ) -DEFAULT_DISCORD_LOG_CHANNEL_LOG_LEVEL: str = "WARNING" +DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME: Final[str] = "TeX-Bot" diff --git a/utils/__init__.py b/utils/__init__.py index 8eca5640e..d71ef2cee 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -17,16 +17,16 @@ ) -import asyncio - 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, TeXBotExitReason -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 MessageSenderComponent +from .suppress_traceback import SuppressTraceback +from .tex_bot import TeXBot, TeXBotExitReason +from .tex_bot_base_cog import TeXBotBaseCog +from .tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext +# noinspection PyProtectedMember +from config._pre_startup_utils import is_running_in_async def generate_invite_url(discord_bot_application_id: int, discord_guild_id: int) -> str: @@ -65,13 +65,3 @@ def is_member_inducted(member: discord.Member) -> bool: return any( role.name.lower().strip().strip("@").strip() not in ("news",) for role in member.roles ) - - -def is_running_in_async() -> bool: - """Determine whether the current context is asynchronous or not.""" - try: - asyncio.get_running_loop() - except RuntimeError: - return False - else: - return True From 36ccf6fdaf629a589583e6ddac1ad2e83a425a4d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 24 May 2024 02:35:05 +0100 Subject: [PATCH 009/128] Add more config loading --- cogs/make_member.py | 32 ++++++++++----------- cogs/startup.py | 10 ++++--- config/__init__.py | 2 ++ config/_settings.py | 70 +++++++++++++++++++++++++++++++-------------- utils/tex_bot.py | 4 +-- 5 files changed, 75 insertions(+), 43 deletions(-) diff --git a/cogs/make_member.py b/cogs/make_member.py index b53ab4974..9c740b755 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -28,16 +28,16 @@ f"""{ "Student" if ( - settings["_GROUP_FULL_NAME"] + settings["_COMMUNITY_GROUP_FULL_NAME"] and ( - "computer science society" in settings["_GROUP_FULL_NAME"].lower() - or "css" in settings["_GROUP_FULL_NAME"].lower() - or "uob" in settings["_GROUP_FULL_NAME"].lower() - or "university of birmingham" in settings["_GROUP_FULL_NAME"].lower() - or "uob" in settings["_GROUP_FULL_NAME"].lower() + "computer science society" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + or "css" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + or "uob" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + or "university of birmingham" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + or "uob" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() or ( - "bham" in settings["_GROUP_FULL_NAME"].lower() - and "uni" in settings["_GROUP_FULL_NAME"].lower() + "bham" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + and "uni" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() ) ) ) @@ -71,16 +71,16 @@ class MakeMemberCommandCog(TeXBotBaseCog): f"""Your UoB Student { "UoB Student" if ( - settings["_GROUP_FULL_NAME"] + settings["_COMMUNITY_GROUP_FULL_NAME"] and ( - "computer science society" in settings["_GROUP_FULL_NAME"].lower() - or "css" in settings["_GROUP_FULL_NAME"].lower() - or "uob" in settings["_GROUP_FULL_NAME"].lower() - or "university of birmingham" in settings["_GROUP_FULL_NAME"].lower() - or "uob" in settings["_GROUP_FULL_NAME"].lower() + "computer science society" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + or "css" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + or "uob" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + or "university of birmingham" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + or "uob" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() or ( - "bham" in settings["_GROUP_FULL_NAME"].lower() - and "uni" in settings["_GROUP_FULL_NAME"].lower() + "bham" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + and "uni" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() ) ) ) diff --git a/cogs/startup.py b/cogs/startup.py index 9198c00c4..4eb0e3b1c 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -90,7 +90,7 @@ async def _get_main_guild(self) -> discord.Guild: try: main_guild: discord.Guild | None = self.bot.main_guild except GuildDoesNotExistError: - main_guild = self.bot.get_guild(settings["DISCORD_MAIN_GUILD_ID"]) + main_guild = self.bot.get_guild(settings["_DISCORD_MAIN_GUILD_ID"]) if main_guild: self.bot.set_main_guild(main_guild) @@ -100,9 +100,11 @@ async def _get_main_guild(self) -> discord.Guild: "Invite URL: %s", utils.generate_invite_url( self.bot.application_id, - settings["DISCORD_MAIN_GUILD_ID"]), + settings["_DISCORD_MAIN_GUILD_ID"]), ) - logger.critical(GuildDoesNotExistError(guild_id=settings["DISCORD_MAIN_GUILD_ID"])) + logger.critical(GuildDoesNotExistError( + guild_id=settings["_DISCORD_MAIN_GUILD_ID"]) + ) await self.bot.close() raise RuntimeError @@ -124,7 +126,7 @@ async def on_ready(self) -> None: "Invite URL: %s", utils.generate_invite_url( self.bot.application_id, - settings["DISCORD_MAIN_GUILD_ID"]), + settings["_DISCORD_MAIN_GUILD_ID"]), ) if not discord.utils.get(main_guild.roles, name="Committee"): # TODO: Move to separate functions diff --git a/config/__init__.py b/config/__init__.py index 9cfc52271..e0d76daf7 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -76,10 +76,12 @@ def check_for_deprecated_environment_variables() -> None: "DISCORD_GUILD_ID", "DISCORD_LOG_CHANNEL_WEBHOOK_URL", "GROUP_NAME", + "GROUP_FULL_NAME", "GROUP_SHORT_NAME", "PURCHASE_MEMBERSHIP_URL", "CONSOLE_LOG_LEVEL", "MEMBERS_LIST_URL", + "MEMBERS_LIST", "MEMBERS_LIST_URL_SESSION_COOKIE", "PING_COMMAND_EASTER_EGG_PROBABILITY", "MESSAGES_FILE_PATH", diff --git a/config/_settings.py b/config/_settings.py index 5c9961b9f..6e061c263 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -106,28 +106,28 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) + if not re.match(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item): + raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) + if self._most_recent_yaml is None: with contextlib.suppress(BotRequiresRestartAfterConfigChange): self.reload() - if item in self._settings: - ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: Final[bool] = bool( - "bot" in item.lower() - and "token" in item.lower() - and is_running_in_async() - ) - if ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: - raise RuntimeError(f"Cannot access {item!r} when TeX-Bot is already running.") - - return self._settings[item] - - if re.match(r"\A[A-Z](?:[A-Z_]*[A-Z])?\Z", item): + if item not in self._settings: INVALID_SETTINGS_KEY_MESSAGE: Final[str] = ( self.format_invalid_settings_key_message(item) ) raise AttributeError(INVALID_SETTINGS_KEY_MESSAGE) - raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) + ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: Final[bool] = bool( + "bot" in item.lower() + and "token" in item.lower() + and is_running_in_async() + ) + if ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: + raise RuntimeError(f"Cannot access {item!r} when TeX-Bot is already running.") + + return self._settings[item] def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 """Retrieve settings value by key lookup.""" @@ -163,6 +163,9 @@ def reload(cls) -> None: ), cls._reload_discord_bot_token(current_yaml["discord"]["bot-token"]), cls._reload_discord_main_guild_id(current_yaml["discord"]["main-guild-id"]), + cls._reload_community_group_full_name( + current_yaml["community-group"].get("full-name", None), + ), ) cls._most_recent_yaml = current_yaml @@ -237,12 +240,11 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y if not DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: return set() - if discord_channel_logging_settings is None: - cls._settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"] = None - else: - cls._settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"] = ( - discord_channel_logging_settings["webhook_url"].data - ) + cls._settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"] = ( + discord_channel_logging_settings + if discord_channel_logging_settings is None + else discord_channel_logging_settings["webhook_url"].data + ) discord_logging_handlers: set[DiscordHandler] = { handler for handler in logger.handlers if isinstance(handler, DiscordHandler) @@ -353,13 +355,39 @@ def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: DISCORD_MAIN_GUILD_ID_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_main_guild_id != cls._most_recent_yaml["discord"]["main-guild-id"] - or "DISCORD_MAIN_GUILD_ID" not in cls._settings + or "_DISCORD_MAIN_GUILD_ID" not in cls._settings ) if not DISCORD_MAIN_GUILD_ID_CHANGED: return set() - cls._settings["DISCORD_MAIN_GUILD_ID"] = discord_main_guild_id.data + cls._settings["_DISCORD_MAIN_GUILD_ID"] = discord_main_guild_id.data return {"discord:main-guild-id"} + @classmethod + def _reload_community_group_full_name(cls, community_group_full_name: YAML | None) -> set[str]: + """ + Reload the community-group full name. + + Returns the set of settings keys that have been changed. + """ + COMMUNITY_GROUP_FULL_NAME_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or community_group_full_name != cls._most_recent_yaml["community-group"].get( + "full-name", + None, + ) + or "_COMMUNITY_GROUP_FULL_NAME" not in cls._settings + ) + if not COMMUNITY_GROUP_FULL_NAME_CHANGED: + return set() + + cls._settings["_COMMUNITY_GROUP_FULL_NAME"] = ( + community_group_full_name + if community_group_full_name is None + else community_group_full_name.data + ) + + return {"community-group:full-name"} + # TODO: Load more config settings diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 7d5567f6e..4b57c6f12 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -249,8 +249,8 @@ def group_full_name(self) -> str: or automatically identified from the name of your group's Discord guild. """ return ( # type: ignore[no-any-return] - settings["_GROUP_FULL_NAME"] - if settings["_GROUP_FULL_NAME"] + settings["_COMMUNITY_GROUP_FULL_NAME"] + if settings["_COMMUNITY_GROUP_FULL_NAME"] else ( "The Computer Science Society" if ( From 91cb156ef48e8d34b4feab4e9eb71c8cd6d68543 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 25 May 2024 01:05:17 +0100 Subject: [PATCH 010/128] Add purchase membership link setting --- cogs/induct.py | 10 +++--- cogs/make_member.py | 32 ++++++++--------- config/__init__.py | 18 ++++++++++ config/_settings.py | 77 ++++++++++++++++++++++++++++++++++------ config/_yaml/__init__.py | 4 +-- messages.json | 2 +- utils/tex_bot.py | 4 +-- 7 files changed, 111 insertions(+), 36 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index 49751c199..07876fabc 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -112,7 +112,7 @@ async def on_member_update(self, before: discord.Member, after: discord.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"""Just head to {settings["PURCHASE_MEMBERSHIP_LINK"]}. """ "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" @@ -153,13 +153,13 @@ async def get_random_welcome_message(self, induction_member: discord.User | disc committee_role_mention, ) - if "" in random_welcome_message: - if not settings["PURCHASE_MEMBERSHIP_URL"]: + if "" in random_welcome_message: + if not settings["PURCHASE_MEMBERSHIP_LINK"]: return await self.get_random_welcome_message(induction_member) random_welcome_message = random_welcome_message.replace( - "", - settings["PURCHASE_MEMBERSHIP_URL"], + "", + settings["PURCHASE_MEMBERSHIP_LINK"], ) if "" in random_welcome_message: diff --git a/cogs/make_member.py b/cogs/make_member.py index 9c740b755..b53ab4974 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -28,16 +28,16 @@ f"""{ "Student" if ( - settings["_COMMUNITY_GROUP_FULL_NAME"] + settings["_GROUP_FULL_NAME"] and ( - "computer science society" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - or "css" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - or "uob" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - or "university of birmingham" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - or "uob" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + "computer science society" in settings["_GROUP_FULL_NAME"].lower() + or "css" in settings["_GROUP_FULL_NAME"].lower() + or "uob" in settings["_GROUP_FULL_NAME"].lower() + or "university of birmingham" in settings["_GROUP_FULL_NAME"].lower() + or "uob" in settings["_GROUP_FULL_NAME"].lower() or ( - "bham" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - and "uni" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + "bham" in settings["_GROUP_FULL_NAME"].lower() + and "uni" in settings["_GROUP_FULL_NAME"].lower() ) ) ) @@ -71,16 +71,16 @@ class MakeMemberCommandCog(TeXBotBaseCog): f"""Your UoB Student { "UoB Student" if ( - settings["_COMMUNITY_GROUP_FULL_NAME"] + settings["_GROUP_FULL_NAME"] and ( - "computer science society" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - or "css" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - or "uob" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - or "university of birmingham" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - or "uob" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + "computer science society" in settings["_GROUP_FULL_NAME"].lower() + or "css" in settings["_GROUP_FULL_NAME"].lower() + or "uob" in settings["_GROUP_FULL_NAME"].lower() + or "university of birmingham" in settings["_GROUP_FULL_NAME"].lower() + or "uob" in settings["_GROUP_FULL_NAME"].lower() or ( - "bham" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() - and "uni" in settings["_COMMUNITY_GROUP_FULL_NAME"].lower() + "bham" in settings["_GROUP_FULL_NAME"].lower() + and "uni" in settings["_GROUP_FULL_NAME"].lower() ) ) ) diff --git a/config/__init__.py b/config/__init__.py index e0d76daf7..40dddae70 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -73,18 +73,31 @@ def check_for_deprecated_environment_variables() -> None: DEPRECATED_ENVIRONMENT_VARIABLE_NAMES: Final[Iterable[str]] = ( "DISCORD_BOT_TOKEN", + "BOT_TOKEN", + "DISCORD_TOKEN", "DISCORD_GUILD_ID", + "DISCORD_MAIN_GUILD_ID", + "MAIN_GUILD_ID", + "GUILD_ID", "DISCORD_LOG_CHANNEL_WEBHOOK_URL", + "DISCORD_LOG_CHANNEL_WEBHOOK", + "DISCORD_LOGGING_WEBHOOK_URL", + "DISCORD_LOGGING_WEBHOOK", + "DISCORD_LOG_CHANNEL_LOCATION", "GROUP_NAME", "GROUP_FULL_NAME", "GROUP_SHORT_NAME", "PURCHASE_MEMBERSHIP_URL", + "PURCHASE_MEMBERSHIP_LINK", "CONSOLE_LOG_LEVEL", "MEMBERS_LIST_URL", + "MEMBERS_LIST_LIST", "MEMBERS_LIST", "MEMBERS_LIST_URL_SESSION_COOKIE", "PING_COMMAND_EASTER_EGG_PROBABILITY", + "PING_EASTER_EGG_PROBABILITY", "MESSAGES_FILE_PATH", + "MESSAGES_FILE", "SEND_INTRODUCTION_REMINDERS", "SEND_INTRODUCTION_REMINDERS_DELAY", "SEND_INTRODUCTION_REMINDERS_INTERVAL", @@ -94,8 +107,13 @@ def check_for_deprecated_environment_variables() -> None: "ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL", "STATISTICS_DAYS", "STATISTICS_ROLES", + "STATS_DAYS", + "STATS_ROLES", "MODERATION_DOCUMENT_URL", + "MODERATION_DOCUMENT_LINK", + "MODERATION_DOCUMENT", "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", + "MANUAL_MODERATION_MESSAGE_LOCATION", ) deprecated_environment_variable_name: str for deprecated_environment_variable_name in DEPRECATED_ENVIRONMENT_VARIABLE_NAMES: diff --git a/config/_settings.py b/config/_settings.py index 6e061c263..fc197aa6f 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -163,9 +163,15 @@ def reload(cls) -> None: ), cls._reload_discord_bot_token(current_yaml["discord"]["bot-token"]), cls._reload_discord_main_guild_id(current_yaml["discord"]["main-guild-id"]), - cls._reload_community_group_full_name( + cls._reload_group_full_name( current_yaml["community-group"].get("full-name", None), ), + cls._reload_group_short_name( + current_yaml["community-group"].get("short-name", None), + ), + cls._reload_purchase_membership_link( + current_yaml["community-group"]["links"].get("purchase-membership"), + ) ) cls._most_recent_yaml = current_yaml @@ -365,29 +371,80 @@ def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: return {"discord:main-guild-id"} @classmethod - def _reload_community_group_full_name(cls, community_group_full_name: YAML | None) -> set[str]: + def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: """ Reload the community-group full name. Returns the set of settings keys that have been changed. """ - COMMUNITY_GROUP_FULL_NAME_CHANGED: Final[bool] = bool( + GROUP_FULL_NAME_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or community_group_full_name != cls._most_recent_yaml["community-group"].get( + or group_full_name != cls._most_recent_yaml["community-group"].get( "full-name", None, ) - or "_COMMUNITY_GROUP_FULL_NAME" not in cls._settings + or "_GROUP_FULL_NAME" not in cls._settings ) - if not COMMUNITY_GROUP_FULL_NAME_CHANGED: + if not GROUP_FULL_NAME_CHANGED: return set() - cls._settings["_COMMUNITY_GROUP_FULL_NAME"] = ( - community_group_full_name - if community_group_full_name is None - else community_group_full_name.data + cls._settings["_GROUP_FULL_NAME"] = ( + group_full_name + if group_full_name is None + else group_full_name.data ) return {"community-group:full-name"} + @classmethod + def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: + """ + Reload the community-group short name. + + Returns the set of settings keys that have been changed. + """ + GROUP_SHORT_NAME_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or group_short_name != cls._most_recent_yaml["community-group"].get( + "short-name", + None, + ) + or "_GROUP_SHORT_NAME" not in cls._settings + ) + if not GROUP_SHORT_NAME_CHANGED: + return set() + + cls._settings["_GROUP_SHORT_NAME"] = ( + group_short_name + if group_short_name is None + else group_short_name.data + ) + + return {"community-group:short-name"} + + @classmethod + def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) -> set[str]: + """ + Reload the link to allow people to purchase a membership. + + Returns the set of settings keys that have been changed. + """ + PURCHASE_MEMBERSHIP_LINK_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or purchase_membership_link != cls._most_recent_yaml["community-group"][ + "links" + ].get("purchase-membership", None) + or "PURCHASE_MEMBERSHIP_LINK" not in cls._settings + ) + if not PURCHASE_MEMBERSHIP_LINK_CHANGED: + return set() + + cls._settings["PURCHASE_MEMBERSHIP_LINK"] = ( + purchase_membership_link + if purchase_membership_link is None + else purchase_membership_link.data + ) + + return {"community-group:links:purchase-membership"} + # TODO: Load more config settings diff --git a/config/_yaml/__init__.py b/config/_yaml/__init__.py index 49fb43ef0..f5620494f 100644 --- a/config/_yaml/__init__.py +++ b/config/_yaml/__init__.py @@ -113,11 +113,11 @@ strictyaml.Optional("short-name"): strictyaml.Regex( r"\A[A-Za-z0-9'&!?:,.#%\"-]+\Z", ), - strictyaml.Optional("links"): SlugKeyMap( + "links": SlugKeyMap( { strictyaml.Optional("purchase-membership"): strictyaml.Url(), strictyaml.Optional("membership-perks"): strictyaml.Url(), - strictyaml.Optional("moderation-document"): strictyaml.Url(), + "moderation-document": strictyaml.Url(), } ), "members-list": SlugKeyMap( diff --git a/messages.json b/messages.json index 2bfc30cd3..ce1a07bd8 100644 --- a/messages.json +++ b/messages.json @@ -15,7 +15,7 @@ " is here to kick butt and chew bubblegum. And is all out of gum.", "A Golden Spirit, blessed with radiance of justice. I saw it within and their friends... As long as it's there, they'll be fine.", "Behold, the saviour of all existence, !", - "? More like <>", + "? More like <>", "I think took a wrong turn to Cavern, so we pointed them here!", " used to be an adventurer until they took an arrow to the knee.", "Their name is, their name is, their name is !", diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 4b57c6f12..7d5567f6e 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -249,8 +249,8 @@ def group_full_name(self) -> str: or automatically identified from the name of your group's Discord guild. """ return ( # type: ignore[no-any-return] - settings["_COMMUNITY_GROUP_FULL_NAME"] - if settings["_COMMUNITY_GROUP_FULL_NAME"] + settings["_GROUP_FULL_NAME"] + if settings["_GROUP_FULL_NAME"] else ( "The Computer Science Society" if ( From 5f75465c3df40e9fc3b8c54a5a596cf93a922fed Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 25 May 2024 14:13:28 +0100 Subject: [PATCH 011/128] Add more config loading --- cogs/kill.py | 1 + cogs/send_get_roles_reminders.py | 2 +- config/__init__.py | 2 +- config/_settings.py | 68 ++++++++++++++++++++++++++++--- config/_yaml/__init__.py | 13 +++--- config/_yaml/custom_validators.py | 31 ++++++++++++++ 6 files changed, 103 insertions(+), 14 deletions(-) diff --git a/cogs/kill.py b/cogs/kill.py index accc57ae3..33170fcce 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 diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 617e0def9..8faad07f4 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -49,7 +49,7 @@ def cog_unload(self) -> None: """ self.send_get_roles_reminders.cancel() - @tasks.loop(**settings["ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL"]) # type: ignore[misc] + @tasks.loop(**settings["SEND_GET_ROLES_REMINDERS_INTERVAL"]) # type: ignore[misc] @functools.partial( ErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError, diff --git a/config/__init__.py b/config/__init__.py index 40dddae70..5ac9def94 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -63,7 +63,7 @@ def check_for_deprecated_environment_variables() -> None: DeprecationWarning( ( "Configuration using environment variables is deprecated. " - "Use a `TeX-Bot-deployment.yaml` file instead." + "Use a `tex-bot-deployment.yaml` file instead." ), ) ) diff --git a/config/_settings.py b/config/_settings.py index fc197aa6f..89e8a5d28 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -37,7 +37,7 @@ def _get_settings_file_path() -> Path: settings_file_not_found_message: str = ( "No settings file was found. " - "Please make sure you have created a `TeX-Bot-deployment.yaml` file." + "Please make sure you have created a `tex-bot-deployment.yaml` file." ) raw_settings_file_path: str | None = ( @@ -67,12 +67,12 @@ def _get_settings_file_path() -> Path: "falling back to `Tex-Bot-deployment.yaml`." ), ) - raw_settings_file_path = "TeX-Bot-deployment.yaml" + raw_settings_file_path = "tex-bot-deployment.yaml" if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): - raw_settings_file_path = "TeX-Bot-settings.yaml" + raw_settings_file_path = "tex-bot-settings.yaml" if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): - raw_settings_file_path = "TeX-Bot-config.yaml" + raw_settings_file_path = "tex-bot-config.yaml" settings_file_path: Path = Path(raw_settings_file_path) @@ -149,7 +149,11 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 @classmethod def reload(cls) -> None: - current_yaml: YAML = load_yaml(_get_settings_file_path().read_text()) # type: ignore[no-any-unimported] + settings_file_path: Path = _get_settings_file_path() + current_yaml: YAML = load_yaml( # type: ignore[no-any-unimported] + settings_file_path.read_text(), + file_name=settings_file_path.name, + ) if current_yaml == cls._most_recent_yaml: return @@ -171,7 +175,13 @@ def reload(cls) -> None: ), cls._reload_purchase_membership_link( current_yaml["community-group"]["links"].get("purchase-membership"), - ) + ), + cls._reload_membership_perks_link( + current_yaml["community-group"]["links"].get("membership-perks"), + ), + cls._reload_moderation_document_link( + current_yaml["community-group"]["links"]["moderation-document"], + ), ) cls._most_recent_yaml = current_yaml @@ -447,4 +457,50 @@ def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) return {"community-group:links:purchase-membership"} + @classmethod + def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> set[str]: + """ + Reload the link to view the perks of getting a membership to join your community group. + + Returns the set of settings keys that have been changed. + """ + MEMBERSHIP_PERKS_LINK_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or membership_perks_link != cls._most_recent_yaml["community-group"][ + "links" + ].get("membership-perks", None) + or "MEMBERSHIP_PERKS_LINK" not in cls._settings + ) + if not MEMBERSHIP_PERKS_LINK_CHANGED: + return set() + + cls._settings["MEMBERSHIP_PERKS_LINK"] = ( + membership_perks_link + if membership_perks_link is None + else membership_perks_link.data + ) + + return {"community-group:links:membership-perks"} + + @classmethod + def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set[str]: + """ + Reload the link to view your community group's moderation document. + + Returns the set of settings keys that have been changed. + """ + MODERATION_DOCUMENT_LINK_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or moderation_document_link != cls._most_recent_yaml["community-group"]["links"][ + "moderation-document" + ] + or "MODERATION_DOCUMENT_LINK" not in cls._settings + ) + if not MODERATION_DOCUMENT_LINK_CHANGED: + return set() + + cls._settings["MODERATION_DOCUMENT_LINK"] = moderation_document_link.data + + return {"community-group:links:moderation-document"} + # TODO: Load more config settings diff --git a/config/_yaml/__init__.py b/config/_yaml/__init__.py index f5620494f..a141bf08e 100644 --- a/config/_yaml/__init__.py +++ b/config/_yaml/__init__.py @@ -20,6 +20,7 @@ from .custom_validators import ( DiscordWebhookURLValidator, LogLevelValidator, + RegexMatcher, DiscordSnowflakeValidator, ProbabilityValidator, SendIntroductionRemindersFlagValidator, @@ -100,7 +101,7 @@ "discord": SlugKeyMap( { "bot-token": strictyaml.Regex( - r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", + r"\A(?!.*__.*)(?!.*--.*)(?:([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38}))\Z", ), "main-guild-id": DiscordSnowflakeValidator(), }, @@ -108,10 +109,10 @@ "community-group": SlugKeyMap( { strictyaml.Optional("full-name"): strictyaml.Regex( - r"\A[A-Za-z0-9 '&!?:,.#%\"-]+\Z", + r"\A(?!.*['&!?:,.#%\"-]['&!?:,.#%\"-].*)(?!.* .*)(?:[A-Za-z0-9 '&!?:,.#%\"-]+)\Z", ), strictyaml.Optional("short-name"): strictyaml.Regex( - r"\A[A-Za-z0-9'&!?:,.#%\"-]+\Z", + r"\A(?!.*['&!?:,.#%\"-]['&!?:,.#%\"-].*)(?:[A-Za-z0-9'&!?:,.#%\"-]+)\Z", ), "links": SlugKeyMap( { @@ -125,7 +126,7 @@ "url": strictyaml.Url(), "auth-session-cookie": strictyaml.Str(), strictyaml.Optional("id-format", default=r"\A\d{6,7}\Z"): ( - strictyaml.Str() + RegexMatcher() ), }, ), @@ -209,8 +210,8 @@ ) -def load_yaml(raw_yaml: str) -> YAML: - parsed_yaml: YAML = strictyaml.load(raw_yaml, SETTINGS_YAML_SCHEMA) # type: ignore[no-any-unimported] +def load_yaml(raw_yaml: str, file_name: str = "tex-bot-deployment.yaml") -> YAML: + parsed_yaml: YAML = strictyaml.load(raw_yaml, SETTINGS_YAML_SCHEMA, label=file_name) # type: ignore[no-any-unimported] # noinspection SpellCheckingInspection if "guildofstudents" in parsed_yaml["community-group"]["members-list"]["url"]: diff --git a/config/_yaml/custom_validators.py b/config/_yaml/custom_validators.py index c4dd5e18e..8ec45e7bd 100644 --- a/config/_yaml/custom_validators.py +++ b/config/_yaml/custom_validators.py @@ -4,6 +4,7 @@ "DiscordWebhookURLValidator", "LogLevelValidator", "DiscordSnowflakeValidator", + "RegexMatcher", "ProbabilityValidator", "SendIntroductionRemindersFlagValidator", "SendIntroductionRemindersFlagType", @@ -116,6 +117,36 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return str(data) +class RegexMatcher(strictyaml.ScalarValidator): + MATCHING_MESSAGE: str = "when expecting a regular expression matcher" + + @override + def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimported,misc] + try: + re.compile(chunk.contents) + except re.error: + chunk.expecting_but_found( + self.MATCHING_MESSAGE, + "found arbitrary string", + ) + else: + return chunk.contents + + # noinspection PyOverrides + @override + def to_yaml(self, data: object) -> str: # type: ignore[misc] + self.should_be_string(data, self.MATCHING_MESSAGE) + if not isinstance(data, str): + raise TypeError + + try: + re.compile(data) + except re.error: + raise YAMLSerializationError(f"{self.MATCHING_MESSAGE} found '{data}'") + else: + return data + + class ProbabilityValidator(strictyaml.Float): # type: ignore[no-any-unimported,misc] @override def validate_scalar(self, chunk: YAMLChunk) -> float: # type: ignore[no-any-unimported,misc] From e38e1c8837b64be06ede68d9c4e1c646f610d039 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 03:10:33 +0100 Subject: [PATCH 012/128] Add final configuration loading --- cogs/induct.py | 6 +- cogs/make_member.py | 4 +- cogs/ping.py | 2 +- cogs/send_get_roles_reminders.py | 6 +- cogs/send_introduction_reminders.py | 12 +- cogs/source.py | 5 +- cogs/startup.py | 16 +- cogs/strike.py | 12 +- config/__init__.py | 19 +- config/_settings.py | 386 +++++++++++++++++++++++++++- config/_yaml/__init__.py | 125 ++++----- config/_yaml/custom_schema_utils.py | 1 + config/_yaml/custom_validators.py | 148 ++++++++--- config/constants.py | 115 ++++++--- tests/test_utils.py | 6 +- utils/tex_bot.py | 4 +- 16 files changed, 695 insertions(+), 172 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index 07876fabc..05d2f4173 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -82,12 +82,12 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) ) # 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 # 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 @@ -206,7 +206,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb general_channel: discord.TextChannel = await self.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 diff --git a/cogs/make_member.py b/cogs/make_member.py index b53ab4974..2ecc82cc3 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -116,7 +116,7 @@ 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.match(settings["MEMBERS_LIST_ID_FORMAT"], group_member_id): await self.command_send_error( ctx, message=( @@ -155,7 +155,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: 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/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 8faad07f4..393be41cd 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -36,7 +36,7 @@ class SendGetRolesRemindersTaskCog(TeXBotBaseCog): def __init__(self, bot: TeXBot) -> None: """Start all task managers when this cog is initialised.""" - if settings["SEND_GET_ROLES_REMINDERS"]: + if settings["SEND_GET_ROLES_REMINDERS_ENABLED"]: self.send_get_roles_reminders.start() super().__init__(bot) @@ -49,7 +49,7 @@ def cog_unload(self) -> None: """ self.send_get_roles_reminders.cancel() - @tasks.loop(**settings["SEND_GET_ROLES_REMINDERS_INTERVAL"]) # type: ignore[misc] + @tasks.loop(seconds=settings["SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] @functools.partial( ErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError, @@ -71,7 +71,7 @@ async def send_get_roles_reminders(self) -> None: guest_role: discord.Role = await self.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 diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index b9e1f50f5..010ab1b08 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -38,8 +38,8 @@ class SendIntroductionRemindersTaskCog(TeXBotBaseCog): def __init__(self, bot: TeXBot) -> None: """Start all task managers when this cog is initialised.""" - if settings["SEND_INTRODUCTION_REMINDERS"]: - if settings["SEND_INTRODUCTION_REMINDERS"] == "interval": + if settings["SEND_INTRODUCTION_REMINDERS_ENABLED"]: + if settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "interval": SentOneOffIntroductionReminderMember.objects.all().delete() self.send_introduction_reminders.start() @@ -61,7 +61,7 @@ async def on_ready(self) -> None: self.OptOutIntroductionRemindersView(self.bot), ) - @tasks.loop(**settings["SEND_INTRODUCTION_REMINDERS_INTERVAL"]) # type: ignore[misc] + @tasks.loop(seconds=settings["SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] @functools.partial( ErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError, @@ -99,7 +99,7 @@ async def send_introduction_reminders(self) -> None: continue member_needs_one_off_reminder: bool = ( - settings["SEND_INTRODUCTION_REMINDERS"] == "once" + settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "once" and not await ( await SentOneOffIntroductionReminderMember.objects.afilter( discord_id=member.id, @@ -107,7 +107,7 @@ async def send_introduction_reminders(self) -> None: ).aexists() ) member_needs_recurring_reminder: bool = ( - settings["SEND_INTRODUCTION_REMINDERS"] == "interval" + settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "interval" ) member_recently_joined: bool = ( (discord.utils.utcnow() - member.joined_at) @@ -158,7 +158,7 @@ async def send_introduction_reminders(self) -> None: ), view=( self.OptOutIntroductionRemindersView(self.bot) - if settings["SEND_INTRODUCTION_REMINDERS"] == "interval" + if settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "interval" else None # type: ignore[arg-type] ), ) diff --git a/cogs/source.py b/cogs/source.py index 9e53be6e9..dc1e4879e 100644 --- a/cogs/source.py +++ b/cogs/source.py @@ -20,8 +20,9 @@ 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.bot.user.mention if self.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 4eb0e3b1c..0294be02a 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -147,26 +147,26 @@ 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": - manual_moderation_warning_message_location_exists: bool = bool( + if settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] != "DM": + strike_performed_manually_warning_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: + if not strike_performed_manually_warning_location_exists: logger.critical( ( "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() + strike_performed_manually_warning_location_similar_to_dm: bool = ( + settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"].lower() in ("dm", "dms") ) - if manual_moderation_warning_message_location_similar_to_dm: + if strike_performed_manually_warning_location_similar_to_dm: logger.info( ( "If you meant to set the location " diff --git a/cogs/strike.py b/cogs/strike.py index a6147a100..19434af2f 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -74,7 +74,7 @@ async def perform_moderation_action(strike_user: discord.Member, strikes: int, c if strikes == 1: await strike_user.timeout_for( - datetime.timedelta(hours=24), + settings["STRIKE_COMMAND_TIMEOUT_DURATION"], reason=MODERATION_ACTION_REASON, ) @@ -209,7 +209,7 @@ 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 @@ -402,9 +402,9 @@ 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: session: aiohttp.ClientSession with aiohttp.ClientSession() as session: # type: ignore[assignment] @@ -438,12 +438,12 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me guild_confirmation_message_channel: discord.TextChannel | None = discord.utils.get( self.bot.main_guild.text_channels, - name=settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"], + 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" ) diff --git a/config/__init__.py b/config/__init__.py index 5ac9def94..a27f1e20f 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -10,8 +10,7 @@ __all__: Sequence[str] = ( "PROJECT_ROOT", "TRANSLATED_MESSAGES_LOCALE_CODES", - "DEFAULT_STATISTICS_ROLES", - "LOG_LEVELS", + "LogLevels", "run_setup", "settings", "check_for_deprecated_environment_variables", @@ -32,8 +31,7 @@ from .constants import ( PROJECT_ROOT, TRANSLATED_MESSAGES_LOCALE_CODES, - DEFAULT_STATISTICS_ROLES, - LOG_LEVELS, + LogLevels, ) logger: Final[Logger] = logging.getLogger("TeX-Bot") @@ -94,6 +92,12 @@ def check_for_deprecated_environment_variables() -> None: "MEMBERS_LIST_LIST", "MEMBERS_LIST", "MEMBERS_LIST_URL_SESSION_COOKIE", + "MEMBERS_LIST_AUTH_SESSION_COOKIE", + "MEMBERS_LIST_URL_AUTH_COOKIE", + "MEMBERS_LIST_SESSION_COOKIE", + "MEMBERS_LIST_URL_COOKIE", + "MEMBERS_LIST_AUTH_COOKIE", + "MEMBERS_LIST_COOKIE", "PING_COMMAND_EASTER_EGG_PROBABILITY", "PING_EASTER_EGG_PROBABILITY", "MESSAGES_FILE_PATH", @@ -113,7 +117,14 @@ def check_for_deprecated_environment_variables() -> None: "MODERATION_DOCUMENT_LINK", "MODERATION_DOCUMENT", "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", + "MANUAL_MODERATION_WARNING_LOCATION", "MANUAL_MODERATION_MESSAGE_LOCATION", + "STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION", + "STRIKE_PERFORMED_MANUALLY_WARNING_MESSAGE_LOCATION", + "STRIKE_PERFORMED_MANUALLY_MESSAGE_LOCATION", + "MANUAL_STRIKE_WARNING_MESSAGE_LOCATION", + "MANUAL_STRIKE_MESSAGE_LOCATION", + "MANUAL_STRIKE_WARNING_LOCATION", ) deprecated_environment_variable_name: str for deprecated_environment_variable_name in DEPRECATED_ENVIRONMENT_VARIABLE_NAMES: diff --git a/config/_settings.py b/config/_settings.py index 89e8a5d28..f2a17914f 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -14,6 +14,7 @@ import logging import os import re +from datetime import timedelta from logging import Logger from pathlib import Path from typing import Any, ClassVar, Final @@ -174,14 +175,57 @@ def reload(cls) -> None: current_yaml["community-group"].get("short-name", None), ), cls._reload_purchase_membership_link( - current_yaml["community-group"]["links"].get("purchase-membership"), + current_yaml["community-group"]["links"].get("purchase-membership", None), ), cls._reload_membership_perks_link( - current_yaml["community-group"]["links"].get("membership-perks"), + current_yaml["community-group"]["links"].get("membership-perks", None), ), cls._reload_moderation_document_link( current_yaml["community-group"]["links"]["moderation-document"], ), + cls._reload_members_list_url( + current_yaml["community-group"]["members-list"]["url"], + ), + cls._reload_members_list_auth_session_cookie( + current_yaml["community-group"]["members-list"]["auth-session-cookie"], + ), + cls._reload_members_list_id_format( + current_yaml["community-group"]["members-list"]["id-format"], + ), + cls._reload_ping_command_easter_egg_probability( + current_yaml["commands"]["ping"]["easter-egg-probability"], + ), + cls._reload_stats_command_lookback_days( + current_yaml["commands"]["stats"]["lookback-days"], + ), + cls._reload_stats_command_displayed_roles( + current_yaml["commands"]["stats"]["displayed-roles"], + ), + cls._reload_stats_command_displayed_roles( + current_yaml["commands"]["strike"]["timeout-duration"], + ), + cls._reload_strike_performed_manually_warning_location( + current_yaml["commands"]["strike"]["performed-manually-warning-location"], + ), + cls._reload_messages_language(current_yaml["messages-language"]), + cls._reload_send_introduction_reminders_enabled( + current_yaml["reminders"]["send-introduction-reminders"]["enabled"] + ), + cls._reload_send_introduction_reminders_delay( + current_yaml["reminders"]["send-introduction-reminders"]["delay"] + ), + cls._reload_send_introduction_reminders_interval( + current_yaml["reminders"]["send-introduction-reminders"]["interval"] + ), + cls._reload_send_get_roles_reminders_enabled( + current_yaml["reminders"]["send-get-roles-reminders"]["enabled"] + ), + cls._reload_send_get_roles_reminders_delay( + current_yaml["reminders"]["send-get-roles-reminders"]["delay"] + ), + cls._reload_send_get_roles_reminders_interval( + current_yaml["reminders"]["send-get-roles-reminders"]["interval"] + ), ) cls._most_recent_yaml = current_yaml @@ -503,4 +547,340 @@ def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set return {"community-group:links:moderation-document"} - # TODO: Load more config settings + @classmethod + def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: + """ + Reload the url that points to the location of your community group's members-list. + + Returns the set of settings keys that have been changed. + """ + MEMBERS_LIST_URL_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or members_list_url != cls._most_recent_yaml["community-group"]["members-list"][ + "url" + ] + or "MEMBERS_LIST_URL" not in cls._settings + ) + if not MEMBERS_LIST_URL_CHANGED: + return set() + + cls._settings["MEMBERS_LIST_URL"] = members_list_url.data + + return {"community-group:members-list:url"} + + @classmethod + def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cookie: YAML) -> set[str]: + """ + Reload the auth session cookie used to authenticate to access your members-list. + + Returns the set of settings keys that have been changed. + """ + MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or members_list_auth_session_cookie != cls._most_recent_yaml["community-group"][ + "members-list" + ]["auth-session-cookie"] + or "MEMBERS_LIST_AUTH_SESSION_COOKIE" not in cls._settings + ) + if not MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: + return set() + + cls._settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"] = ( + members_list_auth_session_cookie.data + ) + + return {"community-group:members-list:auth-session-cookie"} + + @classmethod + def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str]: + """ + Reload the format regex matcher for IDs in your community group's members-list. + + Returns the set of settings keys that have been changed. + """ + MEMBERS_LIST_ID_FORMAT_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or members_list_id_format != cls._most_recent_yaml["community-group"][ + "members-list" + ]["id-format"] + or "MEMBERS_LIST_ID_FORMAT" not in cls._settings + ) + if not MEMBERS_LIST_ID_FORMAT_CHANGED: + return set() + + cls._settings["MEMBERS_LIST_ID_FORMAT"] = members_list_id_format.data + + return {"community-group:members-list:id-format"} + + @classmethod + def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_probability: YAML) -> set[str]: # noqa: E501 + """ + Reload the probability that the rarer response will show when using the ping command. + + Returns the set of settings keys that have been changed. + """ + PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or ping_command_easter_egg_probability != cls._most_recent_yaml["commands"][ + "ping" + ]["easter-egg-probability"] + or "PING_COMMAND_EASTER_EGG_PROBABILITY" not in cls._settings + ) + if not PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: + return set() + + cls._settings["PING_COMMAND_EASTER_EGG_PROBABILITY"] = ( + ping_command_easter_egg_probability.data + ) + + return {"commands:ping:easter-egg-probability"} + + @classmethod + def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) -> set[str]: # noqa: E501 + """ + Reload the number of days to lookback for statistics. + + Returns the set of settings keys that have been changed. + """ + STATS_COMMAND_LOOKBACK_DAYS_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or stats_command_lookback_days != cls._most_recent_yaml["commands"][ + "stats" + ]["lookback-days"] + or "STATS_COMMAND_LOOKBACK_DAYS" not in cls._settings + ) + if not STATS_COMMAND_LOOKBACK_DAYS_CHANGED: + return set() + + cls._settings["STATS_COMMAND_LOOKBACK_DAYS"] = timedelta( + days=stats_command_lookback_days.data + ) + + return {"commands:stats:lookback-days"} + + @classmethod + def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YAML) -> set[str]: # noqa: E501 + """ + Reload the set of roles used to display statistics about. + + Returns the set of settings keys that have been changed. + """ + STATS_COMMAND_DISPLAYED_ROLES_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or stats_command_displayed_roles != cls._most_recent_yaml["commands"][ + "stats" + ]["displayed-roles"] + or "STATS_COMMAND_DISPLAYED_ROLES" not in cls._settings + ) + if not STATS_COMMAND_DISPLAYED_ROLES_CHANGED: + return set() + + cls._settings["STATS_COMMAND_DISPLAYED_ROLES"] = stats_command_displayed_roles.data + + return {"commands:stats:displayed-roles"} + + @classmethod + def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration: YAML) -> set[str]: # noqa: E501 + """ + Reload the duration to use when applying a timeout action for a strike increase. + + Returns the set of settings keys that have been changed. + """ + STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or strike_command_timeout_duration != cls._most_recent_yaml["commands"][ + "strike" + ]["timeout-duration"] + or "STRIKE_COMMAND_TIMEOUT_DURATION" not in cls._settings + ) + if not STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: + return set() + + cls._settings["STRIKE_COMMAND_TIMEOUT_DURATION"] = strike_command_timeout_duration.data + + return {"commands:strike:timeout-duration"} + + @classmethod + def _reload_strike_performed_manually_warning_location(cls, strike_performed_manually_warning_location: YAML) -> set[str]: # noqa: E501 + """ + Reload the location to send warning messages when strikes are performed manually. + + Returns the set of settings keys that have been changed. + """ + STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or strike_performed_manually_warning_location != cls._most_recent_yaml["commands"][ + "strike" + ]["performed-manually-warning-location"] + or "STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION" not in cls._settings + ) + if not STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: + return set() + + cls._settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] = ( + strike_performed_manually_warning_location.data + ) + + return {"commands:strike:performed-manually-warning-location"} + + @classmethod + def _reload_messages_language(cls, messages_language: YAML) -> set[str]: + """ + Reload the selected language for messages to be sent in. + + Returns the set of settings keys that have been changed. + """ + MESSAGES_LANGUAGE_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or messages_language != cls._most_recent_yaml["messages-language"] + or "MESSAGES_LANGUAGE" not in cls._settings + ) + if not MESSAGES_LANGUAGE_CHANGED: + return set() + + cls._settings["MESSAGES_LANGUAGE"] = messages_language.data + + return {"messages-language"} + + @classmethod + def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders_enabled: YAML) -> set[str]: # noqa: E501 + """ + Reload the flag for whether the "send-introduction-reminders" task is enabled. + + Returns the set of settings keys that have been changed. + """ + SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or send_introduction_reminders_enabled != cls._most_recent_yaml["reminders"][ + "send-introduction-reminders" + ]["enabled"] + or "SEND_INTRODUCTION_REMINDERS_ENABLED" not in cls._settings + ) + if not SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: + return set() + + cls._settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] = ( + send_introduction_reminders_enabled.data + ) + + return {"reminders:send-introduction-reminders:enabled"} + + @classmethod + def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_delay: YAML) -> set[str]: # noqa: E501 + """ + Reload the amount of time to wait before sending introduction-reminders to a user. + + Returns the set of settings keys that have been changed. + + Waiting begins from the time that the user joined your community group's Discord guild. + """ + SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or send_introduction_reminders_delay != cls._most_recent_yaml["reminders"][ + "send-introduction-reminders" + ]["delay"] + or "SEND_INTRODUCTION_REMINDERS_DELAY" not in cls._settings + ) + if not SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: + return set() + + cls._settings["SEND_INTRODUCTION_REMINDERS_DELAY"] = ( + send_introduction_reminders_delay.data + ) + + return {"reminders:send-introduction-reminders:delay"} + + @classmethod + def _reload_send_introduction_reminders_interval(cls, send_introduction_reminders_interval: YAML) -> set[str]: # noqa: E501 + """ + Reload the interval of time between executing the task to send introduction-reminders. + + Returns the set of settings keys that have been changed. + """ + SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or send_introduction_reminders_interval != cls._most_recent_yaml["reminders"][ + "send-introduction-reminders" + ]["interval"] + or "SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS" not in cls._settings + ) + if not SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: + return set() + + cls._settings["SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS"] = ( + send_introduction_reminders_interval.data.total_seconds() + ) + + return {"reminders:send-introduction-reminders:interval"} + + @classmethod + def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabled: YAML) -> set[str]: # noqa: E501 + """ + Reload the flag for whether the "send-get-roles-reminders" task is enabled. + + Returns the set of settings keys that have been changed. + """ + SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or send_get_roles_reminders_enabled != cls._most_recent_yaml["reminders"][ + "send-get-roles-reminders" + ]["enabled"] + or "SEND_GET_ROLES_REMINDERS_ENABLED" not in cls._settings + ) + if not SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: + return set() + + cls._settings["SEND_GET_ROLES_REMINDERS_ENABLED"] = ( + send_get_roles_reminders_enabled.data + ) + + return {"reminders:send-get-roles-reminders:enabled"} + + @classmethod + def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: YAML) -> set[str]: # noqa: E501 + """ + Reload the amount of time to wait before sending get-roles-reminders to a user. + + Returns the set of settings keys that have been changed. + + Waiting begins from the time that the user was inducted as a guest + into your community group's Discord guild. + """ + SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or send_get_roles_reminders_delay != cls._most_recent_yaml["reminders"][ + "send-get-roles-reminders" + ]["delay"] + or "SEND_GET_ROLES_REMINDERS_DELAY" not in cls._settings + ) + if not SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: + return set() + + cls._settings["SEND_GET_ROLES_REMINDERS_DELAY"] = ( + send_get_roles_reminders_delay.data + ) + + return {"reminders:send-get-roles-reminders:delay"} + + @classmethod + def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_interval: YAML) -> set[str]: # noqa: E501 + """ + Reload the interval of time between executing the task to send get-roles-reminders. + + Returns the set of settings keys that have been changed. + """ + SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or send_get_roles_reminders_interval != cls._most_recent_yaml["reminders"][ + "send-get-roles-reminders" + ]["interval"] + or "SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS" not in cls._settings + ) + if not SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: + return set() + + cls._settings["SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS"] = ( + send_get_roles_reminders_interval.data.total_seconds() + ) + + return {"reminders:send-get-roles-reminders:interval"} diff --git a/config/_yaml/__init__.py b/config/_yaml/__init__.py index a141bf08e..e3efeb799 100644 --- a/config/_yaml/__init__.py +++ b/config/_yaml/__init__.py @@ -10,67 +10,75 @@ "load_yaml", ) + from collections.abc import Mapping from typing import Final import strictyaml from strictyaml import YAML -from ..constants import DEFAULT_STATISTICS_ROLES, TRANSLATED_MESSAGES_LOCALE_CODES +from ..constants import ( + LogLevels, + SendIntroductionRemindersFlagType, + DEFAULT_CONSOLE_LOG_LEVEL, + DEFAULT_DISCORD_LOGGING_LOG_LEVEL, + TRANSLATED_MESSAGES_LOCALE_CODES, + DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY, + DEFAULT_MEMBERS_LIST_ID_FORMAT, + DEFAULT_STATS_COMMAND_LOOKBACK_DAYS, + DEFAULT_STATS_COMMAND_DISPLAYED_ROLES, + DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, + DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION, + DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED, + DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, + DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, + DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED, + DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, + DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, +) from .custom_validators import ( DiscordWebhookURLValidator, + TimeDeltaValidator, LogLevelValidator, RegexMatcher, DiscordSnowflakeValidator, ProbabilityValidator, SendIntroductionRemindersFlagValidator, - SendIntroductionRemindersFlagType, - LogLevelType, ) from .custom_schema_utils import SlugKeyMap -_DEFAULT_CONSOLE_LOG_LEVEL: Final[LogLevelType] = "INFO" -_DEFAULT_CONSOLE_LOGGING_SETTINGS: Final[Mapping[str, str]] = { - "log-level": _DEFAULT_CONSOLE_LOG_LEVEL, +_DEFAULT_CONSOLE_LOGGING_SETTINGS: Final[Mapping[str, LogLevels]] = { + "log-level": DEFAULT_CONSOLE_LOG_LEVEL, } -_DEFAULT_LOGGING_SETTINGS: Final[Mapping[str, Mapping[str, str]]] = { +_DEFAULT_LOGGING_SETTINGS: Final[Mapping[str, Mapping[str, LogLevels]]] = { "console": _DEFAULT_CONSOLE_LOGGING_SETTINGS, } -_DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY: Final[float] = 0.01 _DEFAULT_PING_COMMAND_SETTINGS: Final[Mapping[str, float]] = { - "easter-egg-probability": _DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY, + "easter-egg-probability": DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY, } -_DEFAULT_STATS_COMMAND_LOOKBACK_DAYS: Final[float] = 30.0 -_DEFAULT_STATS_COMMAND_DISPLAYED_ROLES: Final[Sequence[str]] = list(DEFAULT_STATISTICS_ROLES) _DEFAULT_STATS_COMMAND_SETTINGS: Final[Mapping[str, float | Sequence[str]]] = { - "lookback-days": _DEFAULT_STATS_COMMAND_LOOKBACK_DAYS, - "displayed-roles": _DEFAULT_STATS_COMMAND_DISPLAYED_ROLES, + "lookback-days": DEFAULT_STATS_COMMAND_LOOKBACK_DAYS, + "displayed-roles": DEFAULT_STATS_COMMAND_DISPLAYED_ROLES, } -_DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION: Final[str] = "24h" -_DEFAULT_STRIKE_COMMAND_MANUAL_USE_WARNING_LOCATION: Final[str] = "DM" _DEFAULT_STRIKE_COMMAND_SETTINGS: Final[Mapping[str, str]] = { - "timeout-duration": _DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, - "manual-use-warning-location": _DEFAULT_STRIKE_COMMAND_MANUAL_USE_WARNING_LOCATION, + "timeout-duration": DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, + "performed-manually-warning-location": DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION, } _DEFAULT_COMMANDS_SETTINGS: Final[Mapping[str, Mapping[str, float]]] = { "ping": _DEFAULT_PING_COMMAND_SETTINGS, "stats": _DEFAULT_STATS_COMMAND_SETTINGS, "strike": _DEFAULT_STRIKE_COMMAND_SETTINGS, } -_DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY: Final[str] = "40h" -_DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL: Final[str] = "6h" _DEFAULT_SEND_INTRODUCTION_REMINDERS_SETTINGS: Final[Mapping[str, SendIntroductionRemindersFlagType | str]] = { - "enable": "once", - "delay": _DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, - "interval": _DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, + "enabled": DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED, + "delay": DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, + "interval": DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, } -_DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY: Final[str] = "40h" -_DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL: Final[str] = "6h" _DEFAULT_SEND_GET_ROLES_REMINDERS_SETTINGS: Final[Mapping[str, bool | str]] = { - "enable": True, - "delay": _DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, - "interval": _DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, + "enabled": DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED, + "delay": DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, + "interval": DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, } _DEFAULT_REMINDERS_SETTINGS: Final[Mapping[str, Mapping[str, bool | str] | Mapping[str, SendIntroductionRemindersFlagType | str]]] = { # noqa: E501 "send-introduction-reminders": _DEFAULT_SEND_INTRODUCTION_REMINDERS_SETTINGS, @@ -81,17 +89,19 @@ { strictyaml.Optional("logging", default=_DEFAULT_LOGGING_SETTINGS): SlugKeyMap( { - strictyaml.Optional("console", default=_DEFAULT_CONSOLE_LOGGING_SETTINGS): SlugKeyMap( # noqa: E501 - { - strictyaml.Optional("log-level", default=_DEFAULT_CONSOLE_LOG_LEVEL): ( - LogLevelValidator() - ), - }, + strictyaml.Optional("console", default=_DEFAULT_CONSOLE_LOGGING_SETTINGS): ( + SlugKeyMap( + { + strictyaml.Optional("log-level", default=DEFAULT_CONSOLE_LOG_LEVEL): ( # noqa: E501 + LogLevelValidator() + ), + }, + ) ), strictyaml.Optional("discord-channel"): SlugKeyMap( { "webhook-url": DiscordWebhookURLValidator(), - strictyaml.Optional("log-level", default="WARNING"): ( + strictyaml.Optional("log-level", default=DEFAULT_DISCORD_LOGGING_LOG_LEVEL): ( # noqa: E501 LogLevelValidator() ), }, @@ -125,7 +135,7 @@ { "url": strictyaml.Url(), "auth-session-cookie": strictyaml.Str(), - strictyaml.Optional("id-format", default=r"\A\d{6,7}\Z"): ( + strictyaml.Optional("id-format", default=DEFAULT_MEMBERS_LIST_ID_FORMAT): ( # noqa: E501 RegexMatcher() ), }, @@ -136,7 +146,7 @@ { strictyaml.Optional("ping", default=_DEFAULT_PING_COMMAND_SETTINGS): SlugKeyMap( # noqa: E501 { - strictyaml.Optional("easter-egg-probability", default=_DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY): ( # noqa: E501 + strictyaml.Optional("easter-egg-probability", default=DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY): ( # noqa: E501 ProbabilityValidator() ), }, @@ -144,10 +154,10 @@ strictyaml.Optional("stats", default=_DEFAULT_STATS_COMMAND_SETTINGS): ( SlugKeyMap( { - strictyaml.Optional("lookback-days", default=_DEFAULT_STATS_COMMAND_LOOKBACK_DAYS): ( # noqa: E501 + strictyaml.Optional("lookback-days", default=DEFAULT_STATS_COMMAND_LOOKBACK_DAYS): ( # noqa: E501 strictyaml.Float() ), - strictyaml.Optional("displayed-roles", default=_DEFAULT_STATS_COMMAND_DISPLAYED_ROLES): ( # noqa: E501 + strictyaml.Optional("displayed-roles", default=DEFAULT_STATS_COMMAND_DISPLAYED_ROLES): ( # noqa: E501 strictyaml.UniqueSeq(strictyaml.Str()) ), }, @@ -156,12 +166,15 @@ strictyaml.Optional("strike", default=_DEFAULT_STRIKE_COMMAND_SETTINGS): ( SlugKeyMap( { - strictyaml.Optional("timeout-duration", default=_DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION): ( # noqa: E501 - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + strictyaml.Optional("timeout-duration", default=DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION): ( # noqa: E501 + TimeDeltaValidator( + minutes=True, + hours=True, + days=True, + weeks=True, ) ), - strictyaml.Optional("manual-use-warning-location", default=_DEFAULT_STRIKE_COMMAND_MANUAL_USE_WARNING_LOCATION): ( # noqa: E501 + strictyaml.Optional("performed-manually-warning-location", default=DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION): ( # noqa: E501 strictyaml.Str() ), }, @@ -176,31 +189,23 @@ { strictyaml.Optional("send-introduction-reminders", default=_DEFAULT_SEND_INTRODUCTION_REMINDERS_SETTINGS): SlugKeyMap( # noqa: E501 { - "enable": SendIntroductionRemindersFlagValidator(), - strictyaml.Optional("delay", default=_DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY): ( # noqa: E501 - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - ) + "enabled": SendIntroductionRemindersFlagValidator(), + strictyaml.Optional("delay", default=DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY): ( # noqa: E501 + TimeDeltaValidator(minutes=True, hours=True, days=True, weeks=True) ), - strictyaml.Optional("interval", default=_DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL): ( # noqa: E501 - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - ) + strictyaml.Optional("interval", default=DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL): ( # noqa: E501 + TimeDeltaValidator(minutes=True, hours=True) ), }, ), strictyaml.Optional("send-get-roles-reminders", default=_DEFAULT_SEND_GET_ROLES_REMINDERS_SETTINGS): SlugKeyMap( # noqa: E501 { - "enable": strictyaml.Bool(), - strictyaml.Optional("delay", default=_DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY): ( # noqa: E501 - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - ) + "enabled": strictyaml.Bool(), + strictyaml.Optional("delay", default=DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY): ( # noqa: E501 + TimeDeltaValidator(minutes=True, hours=True, days=True, weeks=True) ), - strictyaml.Optional("interval", default=_DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL): ( # noqa: E501 - strictyaml.Regex( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - ) + strictyaml.Optional("interval", default=DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL): ( # noqa: E501 + TimeDeltaValidator(minutes=True, hours=True) ), }, ), diff --git a/config/_yaml/custom_schema_utils.py b/config/_yaml/custom_schema_utils.py index 3a5b722bd..9e9a5dc73 100644 --- a/config/_yaml/custom_schema_utils.py +++ b/config/_yaml/custom_schema_utils.py @@ -2,6 +2,7 @@ __all__: Sequence[str] = ("SlugKeyValidator", "SlugKeyMap") + from typing import override import slugify diff --git a/config/_yaml/custom_validators.py b/config/_yaml/custom_validators.py index 8ec45e7bd..fc077f1bc 100644 --- a/config/_yaml/custom_validators.py +++ b/config/_yaml/custom_validators.py @@ -6,14 +6,17 @@ "DiscordSnowflakeValidator", "RegexMatcher", "ProbabilityValidator", + "TimeDeltaValidator", "SendIntroductionRemindersFlagValidator", - "SendIntroductionRemindersFlagType", - "LogLevelType", ) + +import functools import math import re -from typing import Final, override, Literal, TypeAlias +from datetime import timedelta +from typing import Final, NoReturn, override, Callable +from re import Match import strictyaml from strictyaml import constants as strictyaml_constants @@ -21,23 +24,23 @@ from strictyaml.exceptions import YAMLSerializationError from strictyaml.yamllocation import YAMLChunk -from ..constants import LOG_LEVELS, VALID_SEND_INTRODUCTION_REMINDERS_VALUES - - -SendIntroductionRemindersFlagType: TypeAlias = Literal["once", "interval", False] -LogLevelType: TypeAlias = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +from ..constants import ( + LogLevels, + VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES, + SendIntroductionRemindersFlagType, +) class LogLevelValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] @override - def validate_scalar(self, chunk: YAMLChunk) -> LogLevelType: # type: ignore[no-any-unimported,misc] - val: str = str(chunk.contents).upper() + def validate_scalar(self, chunk: YAMLChunk) -> LogLevels: # type: ignore[no-any-unimported,misc] + val: str = str(chunk.contents).upper().strip().strip("-").strip("_").strip(".") - if val not in LOG_LEVELS: + if val not in LogLevels: chunk.expecting_but_found( ( "when expecting a valid log-level " - f"(one of: \"{"\", \"".join(LOG_LEVELS)}\")" + f"(one of: \"{"\", \"".join(LogLevels)}\")" ), ) raise RuntimeError @@ -47,11 +50,12 @@ def validate_scalar(self, chunk: YAMLChunk) -> LogLevelType: # type: ignore[no- # noinspection PyOverrides @override def to_yaml(self, data: object) -> str: # type: ignore[misc] - str_data: str = str(data).upper() + self.should_be_string(data, "expected a valid log-level.") + str_data: str = data.upper().strip().strip("-").strip("_").strip(".") - if str_data.upper() not in LOG_LEVELS: + if str_data not in LogLevels: raise YAMLSerializationError( - f"Got {data} when expecting one of: \"{"\", \"".join(LOG_LEVELS)}\".", + f"Got {data} when expecting one of: \"{"\", \"".join(LogLevels)}\".", ) return str_data @@ -76,9 +80,6 @@ def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimp def to_yaml(self, data: object) -> str: # type: ignore[misc] self.should_be_string(data, "expected a URL,") - if not isinstance(data, str): - raise TypeError - DATA_IS_VALID: Final[bool] = bool( ( self.__is_absolute_url(data) @@ -136,8 +137,6 @@ def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimp @override def to_yaml(self, data: object) -> str: # type: ignore[misc] self.should_be_string(data, self.MATCHING_MESSAGE) - if not isinstance(data, str): - raise TypeError try: re.compile(data) @@ -165,9 +164,6 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] ) if strictyaml_utils.has_number_type(data): - if not isinstance(data, float): - raise TypeError - if not 0 <= data <= 100: raise YAML_SERIALIZATION_ERROR @@ -181,10 +177,7 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return str(data / 100) if strictyaml_utils.is_string(data) and strictyaml_utils.is_decimal(data): - if not isinstance(data, str): - raise TypeError - - float_data: float = float(data) + float_data: float = float(str(data)) if not 0 <= float_data <= 100: raise YAML_SERIALIZATION_ERROR @@ -194,12 +187,103 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] raise YAML_SERIALIZATION_ERROR +class TimeDeltaValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] + @override + def __init__(self, *, minutes: bool = True, hours: bool = True, days: bool = False, weeks: bool = False) -> None: # noqa: E501 + regex_matcher: str = r"\A" + + time_resolution_name: str + for time_resolution_name in ("seconds", "minutes", "hours", "days", "weeks"): + time_resolution_name = time_resolution_name.lower().strip() + time_resolution: object = ( + True if time_resolution_name == "seconds" else locals()[time_resolution_name] + ) + + if not isinstance(time_resolution, bool): + raise TypeError + + if not time_resolution: + continue + + regex_matcher += ( + r"(?:(?P<" + + time_resolution_name + + r">(?:\d*\.)?\d+)" + + time_resolution_name[0] + + ")?" + ) + + regex_matcher += r"\Z" + + self.regex_matcher: re.Pattern[str] = re.compile(regex_matcher) + + def _get_value_from_match(self, match: Match[str], key: str) -> float: + if key not in self.regex_matcher.groupindex.keys(): + return 0.0 + + value: str | None = match.group(key) + + if not value: + return 0.0 + + float_conversion_error: ValueError + try: + return float(value) + except ValueError as float_conversion_error: + raise float_conversion_error from float_conversion_error + + @override + def validate_scalar(self, chunk: YAMLChunk) -> timedelta: + chunk_error_func: Callable[[], NoReturn] = functools.partial( + chunk.expecting_but_found, + expecting="when expecting a delay/interval string", + found="found non-matching string", + ) + + match: Match[str] | None = self.regex_matcher.match(chunk.contents) + if match is None: + chunk_error_func() + + try: + return timedelta( + seconds=self._get_value_from_match(match, "seconds"), + minutes=self._get_value_from_match(match, "minutes"), + hours=self._get_value_from_match(match, "hours"), + days=self._get_value_from_match(match, "days"), + weeks=self._get_value_from_match(match, "weeks"), + ) + except ValueError: + chunk_error_func() + + # noinspection PyOverrides + @override + def to_yaml(self, data: object) -> str: + if strictyaml_utils.is_string(data): + match: Match[str] | None = self.regex_matcher.match(str(data)) + if match is None: + raise YAMLSerializationError( + f"when expecting a delay/interval string found {str(data)!r}." + ) + return str(data) + + if not hasattr(data, "total_seconds") or not callable(getattr(data, "total_seconds")): + raise YAMLSerializationError( + f"when expecting a time delta object found {str(data)!r}." + ) + + total_seconds: object = getattr(data, "total_seconds")() + if not isinstance(total_seconds, float): + raise TypeError + + return f"{total_seconds}s" + + class SendIntroductionRemindersFlagValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] @override def validate_scalar(self, chunk: YAMLChunk) -> SendIntroductionRemindersFlagType: # type: ignore[no-any-unimported,misc] val: str = str(chunk.contents).lower() - if val not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: + if val not in VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES: chunk.expecting_but_found( ( "when expecting a send-introduction-reminders-flag " @@ -222,15 +306,15 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] if isinstance(data, bool): return "Once" if data else "False" - if not isinstance(data, str) or data not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: + if str(data).lower() not in VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES: raise YAMLSerializationError( f"Got {data} when expecting one of: \"Once\", \"Interval\" or \"False\".", ) - if data in strictyaml_constants.TRUE_VALUES: + if str(data).lower() in strictyaml_constants.TRUE_VALUES: return "Once" - if data in strictyaml_constants.FALSE_VALUES: + if str(data).lower() in strictyaml_constants.FALSE_VALUES: return "False" - return data.title() + return str(data).lower().title() diff --git a/config/constants.py b/config/constants.py index ae71d600e..414d3e3a2 100644 --- a/config/constants.py +++ b/config/constants.py @@ -3,70 +3,111 @@ from collections.abc import Sequence __all__: Sequence[str] = ( + "SendIntroductionRemindersFlagType", + "LogLevels", "PROJECT_ROOT", - "VALID_SEND_INTRODUCTION_REMINDERS_VALUES", + "VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES", "TRANSLATED_MESSAGES_LOCALE_CODES", - "DEFAULT_STATISTICS_ROLES", - "LOG_LEVELS", "REQUIRES_RESTART_SETTINGS", "DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME", + "DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY", + "DEFAULT_DISCORD_LOGGING_LOG_LEVEL", + "DEFAULT_CONSOLE_LOG_LEVEL", + "DEFAULT_MEMBERS_LIST_ID_FORMAT", + "DEFAULT_STATS_COMMAND_LOOKBACK_DAYS", + "DEFAULT_STATS_COMMAND_DISPLAYED_ROLES", + "DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION", + "DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION", + "DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED", + "DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY", + "DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL", + "DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED", + "DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY", + "DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL", ) +from enum import Enum, EnumMeta from pathlib import Path -from typing import Final +from typing import Final, TypeAlias, Literal from strictyaml import constants as strictyaml_constants -PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.resolve() -VALID_SEND_INTRODUCTION_REMINDERS_VALUES: Final[frozenset[str]] = frozenset( - ({"once", "interval"} | set(strictyaml_constants.BOOL_VALUES)), -) +SendIntroductionRemindersFlagType: TypeAlias = Literal["once", "interval", False] + + +class MetaEnum(EnumMeta): + def __contains__(cls, item: object) -> bool: + try: + cls(item) + except ValueError: + return False + return True + + +class LogLevels(str, Enum, metaclass=MetaEnum): + DEBUG = "DEBUG" + INFO = "INFO" + WARNING = "WARNING" + ERROR = "ERROR" + CRITICAL = "CRITICAL" + + +PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.resolve() TRANSLATED_MESSAGES_LOCALE_CODES: Final[frozenset[str]] = frozenset({"en-GB"}) -DEFAULT_STATISTICS_ROLES: Final[frozenset[str]] = frozenset( - { - "Committee", - "Committee-Elect", - "Student Rep", - "Member", - "Guest", - "Server Booster", - "Foundation Year", - "First Year", - "Second Year", - "Final Year", - "Year In Industry", - "Year Abroad", - "PGT", - "PGR", - "Alumnus/Alumna", - "Postdoc", - "Quiz Victor", - }, -) -LOG_LEVELS: Final[Sequence[str]] = ( - "DEBUG", - "INFO", - "WARNING", - "ERROR", - "CRITICAL", +VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES: Final[frozenset[str]] = frozenset( + ({"once", "interval"} | set(strictyaml_constants.BOOL_VALUES)), ) REQUIRES_RESTART_SETTINGS: Final[frozenset[str]] = frozenset( { "discord:bot-token", "discord:guild-id", - "reminders:send-introduction-reminders:enable", + "reminders:send-introduction-reminders:enabled", "reminders:send-introduction-reminders:delay", "reminders:send-introduction-reminders:interval", - "reminders:send-get-roles-reminders:enable", + "reminders:send-get-roles-reminders:enabled", "reminders:send-get-roles-reminders:delay", "reminders:send-get-roles-reminders:interval", }, ) DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME: Final[str] = "TeX-Bot" + + +DEFAULT_CONSOLE_LOG_LEVEL: Final[LogLevels] = LogLevels.INFO +DEFAULT_DISCORD_LOGGING_LOG_LEVEL: Final[LogLevels] = LogLevels.WARNING +DEFAULT_MEMBERS_LIST_ID_FORMAT: Final[str] = r"\A\d{6,7}\Z" +DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY: Final[float] = 0.01 +DEFAULT_STATS_COMMAND_LOOKBACK_DAYS: Final[float] = 30.0 +DEFAULT_STATS_COMMAND_DISPLAYED_ROLES: Final[Sequence[str]] = [ + "Committee", + "Committee-Elect", + "Student Rep", + "Member", + "Guest", + "Server Booster", + "Foundation Year", + "First Year", + "Second Year", + "Final Year", + "Year In Industry", + "Year Abroad", + "PGT", + "PGR", + "Alumnus/Alumna", + "Postdoc", + "Quiz Victor", +] +DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION: Final[str] = "24h" +DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION: Final[str] = "DM" +DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED: Final[SendIntroductionRemindersFlagType] = "once" +DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY: Final[str] = "40h" +DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL: Final[str] = "6h" +DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED: Final[bool] = True +DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY: Final[str] = "40h" +DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL: Final[str] = "6h" diff --git a/tests/test_utils.py b/tests/test_utils.py index cda4ad8f7..847ccd155 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -83,15 +83,15 @@ 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}", + f"https://discord.com/.*={DISCORD_BOT_APPLICATION_ID}.*={DISCORD_MAIN_GUILD_ID}", invite_url, ) diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 7d5567f6e..405312f9b 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -90,8 +90,8 @@ 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"]) + if not self._main_guild or not self._bot_has_guild(settings["_DISCORD_MAIN_GUILD_ID"]): + raise GuildDoesNotExistError(guild_id=settings["_DISCORD_MAIN_GUILD_ID"]) return self._main_guild From e73b844808bb95f7b4b127246f709c6a207c8fab Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 03:35:04 +0100 Subject: [PATCH 013/128] Fix typing errors --- cogs/startup.py | 6 +-- config/_settings.py | 69 ++++++++++++++++------------- config/_yaml/__init__.py | 4 +- config/_yaml/custom_schema_utils.py | 4 +- config/_yaml/custom_validators.py | 28 ++++++------ 5 files changed, 58 insertions(+), 53 deletions(-) diff --git a/cogs/startup.py b/cogs/startup.py index 0294be02a..5fd51f194 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -38,7 +38,7 @@ def _setup_discord_log_channel(self) -> None: "so error logs will not be sent to the Discord log-channel." ) - discord_logging_handlers: set[DiscordHandler] = { + discord_logging_handlers: set[DiscordHandler] = { # type: ignore[no-any-unimported] handler for handler in logger.handlers if isinstance(handler, DiscordHandler) } @@ -48,12 +48,12 @@ def _setup_discord_log_channel(self) -> None: ) elif len(discord_logging_handlers) == 1: - existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() + existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() # type: ignore[no-any-unimported] logger.removeHandler(existing_discord_logging_handler) if settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"]: - new_discord_logging_handler: DiscordHandler = DiscordHandler( + new_discord_logging_handler: DiscordHandler = DiscordHandler( # type: ignore[no-any-unimported] ( existing_discord_logging_handler.name if existing_discord_logging_handler.name != DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME # noqa: E501 diff --git a/config/_settings.py b/config/_settings.py index f2a17914f..e04e719ba 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -17,7 +17,7 @@ from datetime import timedelta from logging import Logger from pathlib import Path -from typing import Any, ClassVar, Final +from typing import Any, ClassVar, Final, TextIO from discord_logging.handler import DiscordHandler from strictyaml import YAML @@ -234,7 +234,7 @@ def reload(cls) -> None: raise BotRequiresRestartAfterConfigChange(changed_settings=changed_settings_keys) @classmethod - def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: + def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the console logging configuration with the new given log level. @@ -247,7 +247,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: if not CONSOLE_LOGGING_SETTINGS_CHANGED: return set() - stream_handlers: set[logging.StreamHandler] = { + stream_handlers: set[logging.StreamHandler[TextIO]] = { handler for handler in logger.handlers @@ -256,7 +256,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: if len(stream_handlers) > 1: raise ValueError("Cannot determine which logging stream-handler to update.") - console_logging_handler: logging.StreamHandler = logging.StreamHandler() + console_logging_handler: logging.StreamHandler[TextIO] = logging.StreamHandler() if len(stream_handlers) == 0: # noinspection SpellCheckingInspection @@ -283,7 +283,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: return {"logging:console:log-level"} @classmethod - def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: YAML | None) -> set[str]: # noqa: E501 + def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the Discord log channel logging configuration. @@ -306,7 +306,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y else discord_channel_logging_settings["webhook_url"].data ) - discord_logging_handlers: set[DiscordHandler] = { + discord_logging_handlers: set[DiscordHandler] = { # type: ignore[no-any-unimported] handler for handler in logger.handlers if isinstance(handler, DiscordHandler) } if len(discord_logging_handlers) > 1: @@ -320,10 +320,11 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y discord_logging_handler_avatar_url: str | None = None if len(discord_logging_handlers) == 1: - existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() + existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() # type: ignore[no-any-unimported] ONLY_DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = bool( discord_channel_logging_settings is not None + and cls._most_recent_yaml is not None and cls._most_recent_yaml["logging"].get("discord-channel", None) is not None and all( value == cls._most_recent_yaml["logging"]["discord-channel"].get(key, None) @@ -334,7 +335,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y ) if ONLY_DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: DISCORD_LOG_CHANNEL_LOG_LEVEL_IS_SAME: Final[bool] = bool( - discord_channel_logging_settings["log-level"] == cls._most_recent_yaml[ + discord_channel_logging_settings["log-level"] == cls._most_recent_yaml[ # type: ignore[index] "logging" ]["discord-channel"]["log-level"] ) @@ -344,7 +345,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y ) existing_discord_logging_handler.setLevel( - getattr(logging, discord_channel_logging_settings["log-level"].data) + getattr(logging, discord_channel_logging_settings["log-level"].data) # type: ignore[index] ) return {"logging:discord-channel:log-level"} @@ -358,6 +359,9 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y elif len(discord_logging_handlers) == 0 and discord_channel_logging_settings is None: return set() + if discord_channel_logging_settings is None: + raise RuntimeError + discord_logging_handler: logging.Handler = DiscordHandler( discord_logging_handler_display_name, discord_channel_logging_settings["webhook-url"], @@ -376,7 +380,8 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y changed_settings: set[str] = {"logging:discord-channel:webhook-url"} DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = bool( - cls._most_recent_yaml["logging"].get("discord-channel", None) is None + cls._most_recent_yaml is None + or cls._most_recent_yaml["logging"].get("discord-channel", None) is None or discord_channel_logging_settings["log-level"] != cls._most_recent_yaml[ "logging" ]["discord-channel"]["log-level"] @@ -387,7 +392,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y return changed_settings @classmethod - def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: + def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the Discord bot-token. @@ -406,7 +411,7 @@ def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: return {"discord:bot-token"} @classmethod - def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: + def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the Discord main-guild ID. @@ -425,7 +430,7 @@ def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: return {"discord:main-guild-id"} @classmethod - def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: + def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the community-group full name. @@ -451,7 +456,7 @@ def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: return {"community-group:full-name"} @classmethod - def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: + def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the community-group short name. @@ -477,7 +482,7 @@ def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: return {"community-group:short-name"} @classmethod - def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) -> set[str]: + def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the link to allow people to purchase a membership. @@ -502,7 +507,7 @@ def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) return {"community-group:links:purchase-membership"} @classmethod - def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> set[str]: + def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the link to view the perks of getting a membership to join your community group. @@ -527,7 +532,7 @@ def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> se return {"community-group:links:membership-perks"} @classmethod - def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set[str]: + def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the link to view your community group's moderation document. @@ -548,7 +553,7 @@ def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set return {"community-group:links:moderation-document"} @classmethod - def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: + def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the url that points to the location of your community group's members-list. @@ -569,7 +574,7 @@ def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: return {"community-group:members-list:url"} @classmethod - def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cookie: YAML) -> set[str]: + def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cookie: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the auth session cookie used to authenticate to access your members-list. @@ -592,7 +597,7 @@ def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cook return {"community-group:members-list:auth-session-cookie"} @classmethod - def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str]: + def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the format regex matcher for IDs in your community group's members-list. @@ -613,7 +618,7 @@ def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str return {"community-group:members-list:id-format"} @classmethod - def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_probability: YAML) -> set[str]: # noqa: E501 + def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_probability: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the probability that the rarer response will show when using the ping command. @@ -636,7 +641,7 @@ def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_pro return {"commands:ping:easter-egg-probability"} @classmethod - def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) -> set[str]: # noqa: E501 + def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the number of days to lookback for statistics. @@ -659,7 +664,7 @@ def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) return {"commands:stats:lookback-days"} @classmethod - def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YAML) -> set[str]: # noqa: E501 + def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the set of roles used to display statistics about. @@ -680,7 +685,7 @@ def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YA return {"commands:stats:displayed-roles"} @classmethod - def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration: YAML) -> set[str]: # noqa: E501 + def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the duration to use when applying a timeout action for a strike increase. @@ -701,7 +706,7 @@ def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration return {"commands:strike:timeout-duration"} @classmethod - def _reload_strike_performed_manually_warning_location(cls, strike_performed_manually_warning_location: YAML) -> set[str]: # noqa: E501 + def _reload_strike_performed_manually_warning_location(cls, strike_performed_manually_warning_location: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the location to send warning messages when strikes are performed manually. @@ -724,7 +729,7 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man return {"commands:strike:performed-manually-warning-location"} @classmethod - def _reload_messages_language(cls, messages_language: YAML) -> set[str]: + def _reload_messages_language(cls, messages_language: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the selected language for messages to be sent in. @@ -743,7 +748,7 @@ def _reload_messages_language(cls, messages_language: YAML) -> set[str]: return {"messages-language"} @classmethod - def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders_enabled: YAML) -> set[str]: # noqa: E501 + def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders_enabled: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the flag for whether the "send-introduction-reminders" task is enabled. @@ -766,7 +771,7 @@ def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders return {"reminders:send-introduction-reminders:enabled"} @classmethod - def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_delay: YAML) -> set[str]: # noqa: E501 + def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_delay: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the amount of time to wait before sending introduction-reminders to a user. @@ -791,7 +796,7 @@ def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_d return {"reminders:send-introduction-reminders:delay"} @classmethod - def _reload_send_introduction_reminders_interval(cls, send_introduction_reminders_interval: YAML) -> set[str]: # noqa: E501 + def _reload_send_introduction_reminders_interval(cls, send_introduction_reminders_interval: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the interval of time between executing the task to send introduction-reminders. @@ -814,7 +819,7 @@ def _reload_send_introduction_reminders_interval(cls, send_introduction_reminder return {"reminders:send-introduction-reminders:interval"} @classmethod - def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabled: YAML) -> set[str]: # noqa: E501 + def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabled: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the flag for whether the "send-get-roles-reminders" task is enabled. @@ -837,7 +842,7 @@ def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabl return {"reminders:send-get-roles-reminders:enabled"} @classmethod - def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: YAML) -> set[str]: # noqa: E501 + def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the amount of time to wait before sending get-roles-reminders to a user. @@ -863,7 +868,7 @@ def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: return {"reminders:send-get-roles-reminders:delay"} @classmethod - def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_interval: YAML) -> set[str]: # noqa: E501 + def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_interval: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the interval of time between executing the task to send get-roles-reminders. diff --git a/config/_yaml/__init__.py b/config/_yaml/__init__.py index e3efeb799..85c2a48f1 100644 --- a/config/_yaml/__init__.py +++ b/config/_yaml/__init__.py @@ -65,7 +65,7 @@ "timeout-duration": DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, "performed-manually-warning-location": DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION, } -_DEFAULT_COMMANDS_SETTINGS: Final[Mapping[str, Mapping[str, float]]] = { +_DEFAULT_COMMANDS_SETTINGS: Final[Mapping[str, Mapping[str, float] | Mapping[str, float | Sequence[str]] | Mapping[str, str]]] = { # noqa: E501 "ping": _DEFAULT_PING_COMMAND_SETTINGS, "stats": _DEFAULT_STATS_COMMAND_SETTINGS, "strike": _DEFAULT_STRIKE_COMMAND_SETTINGS, @@ -215,7 +215,7 @@ ) -def load_yaml(raw_yaml: str, file_name: str = "tex-bot-deployment.yaml") -> YAML: +def load_yaml(raw_yaml: str, file_name: str = "tex-bot-deployment.yaml") -> YAML: # type: ignore[no-any-unimported] parsed_yaml: YAML = strictyaml.load(raw_yaml, SETTINGS_YAML_SCHEMA, label=file_name) # type: ignore[no-any-unimported] # noinspection SpellCheckingInspection diff --git a/config/_yaml/custom_schema_utils.py b/config/_yaml/custom_schema_utils.py index 9e9a5dc73..6bffa37e2 100644 --- a/config/_yaml/custom_schema_utils.py +++ b/config/_yaml/custom_schema_utils.py @@ -17,9 +17,9 @@ def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimp return slugify.slugify(str(chunk.contents)) -class SlugKeyMap(strictyaml.Map): +class SlugKeyMap(strictyaml.Map): # type: ignore[no-any-unimported,misc] @override - def __init__(self, validator: dict[object, object], key_validator: strictyaml.Validator | None = None) -> None: # noqa: E501 + def __init__(self, validator: dict[object, object], key_validator: strictyaml.Validator | None = None) -> None: # type: ignore[no-any-unimported,misc] # noqa: E501 super().__init__( validator=validator, key_validator=key_validator if key_validator is not None else SlugKeyValidator() diff --git a/config/_yaml/custom_validators.py b/config/_yaml/custom_validators.py index fc077f1bc..ff5fc1a1d 100644 --- a/config/_yaml/custom_validators.py +++ b/config/_yaml/custom_validators.py @@ -51,7 +51,7 @@ def validate_scalar(self, chunk: YAMLChunk) -> LogLevels: # type: ignore[no-any @override def to_yaml(self, data: object) -> str: # type: ignore[misc] self.should_be_string(data, "expected a valid log-level.") - str_data: str = data.upper().strip().strip("-").strip("_").strip(".") + str_data: str = data.upper().strip().strip("-").strip("_").strip(".") # type: ignore[attr-defined] if str_data not in LogLevels: raise YAMLSerializationError( @@ -83,13 +83,13 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] DATA_IS_VALID: Final[bool] = bool( ( self.__is_absolute_url(data) - and data.startswith("https://discord.com/api/webhooks/") + and data.startswith("https://discord.com/api/webhooks/") # type: ignore[attr-defined] ), ) if not DATA_IS_VALID: raise YAMLSerializationError(f"'{data}' is not a Discord webhook URL.") - return data + return data # type: ignore[return-value] class DiscordSnowflakeValidator(strictyaml.Int): # type: ignore[no-any-unimported,misc] @@ -118,7 +118,7 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return str(data) -class RegexMatcher(strictyaml.ScalarValidator): +class RegexMatcher(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] MATCHING_MESSAGE: str = "when expecting a regular expression matcher" @override @@ -130,8 +130,8 @@ def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimp self.MATCHING_MESSAGE, "found arbitrary string", ) - else: - return chunk.contents + + return chunk.contents # type: ignore[no-any-return] # noinspection PyOverrides @override @@ -139,11 +139,11 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] self.should_be_string(data, self.MATCHING_MESSAGE) try: - re.compile(data) + re.compile(data) # type: ignore[call-overload] except re.error: raise YAMLSerializationError(f"{self.MATCHING_MESSAGE} found '{data}'") - else: - return data + + return data # type: ignore[return-value] class ProbabilityValidator(strictyaml.Float): # type: ignore[no-any-unimported,misc] @@ -164,17 +164,17 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] ) if strictyaml_utils.has_number_type(data): - if not 0 <= data <= 100: + if not 0 <= data <= 100: # type: ignore[operator] raise YAML_SERIALIZATION_ERROR - if math.isnan(data): + if math.isnan(data): # type: ignore[arg-type] return "nan" if data == float("inf"): return "inf" if data == float("-inf"): return "-inf" - return str(data / 100) + return str(data / 100) # type: ignore[operator] if strictyaml_utils.is_string(data) and strictyaml_utils.is_decimal(data): float_data: float = float(str(data)) @@ -233,7 +233,7 @@ def _get_value_from_match(self, match: Match[str], key: str) -> float: raise float_conversion_error from float_conversion_error @override - def validate_scalar(self, chunk: YAMLChunk) -> timedelta: + def validate_scalar(self, chunk: YAMLChunk) -> timedelta: # type: ignore[no-any-unimported,misc] chunk_error_func: Callable[[], NoReturn] = functools.partial( chunk.expecting_but_found, expecting="when expecting a delay/interval string", @@ -257,7 +257,7 @@ def validate_scalar(self, chunk: YAMLChunk) -> timedelta: # noinspection PyOverrides @override - def to_yaml(self, data: object) -> str: + def to_yaml(self, data: object) -> str: # type: ignore[misc] if strictyaml_utils.is_string(data): match: Match[str] | None = self.regex_matcher.match(str(data)) if match is None: From 5e98c801d8db15d502189c65edf588875604a9b0 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 03:54:18 +0100 Subject: [PATCH 014/128] Fix some linting errors --- cogs/startup.py | 9 +-- config/__init__.py | 2 + config/_messages.py | 5 ++ config/_settings.py | 98 ++++++++++++++++------------- config/_yaml/__init__.py | 43 +++++++------ config/_yaml/custom_schema_utils.py | 3 +- config/_yaml/custom_validators.py | 51 +++++++++------ config/constants.py | 5 +- utils/__init__.py | 5 +- utils/tex_bot.py | 8 ++- 10 files changed, 131 insertions(+), 98 deletions(-) create mode 100644 config/_messages.py diff --git a/cogs/startup.py b/cogs/startup.py index 5fd51f194..9dc8bd2b3 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -43,11 +43,12 @@ def _setup_discord_log_channel(self) -> None: } if len(discord_logging_handlers) > 1: - raise ValueError( + CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE: Final[str] = ( "Cannot determine which logging Discord-webhook-handler to update." ) + raise ValueError(CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE) - elif len(discord_logging_handlers) == 1: + if len(discord_logging_handlers) == 1: existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() # type: ignore[no-any-unimported] logger.removeHandler(existing_discord_logging_handler) @@ -71,7 +72,7 @@ def _setup_discord_log_channel(self) -> None: ) new_discord_logging_handler.setLevel(existing_discord_logging_handler.level) new_discord_logging_handler.setFormatter( - existing_discord_logging_handler.formatter + existing_discord_logging_handler.formatter, ) new_discord_logging_handler.avatar_url = new_discord_logging_handler.avatar_url @@ -103,7 +104,7 @@ async def _get_main_guild(self) -> discord.Guild: settings["_DISCORD_MAIN_GUILD_ID"]), ) logger.critical(GuildDoesNotExistError( - guild_id=settings["_DISCORD_MAIN_GUILD_ID"]) + guild_id=settings["_DISCORD_MAIN_GUILD_ID"]), ) await self.bot.close() raise RuntimeError diff --git a/config/__init__.py b/config/__init__.py index a27f1e20f..0a7afc3d7 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -46,6 +46,8 @@ def run_setup() -> None: with contextlib.suppress(BotRequiresRestartAfterConfigChange): settings.reload() + # TODO: load messages here using language from settings + logger.debug("Begin database setup") importlib.import_module("db") diff --git a/config/_messages.py b/config/_messages.py new file mode 100644 index 000000000..6c3a68ff4 --- /dev/null +++ b/config/_messages.py @@ -0,0 +1,5 @@ +from collections.abc import Sequence + +__all__: Sequence[str] = () + +# TODO: load messages diff --git a/config/_settings.py b/config/_settings.py index e04e719ba..6e74d2ae0 100644 --- a/config/_settings.py +++ b/config/_settings.py @@ -22,9 +22,9 @@ from discord_logging.handler import DiscordHandler from strictyaml import YAML -from ._pre_startup_utils import is_running_in_async from exceptions import BotRequiresRestartAfterConfigChange +from ._pre_startup_utils import is_running_in_async from ._yaml import load_yaml from .constants import ( DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME, @@ -123,10 +123,13 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: Final[bool] = bool( "bot" in item.lower() and "token" in item.lower() - and is_running_in_async() + and is_running_in_async(), ) if ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: - raise RuntimeError(f"Cannot access {item!r} when TeX-Bot is already running.") + TEX_BOT_ALREADY_RUNNING_MESSAGE: Final[str] = ( + f"Cannot access {item!r} when TeX-Bot is already running." + ) + raise RuntimeError(TEX_BOT_ALREADY_RUNNING_MESSAGE) return self._settings[item] @@ -209,22 +212,22 @@ def reload(cls) -> None: ), cls._reload_messages_language(current_yaml["messages-language"]), cls._reload_send_introduction_reminders_enabled( - current_yaml["reminders"]["send-introduction-reminders"]["enabled"] + current_yaml["reminders"]["send-introduction-reminders"]["enabled"], ), cls._reload_send_introduction_reminders_delay( - current_yaml["reminders"]["send-introduction-reminders"]["delay"] + current_yaml["reminders"]["send-introduction-reminders"]["delay"], ), cls._reload_send_introduction_reminders_interval( - current_yaml["reminders"]["send-introduction-reminders"]["interval"] + current_yaml["reminders"]["send-introduction-reminders"]["interval"], ), cls._reload_send_get_roles_reminders_enabled( - current_yaml["reminders"]["send-get-roles-reminders"]["enabled"] + current_yaml["reminders"]["send-get-roles-reminders"]["enabled"], ), cls._reload_send_get_roles_reminders_delay( - current_yaml["reminders"]["send-get-roles-reminders"]["delay"] + current_yaml["reminders"]["send-get-roles-reminders"]["delay"], ), cls._reload_send_get_roles_reminders_interval( - current_yaml["reminders"]["send-get-roles-reminders"]["interval"] + current_yaml["reminders"]["send-get-roles-reminders"]["interval"], ), ) @@ -242,7 +245,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # """ CONSOLE_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or console_logging_settings != cls._most_recent_yaml["logging"]["console"] + or console_logging_settings != cls._most_recent_yaml["logging"]["console"], ) if not CONSOLE_LOGGING_SETTINGS_CHANGED: return set() @@ -254,7 +257,10 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # if isinstance(handler, logging.StreamHandler) } if len(stream_handlers) > 1: - raise ValueError("Cannot determine which logging stream-handler to update.") + CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE: Final[str] = ( + "Cannot determine which logging stream-handler to update." + ) + raise ValueError(CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE) console_logging_handler: logging.StreamHandler[TextIO] = logging.StreamHandler() @@ -277,7 +283,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # raise ValueError console_logging_handler.setLevel( - getattr(logging, console_logging_settings["log-level"].data) + getattr(logging, console_logging_settings["log-level"].data), ) return {"logging:console:log-level"} @@ -295,7 +301,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y "discord-channel", None, ) - or "DISCORD_LOG_CHANNEL_WEBHOOK_URL" not in cls._settings + or "DISCORD_LOG_CHANNEL_WEBHOOK_URL" not in cls._settings, ) if not DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: return set() @@ -310,9 +316,10 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y handler for handler in logger.handlers if isinstance(handler, DiscordHandler) } if len(discord_logging_handlers) > 1: - raise ValueError( + CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE: Final[str] = ( "Cannot determine which logging Discord-webhook-handler to update." ) + raise ValueError(CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE) discord_logging_handler_display_name: str = ( DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME @@ -331,21 +338,22 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y for key, value in discord_channel_logging_settings.items() if key != "log-level" - ) + ), ) if ONLY_DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: DISCORD_LOG_CHANNEL_LOG_LEVEL_IS_SAME: Final[bool] = bool( discord_channel_logging_settings["log-level"] == cls._most_recent_yaml[ # type: ignore[index] "logging" - ]["discord-channel"]["log-level"] + ]["discord-channel"]["log-level"], ) if DISCORD_LOG_CHANNEL_LOG_LEVEL_IS_SAME: - raise ValueError( + LOG_LEVEL_DIDNT_CHANGE_MESSAGE: Final[str] = ( "Assumed Discord log channel log level had changed, but it hadn't." ) + raise ValueError(LOG_LEVEL_DIDNT_CHANGE_MESSAGE) existing_discord_logging_handler.setLevel( - getattr(logging, discord_channel_logging_settings["log-level"].data) # type: ignore[index] + getattr(logging, discord_channel_logging_settings["log-level"].data), # type: ignore[index] ) return {"logging:discord-channel:log-level"} @@ -368,7 +376,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y avatar_url=discord_logging_handler_avatar_url, ) discord_logging_handler.setLevel( - getattr(logging, discord_channel_logging_settings["log-level"].data) + getattr(logging, discord_channel_logging_settings["log-level"].data), ) # noinspection SpellCheckingInspection discord_logging_handler.setFormatter( @@ -384,7 +392,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y or cls._most_recent_yaml["logging"].get("discord-channel", None) is None or discord_channel_logging_settings["log-level"] != cls._most_recent_yaml[ "logging" - ]["discord-channel"]["log-level"] + ]["discord-channel"]["log-level"], ) if DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: changed_settings.add("logging:discord-channel:log-level") @@ -401,7 +409,7 @@ def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: # type DISCORD_BOT_TOKEN_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_bot_token != cls._most_recent_yaml["discord"]["bot-token"] - or "DISCORD_BOT_TOKEN" not in cls._settings + or "DISCORD_BOT_TOKEN" not in cls._settings, ) if not DISCORD_BOT_TOKEN_CHANGED: return set() @@ -420,7 +428,7 @@ def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: DISCORD_MAIN_GUILD_ID_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_main_guild_id != cls._most_recent_yaml["discord"]["main-guild-id"] - or "_DISCORD_MAIN_GUILD_ID" not in cls._settings + or "_DISCORD_MAIN_GUILD_ID" not in cls._settings, ) if not DISCORD_MAIN_GUILD_ID_CHANGED: return set() @@ -442,7 +450,7 @@ def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # t "full-name", None, ) - or "_GROUP_FULL_NAME" not in cls._settings + or "_GROUP_FULL_NAME" not in cls._settings, ) if not GROUP_FULL_NAME_CHANGED: return set() @@ -468,7 +476,7 @@ def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # "short-name", None, ) - or "_GROUP_SHORT_NAME" not in cls._settings + or "_GROUP_SHORT_NAME" not in cls._settings, ) if not GROUP_SHORT_NAME_CHANGED: return set() @@ -482,7 +490,7 @@ def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # return {"community-group:short-name"} @classmethod - def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the link to allow people to purchase a membership. @@ -493,7 +501,7 @@ def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) or purchase_membership_link != cls._most_recent_yaml["community-group"][ "links" ].get("purchase-membership", None) - or "PURCHASE_MEMBERSHIP_LINK" not in cls._settings + or "PURCHASE_MEMBERSHIP_LINK" not in cls._settings, ) if not PURCHASE_MEMBERSHIP_LINK_CHANGED: return set() @@ -518,7 +526,7 @@ def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> se or membership_perks_link != cls._most_recent_yaml["community-group"][ "links" ].get("membership-perks", None) - or "MEMBERSHIP_PERKS_LINK" not in cls._settings + or "MEMBERSHIP_PERKS_LINK" not in cls._settings, ) if not MEMBERSHIP_PERKS_LINK_CHANGED: return set() @@ -543,7 +551,7 @@ def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set or moderation_document_link != cls._most_recent_yaml["community-group"]["links"][ "moderation-document" ] - or "MODERATION_DOCUMENT_LINK" not in cls._settings + or "MODERATION_DOCUMENT_LINK" not in cls._settings, ) if not MODERATION_DOCUMENT_LINK_CHANGED: return set() @@ -564,7 +572,7 @@ def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: or members_list_url != cls._most_recent_yaml["community-group"]["members-list"][ "url" ] - or "MEMBERS_LIST_URL" not in cls._settings + or "MEMBERS_LIST_URL" not in cls._settings, ) if not MEMBERS_LIST_URL_CHANGED: return set() @@ -574,7 +582,7 @@ def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: return {"community-group:members-list:url"} @classmethod - def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cookie: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cookie: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 """ Reload the auth session cookie used to authenticate to access your members-list. @@ -585,7 +593,7 @@ def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cook or members_list_auth_session_cookie != cls._most_recent_yaml["community-group"][ "members-list" ]["auth-session-cookie"] - or "MEMBERS_LIST_AUTH_SESSION_COOKIE" not in cls._settings + or "MEMBERS_LIST_AUTH_SESSION_COOKIE" not in cls._settings, ) if not MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: return set() @@ -608,7 +616,7 @@ def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str or members_list_id_format != cls._most_recent_yaml["community-group"][ "members-list" ]["id-format"] - or "MEMBERS_LIST_ID_FORMAT" not in cls._settings + or "MEMBERS_LIST_ID_FORMAT" not in cls._settings, ) if not MEMBERS_LIST_ID_FORMAT_CHANGED: return set() @@ -629,7 +637,7 @@ def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_pro or ping_command_easter_egg_probability != cls._most_recent_yaml["commands"][ "ping" ]["easter-egg-probability"] - or "PING_COMMAND_EASTER_EGG_PROBABILITY" not in cls._settings + or "PING_COMMAND_EASTER_EGG_PROBABILITY" not in cls._settings, ) if not PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: return set() @@ -652,13 +660,13 @@ def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) or stats_command_lookback_days != cls._most_recent_yaml["commands"][ "stats" ]["lookback-days"] - or "STATS_COMMAND_LOOKBACK_DAYS" not in cls._settings + or "STATS_COMMAND_LOOKBACK_DAYS" not in cls._settings, ) if not STATS_COMMAND_LOOKBACK_DAYS_CHANGED: return set() cls._settings["STATS_COMMAND_LOOKBACK_DAYS"] = timedelta( - days=stats_command_lookback_days.data + days=stats_command_lookback_days.data, ) return {"commands:stats:lookback-days"} @@ -675,7 +683,7 @@ def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YA or stats_command_displayed_roles != cls._most_recent_yaml["commands"][ "stats" ]["displayed-roles"] - or "STATS_COMMAND_DISPLAYED_ROLES" not in cls._settings + or "STATS_COMMAND_DISPLAYED_ROLES" not in cls._settings, ) if not STATS_COMMAND_DISPLAYED_ROLES_CHANGED: return set() @@ -696,7 +704,7 @@ def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration or strike_command_timeout_duration != cls._most_recent_yaml["commands"][ "strike" ]["timeout-duration"] - or "STRIKE_COMMAND_TIMEOUT_DURATION" not in cls._settings + or "STRIKE_COMMAND_TIMEOUT_DURATION" not in cls._settings, ) if not STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: return set() @@ -717,7 +725,7 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man or strike_performed_manually_warning_location != cls._most_recent_yaml["commands"][ "strike" ]["performed-manually-warning-location"] - or "STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION" not in cls._settings + or "STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION" not in cls._settings, ) if not STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: return set() @@ -738,7 +746,7 @@ def _reload_messages_language(cls, messages_language: YAML) -> set[str]: # type MESSAGES_LANGUAGE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or messages_language != cls._most_recent_yaml["messages-language"] - or "MESSAGES_LANGUAGE" not in cls._settings + or "MESSAGES_LANGUAGE" not in cls._settings, ) if not MESSAGES_LANGUAGE_CHANGED: return set() @@ -759,7 +767,7 @@ def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders or send_introduction_reminders_enabled != cls._most_recent_yaml["reminders"][ "send-introduction-reminders" ]["enabled"] - or "SEND_INTRODUCTION_REMINDERS_ENABLED" not in cls._settings + or "SEND_INTRODUCTION_REMINDERS_ENABLED" not in cls._settings, ) if not SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: return set() @@ -784,7 +792,7 @@ def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_d or send_introduction_reminders_delay != cls._most_recent_yaml["reminders"][ "send-introduction-reminders" ]["delay"] - or "SEND_INTRODUCTION_REMINDERS_DELAY" not in cls._settings + or "SEND_INTRODUCTION_REMINDERS_DELAY" not in cls._settings, ) if not SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: return set() @@ -807,7 +815,7 @@ def _reload_send_introduction_reminders_interval(cls, send_introduction_reminder or send_introduction_reminders_interval != cls._most_recent_yaml["reminders"][ "send-introduction-reminders" ]["interval"] - or "SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS" not in cls._settings + or "SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS" not in cls._settings, ) if not SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: return set() @@ -830,7 +838,7 @@ def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabl or send_get_roles_reminders_enabled != cls._most_recent_yaml["reminders"][ "send-get-roles-reminders" ]["enabled"] - or "SEND_GET_ROLES_REMINDERS_ENABLED" not in cls._settings + or "SEND_GET_ROLES_REMINDERS_ENABLED" not in cls._settings, ) if not SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: return set() @@ -856,7 +864,7 @@ def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: or send_get_roles_reminders_delay != cls._most_recent_yaml["reminders"][ "send-get-roles-reminders" ]["delay"] - or "SEND_GET_ROLES_REMINDERS_DELAY" not in cls._settings + or "SEND_GET_ROLES_REMINDERS_DELAY" not in cls._settings, ) if not SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: return set() @@ -879,7 +887,7 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte or send_get_roles_reminders_interval != cls._most_recent_yaml["reminders"][ "send-get-roles-reminders" ]["interval"] - or "SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS" not in cls._settings + or "SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS" not in cls._settings, ) if not SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: return set() diff --git a/config/_yaml/__init__.py b/config/_yaml/__init__.py index 85c2a48f1..d82d766f5 100644 --- a/config/_yaml/__init__.py +++ b/config/_yaml/__init__.py @@ -17,36 +17,36 @@ import strictyaml from strictyaml import YAML -from ..constants import ( - LogLevels, - SendIntroductionRemindersFlagType, +from config.constants import ( DEFAULT_CONSOLE_LOG_LEVEL, DEFAULT_DISCORD_LOGGING_LOG_LEVEL, - TRANSLATED_MESSAGES_LOCALE_CODES, - DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY, DEFAULT_MEMBERS_LIST_ID_FORMAT, - DEFAULT_STATS_COMMAND_LOOKBACK_DAYS, + DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY, + DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, + DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED, + DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, + DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, + DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED, + DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, DEFAULT_STATS_COMMAND_DISPLAYED_ROLES, + DEFAULT_STATS_COMMAND_LOOKBACK_DAYS, DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION, - DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED, - DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, - DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, - DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED, - DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, - DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, + TRANSLATED_MESSAGES_LOCALE_CODES, + LogLevels, + SendIntroductionRemindersFlagType, ) + +from .custom_schema_utils import SlugKeyMap from .custom_validators import ( + DiscordSnowflakeValidator, DiscordWebhookURLValidator, - TimeDeltaValidator, LogLevelValidator, - RegexMatcher, - DiscordSnowflakeValidator, ProbabilityValidator, + RegexMatcher, SendIntroductionRemindersFlagValidator, + TimeDeltaValidator, ) -from .custom_schema_utils import SlugKeyMap - _DEFAULT_CONSOLE_LOGGING_SETTINGS: Final[Mapping[str, LogLevels]] = { "log-level": DEFAULT_CONSOLE_LOG_LEVEL, @@ -70,7 +70,7 @@ "stats": _DEFAULT_STATS_COMMAND_SETTINGS, "strike": _DEFAULT_STRIKE_COMMAND_SETTINGS, } -_DEFAULT_SEND_INTRODUCTION_REMINDERS_SETTINGS: Final[Mapping[str, SendIntroductionRemindersFlagType | str]] = { +_DEFAULT_SEND_INTRODUCTION_REMINDERS_SETTINGS: Final[Mapping[str, SendIntroductionRemindersFlagType | str]] = { # noqa: E501 "enabled": DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED, "delay": DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, "interval": DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, @@ -119,7 +119,10 @@ "community-group": SlugKeyMap( { strictyaml.Optional("full-name"): strictyaml.Regex( - r"\A(?!.*['&!?:,.#%\"-]['&!?:,.#%\"-].*)(?!.* .*)(?:[A-Za-z0-9 '&!?:,.#%\"-]+)\Z", + ( + r"\A(?!.*['&!?:,.#%\"-]['&!?:,.#%\"-].*)(?!.* .*)" + r"(?:[A-Za-z0-9 '&!?:,.#%\"-]+)\Z" + ), ), strictyaml.Optional("short-name"): strictyaml.Regex( r"\A(?!.*['&!?:,.#%\"-]['&!?:,.#%\"-].*)(?:[A-Za-z0-9'&!?:,.#%\"-]+)\Z", @@ -129,7 +132,7 @@ strictyaml.Optional("purchase-membership"): strictyaml.Url(), strictyaml.Optional("membership-perks"): strictyaml.Url(), "moderation-document": strictyaml.Url(), - } + }, ), "members-list": SlugKeyMap( { diff --git a/config/_yaml/custom_schema_utils.py b/config/_yaml/custom_schema_utils.py index 6bffa37e2..45037377d 100644 --- a/config/_yaml/custom_schema_utils.py +++ b/config/_yaml/custom_schema_utils.py @@ -7,7 +7,6 @@ import slugify import strictyaml - from strictyaml.yamllocation import YAMLChunk @@ -22,5 +21,5 @@ class SlugKeyMap(strictyaml.Map): # type: ignore[no-any-unimported,misc] def __init__(self, validator: dict[object, object], key_validator: strictyaml.Validator | None = None) -> None: # type: ignore[no-any-unimported,misc] # noqa: E501 super().__init__( validator=validator, - key_validator=key_validator if key_validator is not None else SlugKeyValidator() + key_validator=key_validator if key_validator is not None else SlugKeyValidator(), ) diff --git a/config/_yaml/custom_validators.py b/config/_yaml/custom_validators.py index ff5fc1a1d..5400d4989 100644 --- a/config/_yaml/custom_validators.py +++ b/config/_yaml/custom_validators.py @@ -14,9 +14,10 @@ import functools import math import re +from collections.abc import Callable from datetime import timedelta -from typing import Final, NoReturn, override, Callable from re import Match +from typing import Final, NoReturn, override import strictyaml from strictyaml import constants as strictyaml_constants @@ -24,9 +25,9 @@ from strictyaml.exceptions import YAMLSerializationError from strictyaml.yamllocation import YAMLChunk -from ..constants import ( - LogLevels, +from config.constants import ( VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES, + LogLevels, SendIntroductionRemindersFlagType, ) @@ -54,9 +55,10 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] str_data: str = data.upper().strip().strip("-").strip("_").strip(".") # type: ignore[attr-defined] if str_data not in LogLevels: - raise YAMLSerializationError( - f"Got {data} when expecting one of: \"{"\", \"".join(LogLevels)}\".", + INVALID_DATA_MESSAGE: Final[str] = ( + f"Got {data} when expecting one of: \"{"\", \"".join(LogLevels)}\"." ) + raise YAMLSerializationError(INVALID_DATA_MESSAGE) return str_data @@ -87,7 +89,8 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] ), ) if not DATA_IS_VALID: - raise YAMLSerializationError(f"'{data}' is not a Discord webhook URL.") + INVALID_DATA_MESSAGE: Final[str] = f"'{data}' is not a Discord webhook URL." + raise YAMLSerializationError(INVALID_DATA_MESSAGE) return data # type: ignore[return-value] @@ -113,7 +116,8 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] ), ) if not DATA_IS_VALID: - raise YAMLSerializationError(f"'{data}' is not a Discord snowflake ID.") + INVALID_DATA_MESSAGE: Final[str] = f"'{data}' is not a Discord snowflake ID." + raise YAMLSerializationError(INVALID_DATA_MESSAGE) return str(data) @@ -138,10 +142,12 @@ def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimp def to_yaml(self, data: object) -> str: # type: ignore[misc] self.should_be_string(data, self.MATCHING_MESSAGE) + regex_error: re.error try: re.compile(data) # type: ignore[call-overload] - except re.error: - raise YAMLSerializationError(f"{self.MATCHING_MESSAGE} found '{data}'") + except re.error as regex_error: + INVALID_DATA_MESSAGE: Final[str] = f"{self.MATCHING_MESSAGE} found '{data}'" + raise YAMLSerializationError(INVALID_DATA_MESSAGE) from regex_error return data # type: ignore[return-value] @@ -194,9 +200,11 @@ def __init__(self, *, minutes: bool = True, hours: bool = True, days: bool = Fal time_resolution_name: str for time_resolution_name in ("seconds", "minutes", "hours", "days", "weeks"): - time_resolution_name = time_resolution_name.lower().strip() + formatted_time_resolution_name: str = time_resolution_name.lower().strip() time_resolution: object = ( - True if time_resolution_name == "seconds" else locals()[time_resolution_name] + True + if formatted_time_resolution_name == "seconds" + else locals()[formatted_time_resolution_name] ) if not isinstance(time_resolution, bool): @@ -207,9 +215,9 @@ def __init__(self, *, minutes: bool = True, hours: bool = True, days: bool = Fal regex_matcher += ( r"(?:(?P<" - + time_resolution_name + + formatted_time_resolution_name + r">(?:\d*\.)?\d+)" - + time_resolution_name[0] + + formatted_time_resolution_name[0] + ")?" ) @@ -218,7 +226,7 @@ def __init__(self, *, minutes: bool = True, hours: bool = True, days: bool = Fal self.regex_matcher: re.Pattern[str] = re.compile(regex_matcher) def _get_value_from_match(self, match: Match[str], key: str) -> float: - if key not in self.regex_matcher.groupindex.keys(): + if key not in self.regex_matcher.groupindex: return 0.0 value: str | None = match.group(key) @@ -261,17 +269,19 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] if strictyaml_utils.is_string(data): match: Match[str] | None = self.regex_matcher.match(str(data)) if match is None: - raise YAMLSerializationError( + INVALID_DATA_MESSAGE: Final[str] = ( f"when expecting a delay/interval string found {str(data)!r}." ) + raise YAMLSerializationError(INVALID_DATA_MESSAGE) return str(data) - if not hasattr(data, "total_seconds") or not callable(getattr(data, "total_seconds")): - raise YAMLSerializationError( + if not hasattr(data, "total_seconds") or not callable(getattr(data, "total_seconds")): # noqa: B009 + INVALID_DATA_MESSAGE: Final[str] = ( f"when expecting a time delta object found {str(data)!r}." ) + raise YAMLSerializationError(INVALID_DATA_MESSAGE) - total_seconds: object = getattr(data, "total_seconds")() + total_seconds: object = getattr(data, "total_seconds")() # noqa: B009 if not isinstance(total_seconds, float): raise TypeError @@ -307,9 +317,10 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return "Once" if data else "False" if str(data).lower() not in VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES: - raise YAMLSerializationError( - f"Got {data} when expecting one of: \"Once\", \"Interval\" or \"False\".", + INVALID_DATA_MESSAGE: Final[str] = ( + f"Got {data} when expecting one of: \"Once\", \"Interval\" or \"False\"." ) + raise YAMLSerializationError(INVALID_DATA_MESSAGE) if str(data).lower() in strictyaml_constants.TRUE_VALUES: return "Once" diff --git a/config/constants.py b/config/constants.py index 414d3e3a2..a7b57f208 100644 --- a/config/constants.py +++ b/config/constants.py @@ -29,16 +29,15 @@ from enum import Enum, EnumMeta from pathlib import Path -from typing import Final, TypeAlias, Literal +from typing import Final, Literal, TypeAlias from strictyaml import constants as strictyaml_constants - SendIntroductionRemindersFlagType: TypeAlias = Literal["once", "interval", False] class MetaEnum(EnumMeta): - def __contains__(cls, item: object) -> bool: + def __contains__(cls, item: object) -> bool: # noqa: N805 try: cls(item) except ValueError: diff --git a/utils/__init__.py b/utils/__init__.py index d71ef2cee..71ed168da 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -19,14 +19,15 @@ import discord +# noinspection PyProtectedMember +from config._pre_startup_utils import is_running_in_async + from .command_checks import CommandChecks from .message_sender_components import MessageSenderComponent from .suppress_traceback import SuppressTraceback from .tex_bot import TeXBot, TeXBotExitReason from .tex_bot_base_cog import TeXBotBaseCog from .tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext -# noinspection PyProtectedMember -from config._pre_startup_utils import is_running_in_async def generate_invite_url(discord_bot_application_id: int, discord_guild_id: int) -> str: diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 405312f9b..2a6ecb796 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -394,17 +394,21 @@ async def perform_restart_after_config_changes(self) -> None: def reset_exit_reason(self) -> None: """Reset the exit reason of TeX-Bot back to `UNKNOWN_ERROR`.""" if utils.is_running_in_async(): - raise RuntimeError("Cannot reset exit reason when TeX-Bot is currently running.") + TEX_BOT_STILL_RUNNING_MESSAGE: Final[str] = ( + "Cannot reset exit reason when TeX-Bot is currently running." + ) + raise RuntimeError(TEX_BOT_STILL_RUNNING_MESSAGE) RESETABLE_EXIT_REASONS: Collection[TeXBotExitReason] = ( TeXBotExitReason.UNKNOWN_ERROR, TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG, ) if self.EXIT_REASON not in RESETABLE_EXIT_REASONS: - raise RuntimeError( + CURRENT_EXIT_REASON_IS_INVALID_MESSAGE: Final[str] = ( "Cannot reset exit reason, due to incorrect current exit reason. " "Invalid state." ) + raise RuntimeError(CURRENT_EXIT_REASON_IS_INVALID_MESSAGE) self._exit_reason = TeXBotExitReason.UNKNOWN_ERROR From 1e098c5593ace770af1984bbe2a3610e91152091 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 04:09:52 +0100 Subject: [PATCH 015/128] Improve `on_ready` event listener --- cogs/startup.py | 107 ++++++++++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/cogs/startup.py b/cogs/startup.py index 9dc8bd2b3..6de8c7164 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -87,7 +87,7 @@ def _setup_discord_log_channel(self) -> None: else: raise ValueError - async def _get_main_guild(self) -> discord.Guild: + async def _initialise_main_guild(self) -> None: try: main_guild: discord.Guild | None = self.bot.main_guild except GuildDoesNotExistError: @@ -109,28 +109,48 @@ async def _get_main_guild(self) -> discord.Guild: await self.bot.close() raise RuntimeError - return main_guild + async def _check_strike_performed_manually_warning_location_exists(self) -> None: + if settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] == "DM": + return - @TeXBotBaseCog.listener() - async def on_ready(self) -> None: - """ - Populate the shortcut accessors of the bot after initialisation. + STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_EXISTS: Final[bool] = bool( + discord.utils.get( + self.bot.main_guild.text_channels, + name=settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"], + ), + ) + if STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_EXISTS: + return + + logger.critical( + ( + "The channel %s does not exist, so cannot be used as the location " + "for sending manual-moderation warning messages" + ), + repr(settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"]), + ) - Shortcut accessors should only be populated once the bot is ready to make API requests. - """ - self._setup_discord_log_channel() + strike_performed_manually_warning_location_similar_to_dm: bool = ( + settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"].lower() + in ("dm", "dms") + ) + if strike_performed_manually_warning_location_similar_to_dm: + logger.info( + ( + "If you meant to set the location " + "for sending manual-moderation warning messages to be " + "the DMs of the committee member that applied " + "the manual moderation action, use the value of %s" + ), + repr("DM"), + ) - main_guild: discord.Guild = await self._get_main_guild() + await self.bot.close() - if self.bot.application_id: - logger.debug( - "Invite URL: %s", - utils.generate_invite_url( - self.bot.application_id, - settings["_DISCORD_MAIN_GUILD_ID"]), - ) + async def _check_all_shortcut_accessors(self) -> None: + main_guild: discord.Guild = self.bot.main_guild - if not discord.utils.get(main_guild.roles, name="Committee"): # TODO: Move to separate functions + if not discord.utils.get(main_guild.roles, name="Committee"): logger.warning(CommitteeRoleDoesNotExistError()) if not discord.utils.get(main_guild.roles, name="Guest"): @@ -148,36 +168,27 @@ async def on_ready(self) -> None: if not discord.utils.get(main_guild.text_channels, name="general"): logger.warning(GeneralChannelDoesNotExistError()) - if settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] != "DM": - strike_performed_manually_warning_location_exists: bool = bool( - discord.utils.get( - main_guild.text_channels, - name=settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"], - ), + await self._check_strike_performed_manually_warning_location_exists() + + @TeXBotBaseCog.listener() + async def on_ready(self) -> None: + """ + Populate the shortcut accessors of the bot after initialisation. + + Shortcut accessors should only be populated once the bot is ready to make API requests. + """ + self._setup_discord_log_channel() + + await self._initialise_main_guild() + + if self.bot.application_id: + logger.debug( + "Invite URL: %s", + utils.generate_invite_url( + self.bot.application_id, + settings["_DISCORD_MAIN_GUILD_ID"]), ) - if not strike_performed_manually_warning_location_exists: - logger.critical( - ( - "The channel %s does not exist, so cannot be used as the location " - "for sending manual-moderation warning messages" - ), - repr(settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"]), - ) - strike_performed_manually_warning_location_similar_to_dm: bool = ( - settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"].lower() - in ("dm", "dms") - ) - if strike_performed_manually_warning_location_similar_to_dm: - logger.info( - ( - "If you meant to set the location " - "for sending manual-moderation warning messages to be " - "the DMs of the committee member that applied " - "the manual moderation action, use the value of %s" - ), - repr("DM"), - ) - await self.bot.close() - return + + await self._check_all_shortcut_accessors() logger.info("Ready! Logged in as %s", self.bot.user) From f54db19cf94830d00d7b931b0c762b6f0ba1f295 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 04:10:25 +0100 Subject: [PATCH 016/128] Fix redefined final exception message --- config/_yaml/custom_validators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/_yaml/custom_validators.py b/config/_yaml/custom_validators.py index 5400d4989..3259461c6 100644 --- a/config/_yaml/custom_validators.py +++ b/config/_yaml/custom_validators.py @@ -269,17 +269,17 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] if strictyaml_utils.is_string(data): match: Match[str] | None = self.regex_matcher.match(str(data)) if match is None: - INVALID_DATA_MESSAGE: Final[str] = ( + INVALID_STRING_DATA_MESSAGE: Final[str] = ( f"when expecting a delay/interval string found {str(data)!r}." ) - raise YAMLSerializationError(INVALID_DATA_MESSAGE) + raise YAMLSerializationError(INVALID_STRING_DATA_MESSAGE) return str(data) if not hasattr(data, "total_seconds") or not callable(getattr(data, "total_seconds")): # noqa: B009 - INVALID_DATA_MESSAGE: Final[str] = ( + INVALID_TIMEDELTA_DATA_MESSAGE: Final[str] = ( f"when expecting a time delta object found {str(data)!r}." ) - raise YAMLSerializationError(INVALID_DATA_MESSAGE) + raise YAMLSerializationError(INVALID_TIMEDELTA_DATA_MESSAGE) total_seconds: object = getattr(data, "total_seconds")() # noqa: B009 if not isinstance(total_seconds, float): From 154e4e04c4530768c58dcfe79740d710db6c30d4 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 04:27:59 +0100 Subject: [PATCH 017/128] Fixes --- cogs/induct.py | 2 +- cogs/strike.py | 2 +- config/__init__.py | 745 +++++--------------------------------------- config/_messages.py | 1 + 4 files changed, 78 insertions(+), 672 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index 05d2f4173..c1c8a0a74 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -117,7 +117,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) "access to member only events:calendar_spiral: " f"& a cool green name on the {self.bot.group_short_name} Discord server" ":green_square:! " - f"Checkout all the perks at {settings["MEMBERSHIP_PERKS_URL"]}", + f"Checkout all the perks at {settings["MEMBERSHIP_PERKS_LINKS"]}", ) diff --git a/cogs/strike.py b/cogs/strike.py index 19434af2f..ebb3c97ee 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -238,7 +238,7 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me "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 " - f"[here](<{settings.MODERATION_DOCUMENT_URL}>)\nPlease ensure you have read " + f"[here](<{settings.MODERATION_DOCUMENT_LINK}>)\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.", diff --git a/config/__init__.py b/config/__init__.py index 0a7afc3d7..e604dc462 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -89,6 +89,13 @@ def check_for_deprecated_environment_variables() -> None: "GROUP_SHORT_NAME", "PURCHASE_MEMBERSHIP_URL", "PURCHASE_MEMBERSHIP_LINK", + "PURCHASE_MEMBERSHIP_WEBSITE", + "PURCHASE_MEMBERSHIP_INFO", + "MEMBERSHIP_PERKS_URL", + "MEMBERSHIP_PERKS_LINK", + "MEMBERSHIP_PERKS", + "MEMBERSHIP_PERKS_WEBSITE", + "MEMBERSHIP_PERKS_INFO", "CONSOLE_LOG_LEVEL", "MEMBERS_LIST_URL", "MEMBERS_LIST_LIST", @@ -142,678 +149,76 @@ def check_for_deprecated_environment_variables() -> None: raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR -# VALID_SEND_INTRODUCTION_REMINDERS_VALUES: Final[frozenset[str]] = frozenset( -# {"once"} | TRUE_VALUES | FALSE_VALUES, -# ) -# DEFAULT_STATISTICS_ROLES: Final[frozenset[str]] = frozenset( -# { -# "Committee", -# "Committee-Elect", -# "Student Rep", -# "Member", -# "Guest", -# "Server Booster", -# "Foundation Year", -# "First Year", -# "Second Year", -# "Final Year", -# "Year In Industry", -# "Year Abroad", -# "PGT", -# "PGR", -# "Alumnus/Alumna", -# "Postdoc", -# "Quiz Victor", -# }, -# ) -# LOG_LEVEL_CHOICES: Final[Sequence[str]] = ( -# "DEBUG", -# "INFO", -# "WARNING", -# "ERROR", -# "CRITICAL", -# ) +# @classmethod +# @functools.lru_cache(maxsize=5) +# def _get_messages_dict(cls, raw_messages_file_path: str | None) -> Mapping[str, object]: +# JSON_DECODING_ERROR_MESSAGE: Final[str] = ( +# "Messages JSON file must contain a JSON string that can be decoded " +# "into a Python dict object." +# ) # +# messages_file_path: Path = ( +# Path(raw_messages_file_path) +# if raw_messages_file_path +# else PROJECT_ROOT / Path("messages.json") +# ) # -# class Settings(abc.ABC): -# """ -# Settings class that provides access to all settings values. -# -# Settings values can be accessed via key (like a dictionary) or via class attribute. -# """ -# -# _is_env_variables_setup: ClassVar[bool] -# _settings: ClassVar[dict[str, object]] -# -# @classmethod -# def get_invalid_settings_key_message(cls, item: str) -> str: -# """Return the message to state that the given settings key is invalid.""" -# return f"{item!r} is not a valid settings key." -# -# def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 -# """Retrieve settings value by attribute lookup.""" -# MISSING_ATTRIBUTE_MESSAGE: Final[str] = ( -# f"{type(self).__name__!r} object has no attribute {item!r}" +# if not messages_file_path.is_file(): +# MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE: Final[str] = ( +# "MESSAGES_FILE_PATH must be a path to a file that exists." # ) +# raise ImproperlyConfiguredError(MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE) # -# if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values -# raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) -# -# if not self._is_env_variables_setup: -# self._setup_env_variables() -# -# if item in self._settings: -# return self._settings[item] -# -# if re.match(r"\A[A-Z](?:[A-Z_]*[A-Z])?\Z", item): -# INVALID_SETTINGS_KEY_MESSAGE: Final[str] = self.get_invalid_settings_key_message( -# item, -# ) -# raise AttributeError(INVALID_SETTINGS_KEY_MESSAGE) -# -# raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) -# -# def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 -# """Retrieve settings value by key lookup.""" -# attribute_not_exist_error: AttributeError +# messages_file: IO[str] +# with messages_file_path.open(encoding="utf8") as messages_file: +# e: json.JSONDecodeError # try: -# return getattr(self, item) -# except AttributeError as attribute_not_exist_error: -# key_error_message: str = item -# -# if self.get_invalid_settings_key_message(item) in str(attribute_not_exist_error): -# key_error_message = str(attribute_not_exist_error) -# -# raise KeyError(key_error_message) from None -# -# @staticmethod -# def _setup_logging() -> None: -# raw_console_log_level: str = str(os.getenv("CONSOLE_LOG_LEVEL", "INFO")).upper() -# -# if raw_console_log_level not in LOG_LEVEL_CHOICES: -# INVALID_LOG_LEVEL_MESSAGE: Final[str] = f"""LOG_LEVEL must be one of { -# ",".join(f"{log_level_choice!r}" -# for log_level_choice -# in LOG_LEVEL_CHOICES[:-1]) -# } or {LOG_LEVEL_CHOICES[-1]!r}.""" -# raise ImproperlyConfiguredError(INVALID_LOG_LEVEL_MESSAGE) -# -# logger.setLevel(getattr(logging, raw_console_log_level)) -# -# console_logging_handler: logging.Handler = logging.StreamHandler() -# # noinspection SpellCheckingInspection -# console_logging_handler.setFormatter( -# logging.Formatter("{asctime} | {name} | {levelname:^8} - {message}", style="{"), -# ) -# -# logger.addHandler(console_logging_handler) -# logger.propagate = False -# -# @classmethod -# def _setup_discord_bot_token(cls) -> None: -# raw_discord_bot_token: str | None = os.getenv("DISCORD_BOT_TOKEN") -# -# DISCORD_BOT_TOKEN_IS_VALID: Final[bool] = bool( -# raw_discord_bot_token -# and re.match( -# r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", -# raw_discord_bot_token, -# ), -# ) -# if not DISCORD_BOT_TOKEN_IS_VALID: -# INVALID_DISCORD_BOT_TOKEN_MESSAGE: Final[str] = ( -# "DISCORD_BOT_TOKEN must be a valid Discord bot token " -# "(see https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts)." -# ) -# raise ImproperlyConfiguredError(INVALID_DISCORD_BOT_TOKEN_MESSAGE) -# -# cls._settings["DISCORD_BOT_TOKEN"] = raw_discord_bot_token -# -# @classmethod -# def _setup_discord_log_channel_webhook_url(cls) -> None: -# raw_discord_log_channel_webhook_url: str = os.getenv( -# "DISCORD_LOG_CHANNEL_WEBHOOK_URL", -# "", -# ) -# -# DISCORD_LOG_CHANNEL_WEBHOOK_URL_IS_VALID: Final[bool] = bool( -# not raw_discord_log_channel_webhook_url -# or ( -# validators.url(raw_discord_log_channel_webhook_url) -# and raw_discord_log_channel_webhook_url.startswith( -# "https://discord.com/api/webhooks/", -# ) -# ), -# ) -# if not DISCORD_LOG_CHANNEL_WEBHOOK_URL_IS_VALID: -# INVALID_DISCORD_LOG_CHANNEL_WEBHOOK_URL_MESSAGE: Final[str] = ( -# "DISCORD_LOG_CHANNEL_WEBHOOK_URL must be a valid webhook URL " -# "that points to a discord channel where logs should be displayed." -# ) -# raise ImproperlyConfiguredError(INVALID_DISCORD_LOG_CHANNEL_WEBHOOK_URL_MESSAGE) -# -# cls._settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"] = raw_discord_log_channel_webhook_url -# -# @classmethod -# def _setup_discord_guild_id(cls) -> None: -# raw_discord_guild_id: str | None = os.getenv("DISCORD_GUILD_ID") -# -# 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), -# ) -# if not DISCORD_GUILD_ID_IS_VALID: -# INVALID_DISCORD_GUILD_ID_MESSAGE: Final[str] = ( -# "DISCORD_GUILD_ID must be a valid Discord guild ID " -# "(see https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id)." -# ) -# raise ImproperlyConfiguredError(INVALID_DISCORD_GUILD_ID_MESSAGE) -# -# cls._settings["DISCORD_GUILD_ID"] = int(raw_discord_guild_id) # type: ignore[arg-type] -# -# @classmethod -# def _setup_group_full_name(cls) -> None: -# raw_group_full_name: str | None = os.getenv("GROUP_NAME") -# -# 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), -# ) -# if not GROUP_FULL_NAME_IS_VALID: -# INVALID_GROUP_FULL_NAME: Final[str] = ( -# "GROUP_NAME must not contain any invalid characters." -# ) -# raise ImproperlyConfiguredError(INVALID_GROUP_FULL_NAME) -# cls._settings["_GROUP_FULL_NAME"] = raw_group_full_name -# -# @classmethod -# def _setup_group_short_name(cls) -> None: -# raw_group_short_name: str | None = os.getenv("GROUP_SHORT_NAME") -# -# 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), -# ) -# if not GROUP_SHORT_NAME_IS_VALID: -# INVALID_GROUP_SHORT_NAME: Final[str] = ( -# "GROUP_SHORT_NAME must not contain any invalid characters." -# ) -# raise ImproperlyConfiguredError(INVALID_GROUP_SHORT_NAME) -# cls._settings["_GROUP_SHORT_NAME"] = raw_group_short_name -# -# @classmethod -# def _setup_purchase_membership_url(cls) -> None: -# raw_purchase_membership_url: str | None = os.getenv("PURCHASE_MEMBERSHIP_URL") -# -# PURCHASE_MEMBERSHIP_URL_IS_VALID: Final[bool] = bool( -# not raw_purchase_membership_url -# or validators.url(raw_purchase_membership_url), -# ) -# if not PURCHASE_MEMBERSHIP_URL_IS_VALID: -# INVALID_PURCHASE_MEMBERSHIP_URL_MESSAGE: Final[str] = ( -# "PURCHASE_MEMBERSHIP_URL must be a valid URL." -# ) -# raise ImproperlyConfiguredError(INVALID_PURCHASE_MEMBERSHIP_URL_MESSAGE) -# -# cls._settings["PURCHASE_MEMBERSHIP_URL"] = raw_purchase_membership_url -# -# @classmethod -# def _setup_membership_perks_url(cls) -> None: -# raw_membership_perks_url: str | None = os.getenv("MEMBERSHIP_PERKS_URL") -# -# MEMBERSHIP_PERKS_URL_IS_VALID: Final[bool] = bool( -# not raw_membership_perks_url -# or validators.url(raw_membership_perks_url), -# ) -# if not MEMBERSHIP_PERKS_URL_IS_VALID: -# INVALID_MEMBERSHIP_PERKS_URL_MESSAGE: Final[str] = ( -# "MEMBERSHIP_PERKS_URL must be a valid URL." -# ) -# raise ImproperlyConfiguredError(INVALID_MEMBERSHIP_PERKS_URL_MESSAGE) -# -# cls._settings["MEMBERSHIP_PERKS_URL"] = raw_membership_perks_url -# -# @classmethod -# def _setup_ping_command_easter_egg_probability(cls) -> None: -# INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE: Final[str] = ( -# "PING_COMMAND_EASTER_EGG_PROBABILITY must be a float between & including 1 & 0." -# ) -# -# e: ValueError -# try: -# raw_ping_command_easter_egg_probability: float = 100 * float( -# os.getenv("PING_COMMAND_EASTER_EGG_PROBABILITY", "0.01"), -# ) -# except ValueError as e: -# raise ( -# ImproperlyConfiguredError(INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE) -# ) from e -# -# if not 0 <= raw_ping_command_easter_egg_probability <= 100: -# raise ImproperlyConfiguredError( -# INVALID_PING_COMMAND_EASTER_EGG_PROBABILITY_MESSAGE, -# ) -# -# cls._settings["PING_COMMAND_EASTER_EGG_PROBABILITY"] = ( -# raw_ping_command_easter_egg_probability -# ) -# -# @classmethod -# @functools.lru_cache(maxsize=5) -# def _get_messages_dict(cls, raw_messages_file_path: str | None) -> Mapping[str, object]: -# JSON_DECODING_ERROR_MESSAGE: Final[str] = ( -# "Messages JSON file must contain a JSON string that can be decoded " -# "into a Python dict object." -# ) -# -# messages_file_path: Path = ( -# Path(raw_messages_file_path) -# if raw_messages_file_path -# else PROJECT_ROOT / Path("messages.json") -# ) -# -# if not messages_file_path.is_file(): -# MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE: Final[str] = ( -# "MESSAGES_FILE_PATH must be a path to a file that exists." -# ) -# raise ImproperlyConfiguredError(MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE) -# -# messages_file: IO[str] -# with messages_file_path.open(encoding="utf8") as messages_file: -# e: json.JSONDecodeError -# try: -# messages_dict: object = json.load(messages_file) -# except json.JSONDecodeError as e: -# raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) from e -# -# if not isinstance(messages_dict, Mapping): -# raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) -# -# return messages_dict -# -# @classmethod -# def _setup_welcome_messages(cls) -> None: -# messages_dict: Mapping[str, object] = cls._get_messages_dict( -# os.getenv("MESSAGES_FILE_PATH"), -# ) -# -# if "welcome_messages" not in messages_dict: -# raise MessagesJSONFileMissingKeyError(missing_key="welcome_messages") -# -# WELCOME_MESSAGES_KEY_IS_VALID: Final[bool] = bool( -# isinstance(messages_dict["welcome_messages"], Iterable) -# and messages_dict["welcome_messages"], -# ) -# if not WELCOME_MESSAGES_KEY_IS_VALID: -# raise MessagesJSONFileValueError( -# dict_key="welcome_messages", -# invalid_value=messages_dict["welcome_messages"], -# ) -# -# cls._settings["WELCOME_MESSAGES"] = set(messages_dict["welcome_messages"]) # type: ignore[call-overload] -# -# @classmethod -# def _setup_roles_messages(cls) -> None: -# messages_dict: Mapping[str, object] = cls._get_messages_dict( -# os.getenv("MESSAGES_FILE_PATH"), -# ) -# -# if "roles_messages" not in messages_dict: -# raise MessagesJSONFileMissingKeyError(missing_key="roles_messages") -# -# ROLES_MESSAGES_KEY_IS_VALID: Final[bool] = ( -# isinstance(messages_dict["roles_messages"], Iterable) -# and bool(messages_dict["roles_messages"]) -# ) -# if not ROLES_MESSAGES_KEY_IS_VALID: -# raise MessagesJSONFileValueError( -# dict_key="roles_messages", -# invalid_value=messages_dict["roles_messages"], -# ) -# cls._settings["ROLES_MESSAGES"] = set(messages_dict["roles_messages"]) # type: ignore[call-overload] -# -# @classmethod -# def _setup_members_list_url(cls) -> None: -# raw_members_list_url: str | None = os.getenv("MEMBERS_LIST_URL") -# -# MEMBERS_LIST_URL_IS_VALID: Final[bool] = bool( -# raw_members_list_url -# and validators.url(raw_members_list_url), -# ) -# if not MEMBERS_LIST_URL_IS_VALID: -# INVALID_MEMBERS_LIST_URL_MESSAGE: Final[str] = ( -# "MEMBERS_LIST_URL must be a valid URL." -# ) -# raise ImproperlyConfiguredError(INVALID_MEMBERS_LIST_URL_MESSAGE) -# -# 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( -# "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), -# ) -# if not MEMBERS_LIST_URL_SESSION_COOKIE_IS_VALID: -# INVALID_MEMBERS_LIST_URL_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) -# -# cls._settings["MEMBERS_LIST_URL_SESSION_COOKIE"] = raw_members_list_url_session_cookie -# -# @classmethod -# def _setup_send_introduction_reminders(cls) -> None: -# raw_send_introduction_reminders: str | bool = str( -# os.getenv("SEND_INTRODUCTION_REMINDERS", "Once"), -# ).lower() -# -# if raw_send_introduction_reminders not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: -# INVALID_SEND_INTRODUCTION_REMINDERS_MESSAGE: Final[str] = ( -# "SEND_INTRODUCTION_REMINDERS must be one of: " -# "\"Once\", \"Interval\" or \"False\"." -# ) -# raise ImproperlyConfiguredError(INVALID_SEND_INTRODUCTION_REMINDERS_MESSAGE) -# -# if raw_send_introduction_reminders in TRUE_VALUES: -# raw_send_introduction_reminders = "once" -# -# elif raw_send_introduction_reminders not in ("once", "interval"): -# raw_send_introduction_reminders = False -# -# cls._settings["SEND_INTRODUCTION_REMINDERS"] = raw_send_introduction_reminders -# -# @classmethod -# def _setup_send_introduction_reminders_delay(cls) -> None: -# if "SEND_INTRODUCTION_REMINDERS" not in cls._settings: -# INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( -# "Invalid setup order: SEND_INTRODUCTION_REMINDERS must be set up " -# "before SEND_INTRODUCTION_REMINDERS_DELAY can be set up." -# ) -# raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) -# -# raw_send_introduction_reminders_delay: Match[str] | None = re.match( -# 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")), -# ) -# -# raw_timedelta_send_introduction_reminders_delay: timedelta = timedelta() -# -# if cls._settings["SEND_INTRODUCTION_REMINDERS"]: -# if not raw_send_introduction_reminders_delay: -# INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( -# "SEND_INTRODUCTION_REMINDERS_DELAY must contain the delay " -# "in any combination of seconds, minutes, hours, days or weeks." -# ) -# raise ImproperlyConfiguredError( -# INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, -# ) -# -# raw_timedelta_send_introduction_reminders_delay = timedelta( -# **{ -# key: float(value) -# for key, value -# in raw_send_introduction_reminders_delay.groupdict().items() -# if value -# }, -# ) -# -# if raw_timedelta_send_introduction_reminders_delay < timedelta(days=1): -# TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( -# "SEND_INTRODUCTION_REMINDERS_DELAY must be longer than or equal to 1 day " -# "(in any allowed format)." -# ) -# raise ImproperlyConfiguredError( -# TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, -# ) -# -# cls._settings["SEND_INTRODUCTION_REMINDERS_DELAY"] = ( -# raw_timedelta_send_introduction_reminders_delay -# ) -# -# @classmethod -# def _setup_send_introduction_reminders_interval(cls) -> None: -# if "SEND_INTRODUCTION_REMINDERS" not in cls._settings: -# INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( -# "Invalid setup order: SEND_INTRODUCTION_REMINDERS must be set up " -# "before SEND_INTRODUCTION_REMINDERS_INTERVAL can be set up." -# ) -# raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) -# -# raw_send_introduction_reminders_interval: Match[str] | None = re.match( -# r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", -# str(os.getenv("SEND_INTRODUCTION_REMINDERS_INTERVAL", "6h")), -# ) -# -# raw_timedelta_details_send_introduction_reminders_interval: Mapping[str, float] = { -# "hours": 6, -# } -# -# if cls._settings["SEND_INTRODUCTION_REMINDERS"]: -# if not raw_send_introduction_reminders_interval: -# INVALID_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE: Final[str] = ( -# "SEND_INTRODUCTION_REMINDERS_INTERVAL must contain the interval " -# "in any combination of seconds, minutes or hours." -# ) -# raise ImproperlyConfiguredError( -# INVALID_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE, -# ) -# -# raw_timedelta_details_send_introduction_reminders_interval = { -# key: float(value) -# for key, value -# in raw_send_introduction_reminders_interval.groupdict().items() -# if value -# } -# -# cls._settings["SEND_INTRODUCTION_REMINDERS_INTERVAL"] = ( -# raw_timedelta_details_send_introduction_reminders_interval -# ) -# -# @classmethod -# def _setup_send_get_roles_reminders(cls) -> None: -# raw_send_get_roles_reminders: str = str( -# os.getenv("SEND_GET_ROLES_REMINDERS", "True"), -# ).lower() -# -# if raw_send_get_roles_reminders not in TRUE_VALUES | FALSE_VALUES: -# INVALID_SEND_GET_ROLES_REMINDERS_MESSAGE: Final[str] = ( -# "SEND_GET_ROLES_REMINDERS must be a boolean value." -# ) -# raise ImproperlyConfiguredError(INVALID_SEND_GET_ROLES_REMINDERS_MESSAGE) -# -# cls._settings["SEND_GET_ROLES_REMINDERS"] = ( -# raw_send_get_roles_reminders in TRUE_VALUES -# ) -# -# @classmethod -# def _setup_send_get_roles_reminders_delay(cls) -> None: -# if "SEND_GET_ROLES_REMINDERS" not in cls._settings: -# INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( -# "Invalid setup order: SEND_GET_ROLES_REMINDERS must be set up " -# "before SEND_GET_ROLES_REMINDERS_DELAY can be set up." -# ) -# raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) -# -# raw_send_get_roles_reminders_delay: Match[str] | None = re.match( -# 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")), -# ) -# -# raw_timedelta_send_get_roles_reminders_delay: timedelta = timedelta() -# -# if cls._settings["SEND_GET_ROLES_REMINDERS"]: -# if not raw_send_get_roles_reminders_delay: -# INVALID_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE: Final[str] = ( -# "SEND_GET_ROLES_REMINDERS_DELAY must contain the delay " -# "in any combination of seconds, minutes, hours, days or weeks." -# ) -# raise ImproperlyConfiguredError( -# INVALID_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE, -# ) -# -# raw_timedelta_send_get_roles_reminders_delay = timedelta( -# **{ -# key: float(value) -# for key, value -# in raw_send_get_roles_reminders_delay.groupdict().items() -# if value -# }, -# ) -# -# if raw_timedelta_send_get_roles_reminders_delay < timedelta(days=1): -# TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE: Final[str] = ( -# "SEND_SEND_GET_ROLES_REMINDERS_DELAY " -# "must be longer than or equal to 1 day (in any allowed format)." -# ) -# raise ImproperlyConfiguredError( -# TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE, -# ) -# -# cls._settings["SEND_GET_ROLES_REMINDERS_DELAY"] = ( -# raw_timedelta_send_get_roles_reminders_delay -# ) -# -# @classmethod -# def _setup_advanced_send_get_roles_reminders_interval(cls) -> None: -# if "SEND_GET_ROLES_REMINDERS" not in cls._settings: -# INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( -# "Invalid setup order: SEND_GET_ROLES_REMINDERS must be set up " -# "before ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL can be set up." -# ) -# raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) -# -# raw_advanced_send_get_roles_reminders_interval: Match[str] | None = re.match( -# 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")), -# ) -# -# raw_timedelta_details_advanced_send_get_roles_reminders_interval: Mapping[str, float] = { # noqa: E501 -# "hours": 24, -# } -# -# if cls._settings["SEND_GET_ROLES_REMINDERS"]: -# if not raw_advanced_send_get_roles_reminders_interval: -# INVALID_ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL_MESSAGE: Final[str] = ( -# "ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL must contain the interval " -# "in any combination of seconds, minutes or hours." -# ) -# raise ImproperlyConfiguredError( -# INVALID_ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL_MESSAGE, -# ) -# -# raw_timedelta_details_advanced_send_get_roles_reminders_interval = { -# key: float(value) -# for key, value -# in raw_advanced_send_get_roles_reminders_interval.groupdict().items() -# if value -# } -# -# cls._settings["ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL"] = ( -# raw_timedelta_details_advanced_send_get_roles_reminders_interval -# ) -# -# @classmethod -# def _setup_statistics_days(cls) -> None: -# e: ValueError -# try: -# raw_statistics_days: float = float(os.getenv("STATISTICS_DAYS", "30")) -# except ValueError as e: -# INVALID_STATISTICS_DAYS_MESSAGE: Final[str] = ( -# "STATISTICS_DAYS must contain the statistics period in days." -# ) -# raise ImproperlyConfiguredError(INVALID_STATISTICS_DAYS_MESSAGE) from e -# -# cls._settings["STATISTICS_DAYS"] = timedelta(days=raw_statistics_days) -# -# @classmethod -# def _setup_statistics_roles(cls) -> None: -# raw_statistics_roles: str | None = os.getenv("STATISTICS_ROLES") -# -# if not raw_statistics_roles: -# cls._settings["STATISTICS_ROLES"] = DEFAULT_STATISTICS_ROLES -# -# else: -# cls._settings["STATISTICS_ROLES"] = { -# raw_statistics_role -# for raw_statistics_role -# in raw_statistics_roles.split(",") -# if raw_statistics_role -# } -# -# @classmethod -# def _setup_moderation_document_url(cls) -> None: -# raw_moderation_document_url: str | None = os.getenv("MODERATION_DOCUMENT_URL") -# -# MODERATION_DOCUMENT_URL_IS_VALID: Final[bool] = bool( -# raw_moderation_document_url -# and validators.url(raw_moderation_document_url), -# ) -# if not MODERATION_DOCUMENT_URL_IS_VALID: -# MODERATION_DOCUMENT_URL_MESSAGE: Final[str] = ( -# "MODERATION_DOCUMENT_URL must be a valid URL." -# ) -# raise ImproperlyConfiguredError(MODERATION_DOCUMENT_URL_MESSAGE) -# -# 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( -# "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", -# "DM", -# ) -# if not raw_manual_moderation_warning_message_location: -# MANUAL_MODERATION_WARNING_MESSAGE_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) -# -# cls._settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"] = ( -# raw_manual_moderation_warning_message_location -# ) -# -# @classmethod -# def _setup_env_variables(cls) -> None: -# """ -# Load environment values into the settings dictionary. -# -# Environment values are loaded from the .env file/the current environment variables and -# are only stored after the input values have been validated. -# """ -# if cls._is_env_variables_setup: -# logging.warning("Environment variables have already been set up.") -# return -# -# dotenv.load_dotenv() -# -# cls._setup_logging() -# cls._setup_discord_bot_token() -# cls._setup_discord_log_channel_webhook_url() -# cls._setup_discord_guild_id() -# cls._setup_group_full_name() -# cls._setup_group_short_name() -# cls._setup_ping_command_easter_egg_probability() -# cls._setup_welcome_messages() -# cls._setup_roles_messages() -# cls._setup_members_list_url() -# cls._setup_members_list_url_session_cookie() -# cls._setup_membership_perks_url() -# cls._setup_purchase_membership_url() -# cls._setup_send_introduction_reminders() -# cls._setup_send_introduction_reminders_delay() -# cls._setup_send_introduction_reminders_interval() -# cls._setup_send_get_roles_reminders() -# cls._setup_send_get_roles_reminders_delay() -# cls._setup_advanced_send_get_roles_reminders_interval() -# cls._setup_statistics_days() -# cls._setup_statistics_roles() -# cls._setup_moderation_document_url() -# cls._setup_manual_moderation_warning_message_location() -# -# cls._is_env_variables_setup = True +# messages_dict: object = json.load(messages_file) +# except json.JSONDecodeError as e: +# raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) from e +# +# if not isinstance(messages_dict, Mapping): +# raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) +# +# return messages_dict +# +# @classmethod +# def _setup_welcome_messages(cls) -> None: +# messages_dict: Mapping[str, object] = cls._get_messages_dict( +# os.getenv("MESSAGES_FILE_PATH"), +# ) +# +# if "welcome_messages" not in messages_dict: +# raise MessagesJSONFileMissingKeyError(missing_key="welcome_messages") +# +# WELCOME_MESSAGES_KEY_IS_VALID: Final[bool] = bool( +# isinstance(messages_dict["welcome_messages"], Iterable) +# and messages_dict["welcome_messages"], +# ) +# if not WELCOME_MESSAGES_KEY_IS_VALID: +# raise MessagesJSONFileValueError( +# dict_key="welcome_messages", +# invalid_value=messages_dict["welcome_messages"], +# ) +# +# cls._settings["WELCOME_MESSAGES"] = set(messages_dict["welcome_messages"]) # type: ignore[call-overload] +# +# @classmethod +# def _setup_roles_messages(cls) -> None: +# messages_dict: Mapping[str, object] = cls._get_messages_dict( +# os.getenv("MESSAGES_FILE_PATH"), +# ) +# +# if "roles_messages" not in messages_dict: +# raise MessagesJSONFileMissingKeyError(missing_key="roles_messages") +# +# ROLES_MESSAGES_KEY_IS_VALID: Final[bool] = ( +# isinstance(messages_dict["roles_messages"], Iterable) +# and bool(messages_dict["roles_messages"]) +# ) +# if not ROLES_MESSAGES_KEY_IS_VALID: +# raise MessagesJSONFileValueError( +# dict_key="roles_messages", +# invalid_value=messages_dict["roles_messages"], +# ) +# cls._settings["ROLES_MESSAGES"] = set(messages_dict["roles_messages"]) # type: ignore[call-overload] diff --git a/config/_messages.py b/config/_messages.py index 6c3a68ff4..7ab769b4a 100644 --- a/config/_messages.py +++ b/config/_messages.py @@ -3,3 +3,4 @@ __all__: Sequence[str] = () # TODO: load messages +# TODO: Create JSONSchema for messages From 364f446d377e984d02cf25ad242d7b3f76fa6677 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 04:30:16 +0100 Subject: [PATCH 018/128] Remove unused dependencies --- poetry.lock | 155 +++++++++++++++++++++---------------------------- pyproject.toml | 3 - 2 files changed, 65 insertions(+), 93 deletions(-) diff --git a/poetry.lock b/poetry.lock index c27f52768..04afa06dd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -572,53 +572,53 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "fonttools" -version = "4.51.0" +version = "4.52.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.52.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:67a30b872e79577e5319ce660ede4a5131fa8a45de76e696746545e17db4437f"}, + {file = "fonttools-4.52.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a5bff35738f8f6607c4303561ee1d1e5f64d5b14cf3c472d3030566c82e763"}, + {file = "fonttools-4.52.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c9622593dfff042480a1b7e5b72c4d7dc00b96d2b4f98b0bf8acf071087e0db"}, + {file = "fonttools-4.52.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33cfc9fe27af5e113d157d5147e24fc8e5bda3c5aadb55bea9847ec55341ce30"}, + {file = "fonttools-4.52.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa5bec5027d947ee4b2242caecf7dc6e4ea03833e92e9b5211ebb6ab4eede8b2"}, + {file = "fonttools-4.52.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10e44bf8e5654050a332a79285bacd6bd3069084540aec46c0862391147a1daa"}, + {file = "fonttools-4.52.1-cp310-cp310-win32.whl", hash = "sha256:7fba390ac2ca18ebdd456f3a9acfb4557d6dcb2eaba5cc3eadce01003892a770"}, + {file = "fonttools-4.52.1-cp310-cp310-win_amd64.whl", hash = "sha256:15df3517eb95035422a5c953ca19aac99913c16aa0e4ef061aeaef5f3bcaf369"}, + {file = "fonttools-4.52.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40730aab9cf42286f314b985b483eea574f1bcf3a23e28223084cbb9e256457c"}, + {file = "fonttools-4.52.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a19bc2be3af5b22ff5c7fe858c380862e31052c74f62e2c6d565ed0855bed7a6"}, + {file = "fonttools-4.52.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f859066d8afde53f2ddabcd0705061e6d9d9868757c6ae28abe49bc885292df4"}, + {file = "fonttools-4.52.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74cd3e3e9ba501e87a391b62e91f7b1610e8b3f3d706a368e5aee51614c1674e"}, + {file = "fonttools-4.52.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:958957b81418647f66820480363cb617ba6b5bcf189ec6c4cea307d051048545"}, + {file = "fonttools-4.52.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56addf1f995d94dad13aaaf56eb6def3d9ca97c2fada5e27af8190b3141e8633"}, + {file = "fonttools-4.52.1-cp311-cp311-win32.whl", hash = "sha256:fea5456b2af42db8ecb1a6c2f144655ca6dcdcebd970f3145c56e668084ded7e"}, + {file = "fonttools-4.52.1-cp311-cp311-win_amd64.whl", hash = "sha256:228faab7638cd726cdde5e2ec9ee10f780fbf9de9aa38d7f1e56a270437dff36"}, + {file = "fonttools-4.52.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7c6aeb0d53e2ea92009b11c3d4ad9c03d0ecdfe602d547bed8537836e464f51e"}, + {file = "fonttools-4.52.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e871123d12c92e2c9bda6369b69ce2da9cef40b119cc340451e413e90355fa38"}, + {file = "fonttools-4.52.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ff8857dc9bb3e407c25aef3e025409cfbb23adb646a835636bebb1bdfc27a41"}, + {file = "fonttools-4.52.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7685fdc6e23267844eef2b9af585d7f171cca695e4eb369d7682544c3e2e1123"}, + {file = "fonttools-4.52.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1e1b2774485fbbb41a1beccc913b9c6f7971f78da61dd34207b9acc3cc2963e"}, + {file = "fonttools-4.52.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e2c415160397fd6ed3964155aeec4bfefceeee365ab17161a5b3fe3f8dab077"}, + {file = "fonttools-4.52.1-cp312-cp312-win32.whl", hash = "sha256:3ba2c4647e7decfb8e9cd346661c7d151dae1fba23d37b48bcf5fa8351f7b8c8"}, + {file = "fonttools-4.52.1-cp312-cp312-win_amd64.whl", hash = "sha256:d39b926f14a2f7a7f92ded7d266b18f0108d867364769ab59da88ac2fa90d288"}, + {file = "fonttools-4.52.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6e58d8097a269b6c43ec0abb3fa8d6c350ff0c7dfd23fc14d004610df88a4bb3"}, + {file = "fonttools-4.52.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:20f0fc969817c50539dc919ed8c4aef4de28c2d6e0111a064112301f157aede4"}, + {file = "fonttools-4.52.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d62e84d38969491c6c1f6fe3dd63108e99d02de01bb3d98c160a5d4d24120910"}, + {file = "fonttools-4.52.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb5a389bbdee6f4c422881de422ee0e7efdfcd9310b13d540b12aa8ae2c9e7b"}, + {file = "fonttools-4.52.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0caf05c969cbde6729dd97b64bea445ee152bb19215d5886f7b93bd0fb455468"}, + {file = "fonttools-4.52.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df08bee1dd29a767311b50c62c0cfe4d72ae8c793e567d4c60b8c16c7c63a4f0"}, + {file = "fonttools-4.52.1-cp38-cp38-win32.whl", hash = "sha256:82ffcf4782ceda09842b5b7875b36834c15d7cc0d5dd3d23a658ee9cf8819cd6"}, + {file = "fonttools-4.52.1-cp38-cp38-win_amd64.whl", hash = "sha256:26b43bab5a3bce55ed4d9699b16568795eef5597d154f52dcabef5b4804c4b21"}, + {file = "fonttools-4.52.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e8dbc13c4bc12e60df1b1f5e484112a5e96a6e8bba995e2965988ad73c5ea1b"}, + {file = "fonttools-4.52.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7352ba2226e45e8fba11c3fb416363faf1b06f3f2e80d07d2930401265f3bf9c"}, + {file = "fonttools-4.52.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8834d43763e9e92349ce8bb25dfb612aef6691eefefad885212d5e8f36a94a4"}, + {file = "fonttools-4.52.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2a8c1101d06cc8fca7851dceb67afd53dd6fc0288bacaa632e647bc5afff58"}, + {file = "fonttools-4.52.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a99b738227c0f6f2bbe381b45804a7c46653c95b9d7bf13f6f02884bc87e4930"}, + {file = "fonttools-4.52.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:75aa00a16b9a64d1550e2e70d3582c7fe1ef18560e0cf066a4087fe6d11908a2"}, + {file = "fonttools-4.52.1-cp39-cp39-win32.whl", hash = "sha256:c2f09b4aa699cfed4bbebc1829c5f044b41976707dac9230ed00d5a9fc6452c1"}, + {file = "fonttools-4.52.1-cp39-cp39-win_amd64.whl", hash = "sha256:78ea6e0d4c89f8e216995923b854dd10bd09e48d3a5a3ccb48bb68f436a409ad"}, + {file = "fonttools-4.52.1-py3-none-any.whl", hash = "sha256:faf5c83f83f7ddebdafdb453d02efdbea7fb494080d7a8d45a8a20db06ea8da5"}, + {file = "fonttools-4.52.1.tar.gz", hash = "sha256:8c9204435aa6e5e9479a5ba4e669f05dea28b0c61958e0c0923cb164296d9329"}, ] [package.extras] @@ -1423,20 +1423,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - [[package]] name = "python-logging-discord-handler" version = "0.1.4" @@ -1553,28 +1539,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.4" +version = "0.4.5" 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.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, + {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, + {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, + {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, + {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, + {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, ] [[package]] @@ -1725,13 +1711,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.0" 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.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, ] [[package]] @@ -1762,17 +1748,6 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "validators" -version = "0.28.1" -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"}, -] - [[package]] name = "virtualenv" version = "20.26.2" @@ -1910,4 +1885,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "2d775c0e23515319c499b9121a2e9df19bada25b35641735bf9bb5d9099112bc" +content-hash = "44ce0b84cbfab8b8953534c8702fbb5e5e50be9337be6b93cf923ed44d26090d" diff --git a/pyproject.toml b/pyproject.toml index 03a25d31e..b852adbd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,6 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.12" py-cord = "~2.5" -python-dotenv = "^1.0" -validators = "^0.28" beautifulsoup4 = "^4.12" emoji = "^2.12" parsedatetime = "^2.6" @@ -87,7 +85,6 @@ module = [ "mplcyberpunk", "discord_logging.handler", "parsedatetime", - "validators", "strictyaml", "strictyaml.exceptions", "strictyaml.yamllocation", From 0bb33db6fdbe0dcbfe6d953fec901af1f60f9c42 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 17:27:35 +0100 Subject: [PATCH 019/128] Improve config structure --- config/_messages.py | 6 ------ config/_messages/__init__.py | 0 messages.json => config/_messages/langs/en-GB.json | 0 config/{_settings.py => _settings/__init__.py} | 12 ++++++------ config/{ => _settings}/_yaml/__init__.py | 0 config/{ => _settings}/_yaml/custom_schema_utils.py | 0 config/{ => _settings}/_yaml/custom_validators.py | 0 config/{_pre_startup_utils.py => _settings/utils.py} | 0 utils/__init__.py | 11 +++++++---- 9 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 config/_messages.py create mode 100644 config/_messages/__init__.py rename messages.json => config/_messages/langs/en-GB.json (100%) rename config/{_settings.py => _settings/__init__.py} (99%) rename config/{ => _settings}/_yaml/__init__.py (100%) rename config/{ => _settings}/_yaml/custom_schema_utils.py (100%) rename config/{ => _settings}/_yaml/custom_validators.py (100%) rename config/{_pre_startup_utils.py => _settings/utils.py} (100%) diff --git a/config/_messages.py b/config/_messages.py deleted file mode 100644 index 7ab769b4a..000000000 --- a/config/_messages.py +++ /dev/null @@ -1,6 +0,0 @@ -from collections.abc import Sequence - -__all__: Sequence[str] = () - -# TODO: load messages -# TODO: Create JSONSchema for messages diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/messages.json b/config/_messages/langs/en-GB.json similarity index 100% rename from messages.json rename to config/_messages/langs/en-GB.json diff --git a/config/_settings.py b/config/_settings/__init__.py similarity index 99% rename from config/_settings.py rename to config/_settings/__init__.py index 6e74d2ae0..2ab2ae961 100644 --- a/config/_settings.py +++ b/config/_settings/__init__.py @@ -22,15 +22,15 @@ from discord_logging.handler import DiscordHandler from strictyaml import YAML -from exceptions import BotRequiresRestartAfterConfigChange - -from ._pre_startup_utils import is_running_in_async -from ._yaml import load_yaml -from .constants import ( +from config.constants import ( DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME, PROJECT_ROOT, REQUIRES_RESTART_SETTINGS, ) +from exceptions import BotRequiresRestartAfterConfigChange + +from . import utils +from ._yaml import load_yaml logger: Final[Logger] = logging.getLogger("TeX-Bot") @@ -123,7 +123,7 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: Final[bool] = bool( "bot" in item.lower() and "token" in item.lower() - and is_running_in_async(), + and utils.is_running_in_async(), ) if ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: TEX_BOT_ALREADY_RUNNING_MESSAGE: Final[str] = ( diff --git a/config/_yaml/__init__.py b/config/_settings/_yaml/__init__.py similarity index 100% rename from config/_yaml/__init__.py rename to config/_settings/_yaml/__init__.py diff --git a/config/_yaml/custom_schema_utils.py b/config/_settings/_yaml/custom_schema_utils.py similarity index 100% rename from config/_yaml/custom_schema_utils.py rename to config/_settings/_yaml/custom_schema_utils.py diff --git a/config/_yaml/custom_validators.py b/config/_settings/_yaml/custom_validators.py similarity index 100% rename from config/_yaml/custom_validators.py rename to config/_settings/_yaml/custom_validators.py diff --git a/config/_pre_startup_utils.py b/config/_settings/utils.py similarity index 100% rename from config/_pre_startup_utils.py rename to config/_settings/utils.py diff --git a/utils/__init__.py b/utils/__init__.py index 71ed168da..0c28029e1 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -2,7 +2,10 @@ from collections.abc import Sequence -__all__: Sequence[str] = ( +# noinspection PyProtectedMember +from config._settings import utils as config_utils + +__all__: Sequence[str] = ( # noqa: PLE0604 "CommandChecks", "MessageSenderComponent", "SuppressTraceback", @@ -13,14 +16,14 @@ "TeXBotAutocompleteContext", "generate_invite_url", "is_member_inducted", - "is_running_in_async", + *config_utils.__all__, ) import discord -# noinspection PyProtectedMember -from config._pre_startup_utils import is_running_in_async +# noinspection PyUnresolvedReferences,PyProtectedMember +from config._settings.utils import * # noqa: F403 from .command_checks import CommandChecks from .message_sender_components import MessageSenderComponent From e0326a7dd12ca7cecfb0803febe2dda060c0810d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 19:54:30 +0100 Subject: [PATCH 020/128] Add messages loading --- cogs/induct.py | 4 +- cogs/write_roles.py | 4 +- config/__init__.py | 88 +-------------- config/_messages/__init__.py | 104 ++++++++++++++++++ .../_messages/{langs => locales}/en-GB.json | 4 +- config/_settings/__init__.py | 26 ++--- config/_settings/_yaml/__init__.py | 10 +- ...chema_utils.py => custom_map_validator.py} | 0 ...idators.py => custom_scalar_validators.py} | 0 config/constants.py | 4 +- db/_settings.py | 4 +- pyproject.toml | 1 + 12 files changed, 140 insertions(+), 109 deletions(-) rename config/_messages/{langs => locales}/en-GB.json (98%) rename config/_settings/_yaml/{custom_schema_utils.py => custom_map_validator.py} (100%) rename config/_settings/_yaml/{custom_validators.py => custom_scalar_validators.py} (100%) diff --git a/cogs/induct.py b/cogs/induct.py index d6abadd8e..172377a4b 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -20,7 +20,7 @@ import discord -from config import settings +from config import messages, settings from db.core.models import IntroductionReminderOptOutMember from exceptions import ( CommitteeRoleDoesNotExistError, @@ -140,7 +140,7 @@ class BaseInductCog(TeXBotBaseCog): async def get_random_welcome_message(self, induction_member: discord.User | discord.Member | None = None) -> str: # noqa: E501 """Get & format a random welcome message.""" - random_welcome_message: str = random.choice(tuple(settings["WELCOME_MESSAGES"])) + random_welcome_message: str = random.choice(tuple(messages["WELCOME_MESSAGES"])) if "" in random_welcome_message: if not induction_member: diff --git a/cogs/write_roles.py b/cogs/write_roles.py index f209e0b15..657edd05f 100644 --- a/cogs/write_roles.py +++ b/cogs/write_roles.py @@ -7,7 +7,7 @@ import discord -from config import settings +from config import messages from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog @@ -33,7 +33,7 @@ async def write_roles(self, ctx: TeXBotApplicationContext) -> None: roles_channel: discord.TextChannel = await self.bot.roles_channel roles_message: str - for roles_message in settings["ROLES_MESSAGES"]: + for roles_message in messages["OPT_IN_ROLES_SELECTORS"]: await roles_channel.send( roles_message.replace("", self.bot.group_short_name), ) diff --git a/config/__init__.py b/config/__init__.py index e604dc462..7a61dd0bf 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -9,11 +9,12 @@ __all__: Sequence[str] = ( "PROJECT_ROOT", - "TRANSLATED_MESSAGES_LOCALE_CODES", + "MESSAGES_LOCALE_CODES", "LogLevels", "run_setup", "settings", "check_for_deprecated_environment_variables", + "messages", ) @@ -27,16 +28,14 @@ from exceptions import BotRequiresRestartAfterConfigChange +from ._messages import MessagesAccessor from ._settings import SettingsAccessor -from .constants import ( - PROJECT_ROOT, - TRANSLATED_MESSAGES_LOCALE_CODES, - LogLevels, -) +from .constants import MESSAGES_LOCALE_CODES, PROJECT_ROOT, LogLevels logger: Final[Logger] = logging.getLogger("TeX-Bot") settings: Final[SettingsAccessor] = SettingsAccessor() +messages: Final[MessagesAccessor] = MessagesAccessor() def run_setup() -> None: @@ -46,7 +45,7 @@ def run_setup() -> None: with contextlib.suppress(BotRequiresRestartAfterConfigChange): settings.reload() - # TODO: load messages here using language from settings + messages.load(settings["MESSAGES_LOCALE_CODE"]) logger.debug("Begin database setup") @@ -147,78 +146,3 @@ def check_for_deprecated_environment_variables() -> None: ) if deprecated_environment_variable_found: raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR - - -# @classmethod -# @functools.lru_cache(maxsize=5) -# def _get_messages_dict(cls, raw_messages_file_path: str | None) -> Mapping[str, object]: -# JSON_DECODING_ERROR_MESSAGE: Final[str] = ( -# "Messages JSON file must contain a JSON string that can be decoded " -# "into a Python dict object." -# ) -# -# messages_file_path: Path = ( -# Path(raw_messages_file_path) -# if raw_messages_file_path -# else PROJECT_ROOT / Path("messages.json") -# ) -# -# if not messages_file_path.is_file(): -# MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE: Final[str] = ( -# "MESSAGES_FILE_PATH must be a path to a file that exists." -# ) -# raise ImproperlyConfiguredError(MESSAGES_FILE_PATH_DOES_NOT_EXIST_MESSAGE) -# -# messages_file: IO[str] -# with messages_file_path.open(encoding="utf8") as messages_file: -# e: json.JSONDecodeError -# try: -# messages_dict: object = json.load(messages_file) -# except json.JSONDecodeError as e: -# raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) from e -# -# if not isinstance(messages_dict, Mapping): -# raise ImproperlyConfiguredError(JSON_DECODING_ERROR_MESSAGE) -# -# return messages_dict -# -# @classmethod -# def _setup_welcome_messages(cls) -> None: -# messages_dict: Mapping[str, object] = cls._get_messages_dict( -# os.getenv("MESSAGES_FILE_PATH"), -# ) -# -# if "welcome_messages" not in messages_dict: -# raise MessagesJSONFileMissingKeyError(missing_key="welcome_messages") -# -# WELCOME_MESSAGES_KEY_IS_VALID: Final[bool] = bool( -# isinstance(messages_dict["welcome_messages"], Iterable) -# and messages_dict["welcome_messages"], -# ) -# if not WELCOME_MESSAGES_KEY_IS_VALID: -# raise MessagesJSONFileValueError( -# dict_key="welcome_messages", -# invalid_value=messages_dict["welcome_messages"], -# ) -# -# cls._settings["WELCOME_MESSAGES"] = set(messages_dict["welcome_messages"]) # type: ignore[call-overload] -# -# @classmethod -# def _setup_roles_messages(cls) -> None: -# messages_dict: Mapping[str, object] = cls._get_messages_dict( -# os.getenv("MESSAGES_FILE_PATH"), -# ) -# -# if "roles_messages" not in messages_dict: -# raise MessagesJSONFileMissingKeyError(missing_key="roles_messages") -# -# ROLES_MESSAGES_KEY_IS_VALID: Final[bool] = ( -# isinstance(messages_dict["roles_messages"], Iterable) -# and bool(messages_dict["roles_messages"]) -# ) -# if not ROLES_MESSAGES_KEY_IS_VALID: -# raise MessagesJSONFileValueError( -# dict_key="roles_messages", -# invalid_value=messages_dict["roles_messages"], -# ) -# cls._settings["ROLES_MESSAGES"] = set(messages_dict["roles_messages"]) # type: ignore[call-overload] diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index e69de29bb..85d73e2b8 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -0,0 +1,104 @@ +from collections.abc import Sequence + +__all__: Sequence[str] = ("MessagesAccessor",) + + +import json +import re +from typing import TYPE_CHECKING, Any, ClassVar, Final + +from config.constants import MESSAGES_LOCALE_CODES, PROJECT_ROOT + +if TYPE_CHECKING: + from pathlib import Path + + +class MessagesAccessor: + _messages: ClassVar[dict[str, str | set[str] | Sequence[str]]] = {} + _messages_already_loaded: ClassVar[bool] = False + + @classmethod + def format_invalid_message_id_message(cls, item: str) -> str: + """Return the message to state that the given message ID is invalid.""" + return f"{item!r} is not a valid message ID." + + def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 + """Retrieve message(s) value by attribute lookup.""" + MISSING_ATTRIBUTE_MESSAGE: Final[str] = ( + f"{type(self).__name__!r} object has no attribute {item!r}" + ) + + if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values + raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) + + if not re.match(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item): + raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) + + if item not in self._messages: + INVALID_MESSAGE_ID_MESSAGE: Final[str] = ( + self.format_invalid_message_id_message(item) + ) + raise AttributeError(INVALID_MESSAGE_ID_MESSAGE) + + return self._messages[item] + + def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 + """Retrieve message(s) value by key lookup.""" + attribute_not_exist_error: AttributeError + try: + return getattr(self, item) + except AttributeError as attribute_not_exist_error: + key_error_message: str = item + + ERROR_WAS_FROM_INVALID_KEY_NAME: Final[bool] = ( + self.format_invalid_message_id_message(item) in str( + attribute_not_exist_error, + ) + ) + if ERROR_WAS_FROM_INVALID_KEY_NAME: + key_error_message = str(attribute_not_exist_error) + + raise KeyError(key_error_message) from None + + @classmethod + def load(cls, messages_locale_code: str) -> None: + if messages_locale_code not in MESSAGES_LOCALE_CODES: + INVALID_MESSAGES_LOCALE_CODE_MESSAGE: Final[str] = ( + f"{"messages_locale_code"!r} must be one of " + f"\"{"\", \"".join(MESSAGES_LOCALE_CODES)}\"" + ) + raise ValueError(INVALID_MESSAGES_LOCALE_CODE_MESSAGE) + + if cls._messages_already_loaded: + MESSAGES_ALREADY_LOADED_MESSAGE: Final[str] = "Messages have already been loaded." + raise RuntimeError(MESSAGES_ALREADY_LOADED_MESSAGE) + + try: + messages_locale_file_path: Path = next( + path + for path + in (PROJECT_ROOT / "config/_messages/locales/").iterdir() + if path.stem == messages_locale_code + ) + except StopIteration: + NO_MESSAGES_FILE_FOUND_MESSAGE: Final[str] = ( + f"No messages file found for locale: {messages_locale_code!r}" + ) + raise RuntimeError(NO_MESSAGES_FILE_FOUND_MESSAGE) from None + + messages_load_error: Exception + try: + raw_messages: object = json.loads(messages_locale_file_path.read_text()) + + if not hasattr(raw_messages, "__get_item__"): + raise TypeError + + cls._messages["WELCOME_MESSAGES"] = set(raw_messages["welcome-messages"]) # type: ignore[index] + cls._messages["OPT_IN_ROLES_SELECTORS"] = tuple( + raw_messages["opt-in-roles-selectors"], # type: ignore[index] + ) + + except (json.JSONDecodeError, TypeError) as messages_load_error: + raise NotImplementedError from None + + cls._messages_already_loaded = True diff --git a/config/_messages/langs/en-GB.json b/config/_messages/locales/en-GB.json similarity index 98% rename from config/_messages/langs/en-GB.json rename to config/_messages/locales/en-GB.json index ce1a07bd8..6b02eb088 100644 --- a/config/_messages/langs/en-GB.json +++ b/config/_messages/locales/en-GB.json @@ -1,5 +1,5 @@ { - "welcome_messages": [ + "welcome-messages": [ " is the lisan al gaib. As it was written!", "Welcome, . We've been expecting you ( ͡° ͜ʖ ͡°)", "Welcome . Leave your weapons by the door.", @@ -51,7 +51,7 @@ "Did you ever hear the tragedy of Darth the Wise? I thought not, it's not a story the lecturers would tell you.", "``>`" ], - "roles_messages": [ + "opt-in-roles-selectors": [ "_ _\nReact to this message to get pronoun roles\n:regional_indicator_h: - He/Him\n:regional_indicator_s: - She/Her\n:regional_indicator_t: - They/Them", "_ _\nReact to this message to get year group roles\n:zero: - Foundation Year\n:one: - First Year\n:two: - Second Year\n:regional_indicator_f: - Final Year (incl. 3rd Year MSci/MEng)\n:regional_indicator_i: - Year in Industry\n:regional_indicator_a: - Year Abroad\n:regional_indicator_t: - Post-Graduate Taught (Masters/MSc)\n:regional_indicator_r: - Post-Graduate Research (PhD)\n:regional_indicator_j: - Joint Honours\n:a: - Alumnus\n:regional_indicator_d: - Postdoc", "_ _\nReact to this message to join the **opt in channels**\n:speech_balloon: - Serious Talk\n:house_with_garden: - Housing\n:video_game: - Gaming\n:tv: - Anime\n:soccer: - Sport\n:briefcase: - Industry\n:pick: - Minecraft\n:technologist: - GitHub\n:bookmark: - Archivist\n:ramen: - Rate My Meal", diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 2ab2ae961..600f5bf93 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -69,10 +69,10 @@ def _get_settings_file_path() -> Path: ), ) raw_settings_file_path = "tex-bot-deployment.yaml" - if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): + if not (PROJECT_ROOT / raw_settings_file_path).exists(): raw_settings_file_path = "tex-bot-settings.yaml" - if not (PROJECT_ROOT / Path(raw_settings_file_path)).exists(): + if not (PROJECT_ROOT / raw_settings_file_path).exists(): raw_settings_file_path = "tex-bot-config.yaml" settings_file_path: Path = Path(raw_settings_file_path) @@ -154,12 +154,12 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 @classmethod def reload(cls) -> None: settings_file_path: Path = _get_settings_file_path() - current_yaml: YAML = load_yaml( # type: ignore[no-any-unimported] + current_yaml: YAML = load_yaml( # type: ignore[no-any-unimported] # TODO: better error messages when loading yaml settings_file_path.read_text(), file_name=settings_file_path.name, ) - if current_yaml == cls._most_recent_yaml: + if current_yaml == cls._most_recent_yaml and cls._settings: return changed_settings_keys: set[str] = set() @@ -210,7 +210,7 @@ def reload(cls) -> None: cls._reload_strike_performed_manually_warning_location( current_yaml["commands"]["strike"]["performed-manually-warning-location"], ), - cls._reload_messages_language(current_yaml["messages-language"]), + cls._reload_messages_locale(current_yaml["messages-locale"]), cls._reload_send_introduction_reminders_enabled( current_yaml["reminders"]["send-introduction-reminders"]["enabled"], ), @@ -737,23 +737,23 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man return {"commands:strike:performed-manually-warning-location"} @classmethod - def _reload_messages_language(cls, messages_language: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_messages_locale(cls, messages_locale: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ - Reload the selected language for messages to be sent in. + Reload the selected locale for messages to be sent in. Returns the set of settings keys that have been changed. """ - MESSAGES_LANGUAGE_CHANGED: Final[bool] = bool( + MESSAGES_LOCALE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or messages_language != cls._most_recent_yaml["messages-language"] - or "MESSAGES_LANGUAGE" not in cls._settings, + or messages_locale != cls._most_recent_yaml["messages-locale"] + or "MESSAGES_LOCALE" not in cls._settings, ) - if not MESSAGES_LANGUAGE_CHANGED: + if not MESSAGES_LOCALE_CHANGED: return set() - cls._settings["MESSAGES_LANGUAGE"] = messages_language.data + cls._settings["MESSAGES_LOCALE"] = messages_locale.data - return {"messages-language"} + return {"messages-locale"} @classmethod def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders_enabled: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 diff --git a/config/_settings/_yaml/__init__.py b/config/_settings/_yaml/__init__.py index d82d766f5..46955a663 100644 --- a/config/_settings/_yaml/__init__.py +++ b/config/_settings/_yaml/__init__.py @@ -32,13 +32,13 @@ DEFAULT_STATS_COMMAND_LOOKBACK_DAYS, DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION, - TRANSLATED_MESSAGES_LOCALE_CODES, + MESSAGES_LOCALE_CODES, LogLevels, SendIntroductionRemindersFlagType, ) -from .custom_schema_utils import SlugKeyMap -from .custom_validators import ( +from .custom_map_validator import SlugKeyMap +from .custom_scalar_validators import ( DiscordSnowflakeValidator, DiscordWebhookURLValidator, LogLevelValidator, @@ -185,8 +185,8 @@ ), }, ), - strictyaml.Optional("messages-language", default="en-GB"): strictyaml.Enum( - TRANSLATED_MESSAGES_LOCALE_CODES, + strictyaml.Optional("messages-locale", default="en-GB"): strictyaml.Enum( + MESSAGES_LOCALE_CODES, ), strictyaml.Optional("reminders", default=_DEFAULT_REMINDERS_SETTINGS): SlugKeyMap( { diff --git a/config/_settings/_yaml/custom_schema_utils.py b/config/_settings/_yaml/custom_map_validator.py similarity index 100% rename from config/_settings/_yaml/custom_schema_utils.py rename to config/_settings/_yaml/custom_map_validator.py diff --git a/config/_settings/_yaml/custom_validators.py b/config/_settings/_yaml/custom_scalar_validators.py similarity index 100% rename from config/_settings/_yaml/custom_validators.py rename to config/_settings/_yaml/custom_scalar_validators.py diff --git a/config/constants.py b/config/constants.py index a7b57f208..7e422ce54 100644 --- a/config/constants.py +++ b/config/constants.py @@ -7,7 +7,7 @@ "LogLevels", "PROJECT_ROOT", "VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES", - "TRANSLATED_MESSAGES_LOCALE_CODES", + "MESSAGES_LOCALE_CODES", "REQUIRES_RESTART_SETTINGS", "DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME", "DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY", @@ -55,7 +55,7 @@ class LogLevels(str, Enum, metaclass=MetaEnum): PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.resolve() -TRANSLATED_MESSAGES_LOCALE_CODES: Final[frozenset[str]] = frozenset({"en-GB"}) +MESSAGES_LOCALE_CODES: Final[frozenset[str]] = frozenset({"en-GB"}) VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES: Final[frozenset[str]] = frozenset( diff --git a/db/_settings.py b/db/_settings.py index 3eadc4df7..fc93787bd 100644 --- a/db/_settings.py +++ b/db/_settings.py @@ -25,12 +25,15 @@ ) if IMPORTED_BY_MYPY_OR_PYTEST: SECRET_KEY = "unsecure-secret-key" # noqa: S105 + LANGUAGE_CODE = "en-gb" else: from config import settings # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = settings.DISCORD_BOT_TOKEN + LANGUAGE_CODE = settings.MESSAGES_LOCALE_CODE + # Application definition @@ -53,7 +56,6 @@ # Internationalization # https://docs.djangoproject.com/en/stable/topics/i18n/ -LANGUAGE_CODE = "en-gb" TIME_ZONE = "Europe/London" diff --git a/pyproject.toml b/pyproject.toml index b852adbd6..36caaaac8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,6 +172,7 @@ ignore = [ "UP040", # NOTE: Mypy does not currently support PEP 695 type aliases, so they should not be used "PT009", "PT027", + "TRY301", ] task-tags = [ "TODO", From a616c20b02da6ce6b264dcbc6236c53f20a42242 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 19:56:42 +0100 Subject: [PATCH 021/128] Fix Dockerfile --- Dockerfile | 2 +- exceptions.py | 55 --------------------------------------------------- 2 files changed, 1 insertion(+), 56 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6ab73d46e..e6c67fbaa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,7 @@ COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} WORKDIR /app -COPY LICENSE .en[v] exceptions.py main.py messages.json ./ +COPY LICENSE exceptions.py main.py ./ RUN chmod +x main.py COPY utils/ ./utils/ diff --git a/exceptions.py b/exceptions.py index 89278fb4b..ffe966f37 100644 --- a/exceptions.py +++ b/exceptions.py @@ -272,61 +272,6 @@ def ERROR_CODE(cls) -> str: # noqa: N802,N805 return "E1042" -class InvalidMessagesJSONFileError(BaseTeXBotError, 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 - return "The messages JSON file has an invalid structure at the given key." - - def __init__(self, message: str | None = None, dict_key: str | None = None) -> None: - """Initialize an ImproperlyConfigured exception for an invalid messages.json file.""" - self.dict_key: str | None = dict_key - - super().__init__(message) - - -class MessagesJSONFileMissingKeyError(InvalidMessagesJSONFileError): - """Exception class to raise when a key in the messages.json file is missing.""" - - # 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 - return "The messages JSON file is missing a required key." - - def __init__(self, message: str | None = None, missing_key: str | None = None) -> None: - """Initialize a new InvalidMessagesJSONFile exception for a missing key.""" - super().__init__(message, dict_key=missing_key) - - @property - def missing_key(self) -> str | None: - """The key that was missing from the messages.json file.""" - return self.dict_key - - @missing_key.setter - def missing_key(self, value: str | None) -> None: - self.dict_key = value - - -class MessagesJSONFileValueError(InvalidMessagesJSONFileError): - """Exception class to raise when a key in the messages.json file has an invalid value.""" - - # 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 - return "The messages JSON file has an invalid value." - - 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.""" - self.invalid_value: object | None = invalid_value - - super().__init__(message, dict_key) - - class StrikeTrackingError(BaseTeXBotError, RuntimeError): """ Exception class to raise when any error occurs while tracking moderation actions. From 2fc68670b266f6aa1501ae1d139b80cb1fa12c35 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 26 May 2024 20:03:34 +0100 Subject: [PATCH 022/128] Remove unused exceptions --- exceptions.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/exceptions.py b/exceptions.py index ffe966f37..f8d85f1c0 100644 --- a/exceptions.py +++ b/exceptions.py @@ -3,16 +3,12 @@ from collections.abc import Sequence __all__: Sequence[str] = ( - "ImproperlyConfiguredError", "BaseTeXBotError", "BaseErrorWithErrorCode", "BaseDoesNotExistError", "RulesChannelDoesNotExistError", "DiscordMemberNotInMainGuildError", "EveryoneRoleCouldNotBeRetrievedError", - "InvalidMessagesJSONFileError", - "MessagesJSONFileMissingKeyError", - "MessagesJSONFileValueError", "StrikeTrackingError", "NoAuditLogsStrikeTrackingError", "GuildDoesNotExistError", @@ -34,10 +30,6 @@ from classproperties import classproperty -class ImproperlyConfiguredError(Exception): - """Exception class to raise when environment variables are not correctly provided.""" - - class BaseTeXBotError(BaseException, abc.ABC): """Base exception parent class.""" From 43972d9c9657385ed931fa3d033f8ad692e7c66e Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 00:10:00 +0100 Subject: [PATCH 023/128] Fix linting errors --- config/__init__.py | 1 + config/_messages/__init__.py | 21 ++++++++++++++------- config/_settings/__init__.py | 16 ++++++++-------- config/_settings/_yaml/__init__.py | 2 +- config/constants.py | 2 ++ exceptions.py | 2 ++ main.py | 11 ++++++----- utils/tex_bot.py | 2 ++ 8 files changed, 36 insertions(+), 21 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index 7a61dd0bf..e14425fbe 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -58,6 +58,7 @@ def run_setup() -> None: def check_for_deprecated_environment_variables() -> None: + """Raise an error if the old method of configuration (environment variables) is used.""" CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR: Final[DeprecationWarning] = ( DeprecationWarning( ( diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index 85d73e2b8..4125bfa75 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -73,6 +73,10 @@ def load(cls, messages_locale_code: str) -> None: MESSAGES_ALREADY_LOADED_MESSAGE: Final[str] = "Messages have already been loaded." raise RuntimeError(MESSAGES_ALREADY_LOADED_MESSAGE) + NO_MESSAGES_FILE_FOUND_ERROR: Final[RuntimeError] = RuntimeError( + f"No messages file found for locale: {messages_locale_code!r}", + ) + try: messages_locale_file_path: Path = next( path @@ -81,16 +85,16 @@ def load(cls, messages_locale_code: str) -> None: if path.stem == messages_locale_code ) except StopIteration: - NO_MESSAGES_FILE_FOUND_MESSAGE: Final[str] = ( - f"No messages file found for locale: {messages_locale_code!r}" - ) - raise RuntimeError(NO_MESSAGES_FILE_FOUND_MESSAGE) from None + raise NO_MESSAGES_FILE_FOUND_ERROR from None + + if not messages_locale_file_path.is_file(): + raise NO_MESSAGES_FILE_FOUND_ERROR messages_load_error: Exception try: raw_messages: object = json.loads(messages_locale_file_path.read_text()) - if not hasattr(raw_messages, "__get_item__"): + if not hasattr(raw_messages, "__getitem__"): raise TypeError cls._messages["WELCOME_MESSAGES"] = set(raw_messages["welcome-messages"]) # type: ignore[index] @@ -98,7 +102,10 @@ def load(cls, messages_locale_code: str) -> None: raw_messages["opt-in-roles-selectors"], # type: ignore[index] ) - except (json.JSONDecodeError, TypeError) as messages_load_error: - raise NotImplementedError from None + except (json.JSONDecodeError, TypeError, KeyError) as messages_load_error: + INVALID_MESSAGES_FILE_MESSAGE: Final[str] = ( + "Messages file contained invalid contents." + ) + raise ValueError(INVALID_MESSAGES_FILE_MESSAGE) from messages_load_error cls._messages_already_loaded = True diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 600f5bf93..b220a18ea 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -210,7 +210,7 @@ def reload(cls) -> None: cls._reload_strike_performed_manually_warning_location( current_yaml["commands"]["strike"]["performed-manually-warning-location"], ), - cls._reload_messages_locale(current_yaml["messages-locale"]), + cls._reload_messages_locale_code(current_yaml["messages-locale-code"]), cls._reload_send_introduction_reminders_enabled( current_yaml["reminders"]["send-introduction-reminders"]["enabled"], ), @@ -737,23 +737,23 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man return {"commands:strike:performed-manually-warning-location"} @classmethod - def _reload_messages_locale(cls, messages_locale: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_messages_locale_code(cls, messages_locale_code: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] """ Reload the selected locale for messages to be sent in. Returns the set of settings keys that have been changed. """ - MESSAGES_LOCALE_CHANGED: Final[bool] = bool( + MESSAGES_LOCALE_CODE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or messages_locale != cls._most_recent_yaml["messages-locale"] - or "MESSAGES_LOCALE" not in cls._settings, + or messages_locale_code != cls._most_recent_yaml["messages-locale-code"] + or "MESSAGES_LOCALE_CODE" not in cls._settings, ) - if not MESSAGES_LOCALE_CHANGED: + if not MESSAGES_LOCALE_CODE_CHANGED: return set() - cls._settings["MESSAGES_LOCALE"] = messages_locale.data + cls._settings["MESSAGES_LOCALE_CODE"] = messages_locale_code.data - return {"messages-locale"} + return {"messages-locale-code"} @classmethod def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders_enabled: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 diff --git a/config/_settings/_yaml/__init__.py b/config/_settings/_yaml/__init__.py index 46955a663..967c85924 100644 --- a/config/_settings/_yaml/__init__.py +++ b/config/_settings/_yaml/__init__.py @@ -185,7 +185,7 @@ ), }, ), - strictyaml.Optional("messages-locale", default="en-GB"): strictyaml.Enum( + strictyaml.Optional("messages-locale-code", default="en-GB"): strictyaml.Enum( MESSAGES_LOCALE_CODES, ), strictyaml.Optional("reminders", default=_DEFAULT_REMINDERS_SETTINGS): SlugKeyMap( diff --git a/config/constants.py b/config/constants.py index 7e422ce54..b066409d7 100644 --- a/config/constants.py +++ b/config/constants.py @@ -46,6 +46,8 @@ def __contains__(cls, item: object) -> bool: # noqa: N805 class LogLevels(str, Enum, metaclass=MetaEnum): + """Set of valid string values used for logging log-levels.""" + DEBUG = "DEBUG" INFO = "INFO" WARNING = "WARNING" diff --git a/exceptions.py b/exceptions.py index f8d85f1c0..55935adab 100644 --- a/exceptions.py +++ b/exceptions.py @@ -66,6 +66,8 @@ def __repr__(self) -> str: class BotRequiresRestartAfterConfigChange(BaseTeXBotError): + """Exception class to raise to enforce handling of bot restarts after config changes.""" + # noinspection PyMethodParameters,PyPep8Naming @classproperty def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 diff --git a/main.py b/main.py index 020074c1f..0056feb45 100644 --- a/main.py +++ b/main.py @@ -29,17 +29,18 @@ bot = TeXBot(intents=intents) -bot.load_extension("cogs") + bot.load_extension("cogs") def _run_bot() -> NoReturn: bot.run(settings["DISCORD_BOT_TOKEN"]) if bot.EXIT_REASON is TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG: - bot.reset_exit_reason() - config.run_setup() - bot.reload_extension("cogs") - _run_bot() + with SuppressTraceback(): + bot.reset_exit_reason() + config.run_setup() + bot.reload_extension("cogs") + _run_bot() raise SystemExit(bot.EXIT_REASON.value) diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 2a6ecb796..3c7455e11 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -42,6 +42,8 @@ class TeXBotExitReason(IntEnum): + """Enum flag for the reason for TeX-Bot exiting.""" + UNKNOWN_ERROR = -1 KILL_COMMAND_USED = 0 RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG = 1 From 40c6e6186749d123cc61c9b28972c13da7674486 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 00:40:24 +0100 Subject: [PATCH 024/128] Don't add issue details in TODO --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 29d54e37b965efea164197354409a5c3880382cd Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 00:40:48 +0100 Subject: [PATCH 025/128] Fix typing error --- config/_messages/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index 4125bfa75..3e68d1793 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -97,9 +97,11 @@ def load(cls, messages_locale_code: str) -> None: if not hasattr(raw_messages, "__getitem__"): raise TypeError - cls._messages["WELCOME_MESSAGES"] = set(raw_messages["welcome-messages"]) # type: ignore[index] + # noinspection PyUnresolvedReferences + cls._messages["WELCOME_MESSAGES"] = set(raw_messages["welcome-messages"]) + # noinspection PyUnresolvedReferences cls._messages["OPT_IN_ROLES_SELECTORS"] = tuple( - raw_messages["opt-in-roles-selectors"], # type: ignore[index] + raw_messages["opt-in-roles-selectors"], ) except (json.JSONDecodeError, TypeError, KeyError) as messages_load_error: From b4a49261018cfcca42d72b9832c2cb769b273c17 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 03:11:55 +0100 Subject: [PATCH 026/128] Add `get_config_value` command --- cogs/__init__.py | 3 + cogs/change_config.py | 96 +++++++++++++++++++ cogs/delete_all.py | 6 +- cogs/induct.py | 1 + cogs/stats.py | 4 +- config/__init__.py | 46 +++++++++ config/_settings/__init__.py | 51 ++++++++++ .../_yaml/custom_scalar_validators.py | 12 +-- config/constants.py | 1 + utils/error_capture_decorators.py | 2 +- utils/message_sender_components.py | 2 +- utils/tex_bot_base_cog.py | 4 +- utils/tex_bot_contexts.py | 2 +- 13 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 cogs/change_config.py diff --git a/cogs/__init__.py b/cogs/__init__.py index fb8e4a8d2..6daeb0b48 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -9,6 +9,7 @@ __all__: Sequence[str] = ( "ArchiveCommandCog", + "ConfigChangeCommandsCog", "CommandErrorCog", "DeleteAllCommandsCog", "EditMessageCommandCog", @@ -37,6 +38,7 @@ from typing import TYPE_CHECKING from cogs.archive import ArchiveCommandCog +from cogs.change_config import ConfigChangeCommandsCog from cogs.command_error import CommandErrorCog from cogs.delete_all import DeleteAllCommandsCog from cogs.edit_message import EditMessageCommandCog @@ -69,6 +71,7 @@ def setup(bot: TeXBot) -> None: """Add all the cogs to the bot, at bot startup.""" cogs: Iterable[type[TeXBotBaseCog]] = ( ArchiveCommandCog, + ConfigChangeCommandsCog, CommandErrorCog, DeleteAllCommandsCog, EditMessageCommandCog, diff --git a/cogs/change_config.py b/cogs/change_config.py new file mode 100644 index 000000000..d7860a381 --- /dev/null +++ b/cogs/change_config.py @@ -0,0 +1,96 @@ +"""Contains cog classes for any config changing interactions.""" + +from collections.abc import Sequence + +__all__: Sequence[str] = ("ConfigChangeCommandsCog",) + + +import discord +from typing import Final +import config +from utils import ( + CommandChecks, + TeXBotAutocompleteContext, + TeXBotBaseCog, + TeXBotApplicationContext, +) + + +class ConfigChangeCommandsCog(TeXBotBaseCog): + """Cog class that defines the "/config" command group and command call-back methods.""" + + change_config: discord.SlashCommandGroup = discord.SlashCommandGroup( + name="config", + description="Display, edit and get help about TeX-Bot's configuration.", + ) + + @staticmethod + async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice]: # noqa: E501 + """Autocomplete callable that generates the set of available settings names.""" + if not ctx.interaction.user: + return set() + + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + return set() + + return { + discord.OptionChoice(name=setting_name, value=setting_name) + for setting_name + in config.get_loaded_config_settings_names() + } + + @change_config.command( + name="get", + description="Display the current value of a configuration setting.", + ) + @discord.option( # type: ignore[no-untyped-call, misc] + name="setting", + description="The name of the configuration setting value to retrieve.", + input_type=str, + autocomplete=discord.utils.basic_autocomplete(autocomplete_get_settings_names), # type: ignore[arg-type] + required=True, + parameter_name="config_setting_name", + ) + @CommandChecks.check_interaction_user_has_committee_role + @CommandChecks.check_interaction_user_in_main_guild + async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_name: str) -> None: + """Definition & callback response of the "get_config_value" command.""" + if config_setting_name not in config.get_loaded_config_settings_names(): + await self.command_send_error( + ctx, + f"Invalid setting: {config_setting_name!r}", + ) + return + + config_setting_value: str | None = config.view_single_config_setting_value( + config_setting_name, + ) + + if isinstance(config_setting_value, str): + config_setting_value = config_setting_value.strip() + + CONFIG_SETTING_NEEDS_HIDING: Final[bool] = bool( + "token" in config_setting_name + or "cookie" in config_setting_name + or "secret" in config_setting_name + ) + + await ctx.respond( + ( + f"`{config_setting_name.replace("`", "\\`")}` " + f"{ + ( + f"**=** { + "||" if CONFIG_SETTING_NEEDS_HIDING else "" + }`{ + config_setting_value.replace("`", "\\`") + }`{ + "||" if CONFIG_SETTING_NEEDS_HIDING else "" + }" + ) + if config_setting_value + else "**is not set**." + }" + ), + ephemeral=True, + ) 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/induct.py b/cogs/induct.py index 172377a4b..a759b1b11 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -40,6 +40,7 @@ logger: Logger = logging.getLogger("TeX-Bot") + class InductSendMessageCog(TeXBotBaseCog): """Cog class that defines the "/induct" command and its call-back method.""" diff --git a/cogs/stats.py b/cogs/stats.py index e27c263f3..761b88e15 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -160,8 +160,8 @@ class StatsCommandsCog(TeXBotBaseCog): } 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}", ) # noinspection SpellCheckingInspection diff --git a/config/__init__.py b/config/__init__.py index e14425fbe..5057b0ef6 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -15,6 +15,8 @@ "settings", "check_for_deprecated_environment_variables", "messages", + "get_loaded_config_settings_names", + "view_single_config_setting_value", ) @@ -27,6 +29,7 @@ from typing import Final from exceptions import BotRequiresRestartAfterConfigChange +from strictyaml import YAML from ._messages import MessagesAccessor from ._settings import SettingsAccessor @@ -147,3 +150,46 @@ def check_for_deprecated_environment_variables() -> None: ) if deprecated_environment_variable_found: raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR + + +def get_loaded_config_settings_names() -> set[str]: + return settings._loaded_config_settings_names + + +def _get_scalar_config_setting_value(config_setting_name: str, config_settings: YAML) -> str | None: # noqa: E501 + scalar_config_setting: YAML | None = config_settings.get(config_setting_name, None) + + if scalar_config_setting is None: + return scalar_config_setting + + scalar_config_setting_value: object = scalar_config_setting.validator.to_yaml( + scalar_config_setting.data, + ) + + if isinstance(scalar_config_setting_value, str): + return scalar_config_setting_value + + if isinstance(scalar_config_setting_value, Iterable): + return ", ".join(scalar_config_setting_value) + + raise NotImplementedError + + +def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML) -> str | None: # noqa: E501 + if ":" not in partial_config_setting_name: + return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) + + key: str + remainder: str + key, _, remainder = partial_config_setting_name.partition(":") + + mapping_config_setting: YAML | None = config_settings.get(key, None) + + if mapping_config_setting is not None and mapping_config_setting.is_mapping(): + return _get_mapping_config_setting_value(remainder, mapping_config_setting) + + return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) + + +def view_single_config_setting_value(config_setting_name: str) -> str | None: + return _get_mapping_config_setting_value(config_setting_name, settings._most_recent_yaml) diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index b220a18ea..ffdd70895 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -92,6 +92,7 @@ class SettingsAccessor: _settings: ClassVar[dict[str, object]] = {} _most_recent_yaml: ClassVar[YAML | None] = None # type: ignore[no-any-unimported] + _loaded_config_settings_names: ClassVar[set[str]] = set() @classmethod def format_invalid_settings_key_message(cls, item: str) -> str: @@ -243,6 +244,8 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("logging:console:log-level") + CONSOLE_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or console_logging_settings != cls._most_recent_yaml["logging"]["console"], @@ -295,6 +298,10 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.update( + {"logging:discord-channel:log-level", "logging:discord-channel:webhook-url"} + ) + DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_channel_logging_settings != cls._most_recent_yaml["logging"].get( @@ -406,6 +413,8 @@ def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: # type Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("discord:bot-token") + DISCORD_BOT_TOKEN_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_bot_token != cls._most_recent_yaml["discord"]["bot-token"] @@ -425,6 +434,8 @@ def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("discord:main-guild-id") + DISCORD_MAIN_GUILD_ID_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_main_guild_id != cls._most_recent_yaml["discord"]["main-guild-id"] @@ -444,6 +455,8 @@ def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # t Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("community-group:full-name") + GROUP_FULL_NAME_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or group_full_name != cls._most_recent_yaml["community-group"].get( @@ -470,6 +483,8 @@ def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("community-group:short-name") + GROUP_SHORT_NAME_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or group_short_name != cls._most_recent_yaml["community-group"].get( @@ -496,6 +511,8 @@ def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("community-group:links:purchase-membership") + PURCHASE_MEMBERSHIP_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or purchase_membership_link != cls._most_recent_yaml["community-group"][ @@ -521,6 +538,8 @@ def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> se Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("community-group:links:membership-perks") + MEMBERSHIP_PERKS_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or membership_perks_link != cls._most_recent_yaml["community-group"][ @@ -546,6 +565,8 @@ def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("community-group:links:moderation-document") + MODERATION_DOCUMENT_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or moderation_document_link != cls._most_recent_yaml["community-group"]["links"][ @@ -567,6 +588,8 @@ def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("community-group:members-list:url") + MEMBERS_LIST_URL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or members_list_url != cls._most_recent_yaml["community-group"]["members-list"][ @@ -588,6 +611,8 @@ def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cook Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("community-group:members-list:auth-session-cookie") + MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or members_list_auth_session_cookie != cls._most_recent_yaml["community-group"][ @@ -611,6 +636,8 @@ def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("community-group:members-list:id-format") + MEMBERS_LIST_ID_FORMAT_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or members_list_id_format != cls._most_recent_yaml["community-group"][ @@ -632,6 +659,8 @@ def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_pro Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("commands:ping:easter-egg-probability") + PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or ping_command_easter_egg_probability != cls._most_recent_yaml["commands"][ @@ -655,6 +684,8 @@ def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("commands:stats:lookback-days") + STATS_COMMAND_LOOKBACK_DAYS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or stats_command_lookback_days != cls._most_recent_yaml["commands"][ @@ -678,6 +709,8 @@ def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YA Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("commands:stats:displayed-roles") + STATS_COMMAND_DISPLAYED_ROLES_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or stats_command_displayed_roles != cls._most_recent_yaml["commands"][ @@ -699,6 +732,8 @@ def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("commands:strike:timeout-duration") + STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or strike_command_timeout_duration != cls._most_recent_yaml["commands"][ @@ -720,6 +755,8 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("commands:strike:performed-manually-warning-location") + STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or strike_performed_manually_warning_location != cls._most_recent_yaml["commands"][ @@ -743,6 +780,8 @@ def _reload_messages_locale_code(cls, messages_locale_code: YAML) -> set[str]: Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("messages-locale-code") + MESSAGES_LOCALE_CODE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or messages_locale_code != cls._most_recent_yaml["messages-locale-code"] @@ -762,6 +801,8 @@ def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("reminders:send-introduction-reminders:enabled") + SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_introduction_reminders_enabled != cls._most_recent_yaml["reminders"][ @@ -787,6 +828,8 @@ def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_d Waiting begins from the time that the user joined your community group's Discord guild. """ + cls._loaded_config_settings_names.add("reminders:send-introduction-reminders:delay") + SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_introduction_reminders_delay != cls._most_recent_yaml["reminders"][ @@ -810,6 +853,8 @@ def _reload_send_introduction_reminders_interval(cls, send_introduction_reminder Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("reminders:send-introduction-reminders:interval") + SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_introduction_reminders_interval != cls._most_recent_yaml["reminders"][ @@ -833,6 +878,8 @@ def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabl Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("reminders:send-get-roles-reminders:enabled") + SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_get_roles_reminders_enabled != cls._most_recent_yaml["reminders"][ @@ -859,6 +906,8 @@ def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: Waiting begins from the time that the user was inducted as a guest into your community group's Discord guild. """ + cls._loaded_config_settings_names.add("reminders:send-get-roles-reminders:delay") + SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_get_roles_reminders_delay != cls._most_recent_yaml["reminders"][ @@ -882,6 +931,8 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte Returns the set of settings keys that have been changed. """ + cls._loaded_config_settings_names.add("reminders:send-get-roles-reminders:interval") + SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_get_roles_reminders_interval != cls._most_recent_yaml["reminders"][ diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index 3259461c6..a5944f90d 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -297,7 +297,7 @@ def validate_scalar(self, chunk: YAMLChunk) -> SendIntroductionRemindersFlagType chunk.expecting_but_found( ( "when expecting a send-introduction-reminders-flag " - "(one of: \"Once\", \"Interval\" or \"False\")" + "(one of: \"once\", \"interval\" or \"false\")" ), ) raise RuntimeError @@ -314,18 +314,18 @@ def validate_scalar(self, chunk: YAMLChunk) -> SendIntroductionRemindersFlagType @override def to_yaml(self, data: object) -> str: # type: ignore[misc] if isinstance(data, bool): - return "Once" if data else "False" + return "once" if data else "false" if str(data).lower() not in VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES: INVALID_DATA_MESSAGE: Final[str] = ( - f"Got {data} when expecting one of: \"Once\", \"Interval\" or \"False\"." + f"Got {data} when expecting one of: \"once\", \"interval\" or \"false\"." ) raise YAMLSerializationError(INVALID_DATA_MESSAGE) if str(data).lower() in strictyaml_constants.TRUE_VALUES: - return "Once" + return "once" if str(data).lower() in strictyaml_constants.FALSE_VALUES: - return "False" + return "false" - return str(data).lower().title() + return str(data).lower() diff --git a/config/constants.py b/config/constants.py index b066409d7..52cbfbed5 100644 --- a/config/constants.py +++ b/config/constants.py @@ -68,6 +68,7 @@ class LogLevels(str, Enum, metaclass=MetaEnum): { "discord:bot-token", "discord:guild-id", + "messages-locale-code", "reminders:send-introduction-reminders:enabled", "reminders:send-introduction-reminders:delay", "reminders:send-introduction-reminders:interval", diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index 7c7793f2d..f2b435039 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -20,7 +20,7 @@ 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 diff --git a/utils/message_sender_components.py b/utils/message_sender_components.py index 196bb0fe8..b6c8c9762 100644 --- a/utils/message_sender_components.py +++ b/utils/message_sender_components.py @@ -15,7 +15,7 @@ import discord from discord.ui import View -from utils.tex_bot_contexts import TeXBotApplicationContext +from .tex_bot_contexts import TeXBotApplicationContext class MessageSenderComponent(abc.ABC): diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index f16172682..51b98278f 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -20,8 +20,8 @@ CommitteeRoleDoesNotExistError, DiscordMemberNotInMainGuildError, ) -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 diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 28170ecff..99f652730 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -12,7 +12,7 @@ import discord -from utils.tex_bot import TeXBot +from .tex_bot import TeXBot class TeXBotAutocompleteContext(discord.AutocompleteContext): From 2a26a968d8fa614a8142c5385750e64c07476619 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 03:15:43 +0100 Subject: [PATCH 027/128] Remove re-added exceptions after merge --- exceptions/config_changes.py | 27 +++++++++----- exceptions/messages.py | 68 +----------------------------------- 2 files changed, 20 insertions(+), 75 deletions(-) diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index 721a45872..48127f13f 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -2,14 +2,25 @@ from collections.abc import Sequence -__all__: Sequence[str] = ( - "ImproperlyConfiguredError", - "BotRequiresRestartAfterConfigChange", -) +__all__: Sequence[str] = ("BotRequiresRestartAfterConfigChange",) -class ImproperlyConfiguredError(Exception): - """Exception class to raise when environment variables are not correctly provided.""" +from .base import BaseTeXBotError -class BotRequiresRestartAfterConfigChange(Exception): - """Exception class to raise when the bot requires a reboot to apply changes.""" +from classproperties import classproperty + + +class BotRequiresRestartAfterConfigChange(BaseTeXBotError): + """Exception class to raise to enforce handling of bot restarts after config changes.""" + + # 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 + return "TeX-Bot requires a restart due to configuration changes." + + def __init__(self, message: str | None = None, changed_settings: set[str] | None = None) -> None: # noqa: E501 + """Initialize a ValueError exception for a non-existent user ID.""" + self.changed_settings: set[str] | None = changed_settings + + super().__init__(message) diff --git a/exceptions/messages.py b/exceptions/messages.py index 23acfa2ef..3700e5295 100644 --- a/exceptions/messages.py +++ b/exceptions/messages.py @@ -2,70 +2,4 @@ from collections.abc import Sequence -__all__: Sequence[str] = ( - "InvalidMessagesJSONFileError", - "MessagesJSONFileMissingKeyError", - "MessagesJSONFileValueError", -) - - -from classproperties import classproperty - -from .base import BaseTeXBotError -from .config_changes import ImproperlyConfiguredError - - -class InvalidMessagesJSONFileError(BaseTeXBotError, 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 - return "The messages JSON file has an invalid structure at the given key." - - def __init__(self, message: str | None = None, dict_key: str | None = None) -> None: - """Initialize an ImproperlyConfigured exception for an invalid messages.json file.""" - self.dict_key: str | None = dict_key - - super().__init__(message) - - -class MessagesJSONFileMissingKeyError(InvalidMessagesJSONFileError): - """Exception class to raise when a key in the messages.json file is missing.""" - - # 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 - return "The messages JSON file is missing a required key." - - def __init__(self, message: str | None = None, missing_key: str | None = None) -> None: - """Initialize a new InvalidMessagesJSONFile exception for a missing key.""" - super().__init__(message, dict_key=missing_key) - - @property - def missing_key(self) -> str | None: - """The key that was missing from the messages.json file.""" - return self.dict_key - - @missing_key.setter - def missing_key(self, value: str | None) -> None: - self.dict_key = value - - -class MessagesJSONFileValueError(InvalidMessagesJSONFileError): - """Exception class to raise when a key in the messages.json file has an invalid value.""" - - # 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 - return "The messages JSON file has an invalid value." - - 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.""" - self.invalid_value: object | None = invalid_value - - super().__init__(message, dict_key) - +__all__: Sequence[str] = () From 6ae017635c1a4ba9d244a3999d3165fe1bc91bc7 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 03:18:35 +0100 Subject: [PATCH 028/128] Fix exception imports after being removed --- exceptions/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/exceptions/__init__.py b/exceptions/__init__.py index a5efadf3a..4c81e9f71 100644 --- a/exceptions/__init__.py +++ b/exceptions/__init__.py @@ -17,16 +17,11 @@ "EveryoneRoleCouldNotBeRetrievedError", "StrikeTrackingError", "NoAuditLogsStrikeTrackingError", - "MessagesJSONFileMissingKeyError", - "MessagesJSONFileValueError", - "InvalidMessagesJSONFileError", - "ImproperlyConfiguredError", "BotRequiresRestartAfterConfigChange", ) from .config_changes import ( BotRequiresRestartAfterConfigChange, - ImproperlyConfiguredError, ) from .does_not_exist import ( ArchivistRoleDoesNotExistError, @@ -44,9 +39,4 @@ DiscordMemberNotInMainGuildError, EveryoneRoleCouldNotBeRetrievedError, ) -from .messages import ( - InvalidMessagesJSONFileError, - MessagesJSONFileMissingKeyError, - MessagesJSONFileValueError, -) from .strike import NoAuditLogsStrikeTrackingError, StrikeTrackingError From aa796faeeacda08025eb8b9c2520f5567cbd3f15 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 03:22:11 +0100 Subject: [PATCH 029/128] Fix linting errors --- cogs/change_config.py | 10 ++++++---- config/__init__.py | 9 ++++++--- config/_settings/__init__.py | 2 +- exceptions/config_changes.py | 4 ++-- utils/error_capture_decorators.py | 1 + utils/tex_bot_base_cog.py | 1 + 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index d7860a381..bc29a12ca 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -5,14 +5,16 @@ __all__: Sequence[str] = ("ConfigChangeCommandsCog",) -import discord from typing import Final + +import discord + import config from utils import ( CommandChecks, + TeXBotApplicationContext, TeXBotAutocompleteContext, TeXBotBaseCog, - TeXBotApplicationContext, ) @@ -53,7 +55,7 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set ) @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild - async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_name: str) -> None: + async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_name: str) -> None: # noqa: E501 """Definition & callback response of the "get_config_value" command.""" if config_setting_name not in config.get_loaded_config_settings_names(): await self.command_send_error( @@ -72,7 +74,7 @@ async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_n CONFIG_SETTING_NEEDS_HIDING: Final[bool] = bool( "token" in config_setting_name or "cookie" in config_setting_name - or "secret" in config_setting_name + or "secret" in config_setting_name # noqa: COM812 ) await ctx.respond( diff --git a/config/__init__.py b/config/__init__.py index 5057b0ef6..6c2998030 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -28,9 +28,10 @@ from logging import Logger from typing import Final -from exceptions import BotRequiresRestartAfterConfigChange from strictyaml import YAML +from exceptions import BotRequiresRestartAfterConfigChange + from ._messages import MessagesAccessor from ._settings import SettingsAccessor from .constants import MESSAGES_LOCALE_CODES, PROJECT_ROOT, LogLevels @@ -153,7 +154,8 @@ def check_for_deprecated_environment_variables() -> None: def get_loaded_config_settings_names() -> set[str]: - return settings._loaded_config_settings_names + # noinspection PyProtectedMember + return settings._loaded_config_settings_names # noqa: SLF001 def _get_scalar_config_setting_value(config_setting_name: str, config_settings: YAML) -> str | None: # noqa: E501 @@ -192,4 +194,5 @@ def _get_mapping_config_setting_value(partial_config_setting_name: str, config_s def view_single_config_setting_value(config_setting_name: str) -> str | None: - return _get_mapping_config_setting_value(config_setting_name, settings._most_recent_yaml) + # noinspection PyProtectedMember + return _get_mapping_config_setting_value(config_setting_name, settings._most_recent_yaml) # noqa: SLF001 diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index ffdd70895..de8356743 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -299,7 +299,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y Returns the set of settings keys that have been changed. """ cls._loaded_config_settings_names.update( - {"logging:discord-channel:log-level", "logging:discord-channel:webhook-url"} + {"logging:discord-channel:log-level", "logging:discord-channel:webhook-url"}, ) DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index 48127f13f..2e6e33768 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -5,10 +5,10 @@ __all__: Sequence[str] = ("BotRequiresRestartAfterConfigChange",) -from .base import BaseTeXBotError - from classproperties import classproperty +from .base import BaseTeXBotError + class BotRequiresRestartAfterConfigChange(BaseTeXBotError): """Exception class to raise to enforce handling of bot restarts after config changes.""" diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index f2b435039..99636289a 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Final, ParamSpec, TypeVar from exceptions import GuildDoesNotExistError, StrikeTrackingError + from .tex_bot_base_cog import TeXBotBaseCog if TYPE_CHECKING: diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index df4faff13..0d18cd27d 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -19,6 +19,7 @@ from exceptions.base import ( BaseDoesNotExistError, ) + from .tex_bot import TeXBot from .tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext From c6df83445e64ad38d188561b865e3d62c59c1193 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 03:23:17 +0100 Subject: [PATCH 030/128] Fix typing errors --- config/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index 6c2998030..72dfd452f 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -158,8 +158,8 @@ def get_loaded_config_settings_names() -> set[str]: return settings._loaded_config_settings_names # noqa: SLF001 -def _get_scalar_config_setting_value(config_setting_name: str, config_settings: YAML) -> str | None: # noqa: E501 - scalar_config_setting: YAML | None = config_settings.get(config_setting_name, None) +def _get_scalar_config_setting_value(config_setting_name: str, config_settings: YAML) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 + scalar_config_setting: YAML | None = config_settings.get(config_setting_name, None) # type: ignore[no-any-unimported] if scalar_config_setting is None: return scalar_config_setting @@ -177,7 +177,7 @@ def _get_scalar_config_setting_value(config_setting_name: str, config_settings: raise NotImplementedError -def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML) -> str | None: # noqa: E501 +def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 if ":" not in partial_config_setting_name: return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) @@ -185,7 +185,7 @@ def _get_mapping_config_setting_value(partial_config_setting_name: str, config_s remainder: str key, _, remainder = partial_config_setting_name.partition(":") - mapping_config_setting: YAML | None = config_settings.get(key, None) + mapping_config_setting: YAML | None = config_settings.get(key, None) # type: ignore[no-any-unimported] if mapping_config_setting is not None and mapping_config_setting.is_mapping(): return _get_mapping_config_setting_value(remainder, mapping_config_setting) From b591fb0a33b3f8c459bebbae5f8614b1a512931b Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 27 May 2024 16:15:11 +0100 Subject: [PATCH 031/128] Implement config help command --- cogs/change_config.py | 69 +++++- config/__init__.py | 21 +- config/_settings/__init__.py | 51 ----- config/_settings/_yaml/__init__.py | 5 +- config/constants.py | 344 ++++++++++++++++++++++++++++- 5 files changed, 427 insertions(+), 63 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index bc29a12ca..11787176b 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -10,6 +10,7 @@ import discord import config +from config import ConfigSettingHelp from utils import ( CommandChecks, TeXBotApplicationContext, @@ -38,7 +39,7 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set return { discord.OptionChoice(name=setting_name, value=setting_name) for setting_name - in config.get_loaded_config_settings_names() + in config.CONFIG_SETTINGS_HELPS } @change_config.command( @@ -57,7 +58,7 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set @CommandChecks.check_interaction_user_in_main_guild async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_name: str) -> None: # noqa: E501 """Definition & callback response of the "get_config_value" command.""" - if config_setting_name not in config.get_loaded_config_settings_names(): + if config_setting_name not in config.CONFIG_SETTINGS_HELPS: await self.command_send_error( ctx, f"Invalid setting: {config_setting_name!r}", @@ -96,3 +97,67 @@ async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ), ephemeral=True, ) + + @change_config.command( + name="help", + description="Show the description of what a given configuration setting does.", + ) + @discord.option( # type: ignore[no-untyped-call, misc] + name="setting", + description="The name of the configuration setting to show the description of.", + input_type=str, + autocomplete=discord.utils.basic_autocomplete(autocomplete_get_settings_names), # type: ignore[arg-type] + required=True, + parameter_name="config_setting_name", + ) + @CommandChecks.check_interaction_user_has_committee_role + @CommandChecks.check_interaction_user_in_main_guild + async def help_config_setting(self, ctx: TeXBotApplicationContext, config_setting_name: str) -> None: # noqa: E501 + """Definition & callback response of the "help_config_setting" command.""" + if config_setting_name not in config.CONFIG_SETTINGS_HELPS: + await self.command_send_error( + ctx, + f"Invalid setting: {config_setting_name!r}", + ) + return + + config_setting_help: ConfigSettingHelp = config.CONFIG_SETTINGS_HELPS[ + config_setting_name + ] + + # noinspection PyProtectedMember + await ctx.respond( + ( + f"## `{ + config_setting_name.replace("`", "\\`") + }`\n" + f"{ + config_setting_help.description.replace( + "**`@TeX-Bot`**", + self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", + ).replace( + "TeX-Bot", + self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", + ).replace( + "the bot", + self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", + ) + }\n\n" + f"{ + f"{config_setting_help.value_type_message}\n\n" + if config_setting_help.value_type_message + else "" + }" + f"This setting is **{ + "required" if config_setting_help.required else "optional" + }**.\n\n" + f"{ + f"The default value for this setting is: `{ + config_setting_help.default.replace("`", "\\`") + }`" + if config_setting_help.default + else "" + }" + ), + ephemeral=True, + ) diff --git a/config/__init__.py b/config/__init__.py index 72dfd452f..a9a5bd763 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -15,8 +15,9 @@ "settings", "check_for_deprecated_environment_variables", "messages", - "get_loaded_config_settings_names", "view_single_config_setting_value", + "CONFIG_SETTINGS_HELPS", + "ConfigSettingHelp", ) @@ -34,7 +35,13 @@ from ._messages import MessagesAccessor from ._settings import SettingsAccessor -from .constants import MESSAGES_LOCALE_CODES, PROJECT_ROOT, LogLevels +from .constants import ( + CONFIG_SETTINGS_HELPS, + MESSAGES_LOCALE_CODES, + PROJECT_ROOT, + ConfigSettingHelp, + LogLevels, +) logger: Final[Logger] = logging.getLogger("TeX-Bot") @@ -153,11 +160,6 @@ def check_for_deprecated_environment_variables() -> None: raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR -def get_loaded_config_settings_names() -> set[str]: - # noinspection PyProtectedMember - return settings._loaded_config_settings_names # noqa: SLF001 - - def _get_scalar_config_setting_value(config_setting_name: str, config_settings: YAML) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 scalar_config_setting: YAML | None = config_settings.get(config_setting_name, None) # type: ignore[no-any-unimported] @@ -172,6 +174,10 @@ def _get_scalar_config_setting_value(config_setting_name: str, config_settings: return scalar_config_setting_value if isinstance(scalar_config_setting_value, Iterable): + with contextlib.suppress(StopIteration): + if not isinstance(next(iter(scalar_config_setting_value)), str): + raise TypeError + return ", ".join(scalar_config_setting_value) raise NotImplementedError @@ -194,5 +200,6 @@ def _get_mapping_config_setting_value(partial_config_setting_name: str, config_s def view_single_config_setting_value(config_setting_name: str) -> str | None: + """Return the value of a single configuration setting.""" # noinspection PyProtectedMember return _get_mapping_config_setting_value(config_setting_name, settings._most_recent_yaml) # noqa: SLF001 diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index de8356743..b220a18ea 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -92,7 +92,6 @@ class SettingsAccessor: _settings: ClassVar[dict[str, object]] = {} _most_recent_yaml: ClassVar[YAML | None] = None # type: ignore[no-any-unimported] - _loaded_config_settings_names: ClassVar[set[str]] = set() @classmethod def format_invalid_settings_key_message(cls, item: str) -> str: @@ -244,8 +243,6 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("logging:console:log-level") - CONSOLE_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or console_logging_settings != cls._most_recent_yaml["logging"]["console"], @@ -298,10 +295,6 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.update( - {"logging:discord-channel:log-level", "logging:discord-channel:webhook-url"}, - ) - DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_channel_logging_settings != cls._most_recent_yaml["logging"].get( @@ -413,8 +406,6 @@ def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: # type Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("discord:bot-token") - DISCORD_BOT_TOKEN_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_bot_token != cls._most_recent_yaml["discord"]["bot-token"] @@ -434,8 +425,6 @@ def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("discord:main-guild-id") - DISCORD_MAIN_GUILD_ID_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or discord_main_guild_id != cls._most_recent_yaml["discord"]["main-guild-id"] @@ -455,8 +444,6 @@ def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # t Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("community-group:full-name") - GROUP_FULL_NAME_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or group_full_name != cls._most_recent_yaml["community-group"].get( @@ -483,8 +470,6 @@ def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("community-group:short-name") - GROUP_SHORT_NAME_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or group_short_name != cls._most_recent_yaml["community-group"].get( @@ -511,8 +496,6 @@ def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("community-group:links:purchase-membership") - PURCHASE_MEMBERSHIP_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or purchase_membership_link != cls._most_recent_yaml["community-group"][ @@ -538,8 +521,6 @@ def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> se Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("community-group:links:membership-perks") - MEMBERSHIP_PERKS_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or membership_perks_link != cls._most_recent_yaml["community-group"][ @@ -565,8 +546,6 @@ def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("community-group:links:moderation-document") - MODERATION_DOCUMENT_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or moderation_document_link != cls._most_recent_yaml["community-group"]["links"][ @@ -588,8 +567,6 @@ def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("community-group:members-list:url") - MEMBERS_LIST_URL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or members_list_url != cls._most_recent_yaml["community-group"]["members-list"][ @@ -611,8 +588,6 @@ def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cook Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("community-group:members-list:auth-session-cookie") - MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or members_list_auth_session_cookie != cls._most_recent_yaml["community-group"][ @@ -636,8 +611,6 @@ def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("community-group:members-list:id-format") - MEMBERS_LIST_ID_FORMAT_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or members_list_id_format != cls._most_recent_yaml["community-group"][ @@ -659,8 +632,6 @@ def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_pro Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("commands:ping:easter-egg-probability") - PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or ping_command_easter_egg_probability != cls._most_recent_yaml["commands"][ @@ -684,8 +655,6 @@ def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("commands:stats:lookback-days") - STATS_COMMAND_LOOKBACK_DAYS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or stats_command_lookback_days != cls._most_recent_yaml["commands"][ @@ -709,8 +678,6 @@ def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YA Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("commands:stats:displayed-roles") - STATS_COMMAND_DISPLAYED_ROLES_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or stats_command_displayed_roles != cls._most_recent_yaml["commands"][ @@ -732,8 +699,6 @@ def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("commands:strike:timeout-duration") - STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or strike_command_timeout_duration != cls._most_recent_yaml["commands"][ @@ -755,8 +720,6 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("commands:strike:performed-manually-warning-location") - STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or strike_performed_manually_warning_location != cls._most_recent_yaml["commands"][ @@ -780,8 +743,6 @@ def _reload_messages_locale_code(cls, messages_locale_code: YAML) -> set[str]: Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("messages-locale-code") - MESSAGES_LOCALE_CODE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or messages_locale_code != cls._most_recent_yaml["messages-locale-code"] @@ -801,8 +762,6 @@ def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("reminders:send-introduction-reminders:enabled") - SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_introduction_reminders_enabled != cls._most_recent_yaml["reminders"][ @@ -828,8 +787,6 @@ def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_d Waiting begins from the time that the user joined your community group's Discord guild. """ - cls._loaded_config_settings_names.add("reminders:send-introduction-reminders:delay") - SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_introduction_reminders_delay != cls._most_recent_yaml["reminders"][ @@ -853,8 +810,6 @@ def _reload_send_introduction_reminders_interval(cls, send_introduction_reminder Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("reminders:send-introduction-reminders:interval") - SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_introduction_reminders_interval != cls._most_recent_yaml["reminders"][ @@ -878,8 +833,6 @@ def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabl Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("reminders:send-get-roles-reminders:enabled") - SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_get_roles_reminders_enabled != cls._most_recent_yaml["reminders"][ @@ -906,8 +859,6 @@ def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: Waiting begins from the time that the user was inducted as a guest into your community group's Discord guild. """ - cls._loaded_config_settings_names.add("reminders:send-get-roles-reminders:delay") - SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_get_roles_reminders_delay != cls._most_recent_yaml["reminders"][ @@ -931,8 +882,6 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte Returns the set of settings keys that have been changed. """ - cls._loaded_config_settings_names.add("reminders:send-get-roles-reminders:interval") - SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or send_get_roles_reminders_interval != cls._most_recent_yaml["reminders"][ diff --git a/config/_settings/_yaml/__init__.py b/config/_settings/_yaml/__init__.py index 967c85924..20aaff5b1 100644 --- a/config/_settings/_yaml/__init__.py +++ b/config/_settings/_yaml/__init__.py @@ -21,6 +21,7 @@ DEFAULT_CONSOLE_LOG_LEVEL, DEFAULT_DISCORD_LOGGING_LOG_LEVEL, DEFAULT_MEMBERS_LIST_ID_FORMAT, + DEFAULT_MESSAGE_LOCALE_CODE, DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY, DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED, @@ -185,8 +186,8 @@ ), }, ), - strictyaml.Optional("messages-locale-code", default="en-GB"): strictyaml.Enum( - MESSAGES_LOCALE_CODES, + strictyaml.Optional("messages-locale-code", default=DEFAULT_MESSAGE_LOCALE_CODE): ( + strictyaml.Enum(MESSAGES_LOCALE_CODES) ), strictyaml.Optional("reminders", default=_DEFAULT_REMINDERS_SETTINGS): SlugKeyMap( { diff --git a/config/constants.py b/config/constants.py index 52cbfbed5..26db25503 100644 --- a/config/constants.py +++ b/config/constants.py @@ -5,6 +5,7 @@ __all__: Sequence[str] = ( "SendIntroductionRemindersFlagType", "LogLevels", + "ConfigSettingHelp", "PROJECT_ROOT", "VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES", "MESSAGES_LOCALE_CODES", @@ -18,18 +19,21 @@ "DEFAULT_STATS_COMMAND_DISPLAYED_ROLES", "DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION", "DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION", + "DEFAULT_MESSAGE_LOCALE_CODE", "DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED", "DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY", "DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL", "DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED", "DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY", "DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL", + "CONFIG_SETTINGS_HELPS", ) +from collections.abc import Iterable, Mapping from enum import Enum, EnumMeta from pathlib import Path -from typing import Final, Literal, TypeAlias +from typing import Final, Literal, NamedTuple, TypeAlias from strictyaml import constants as strictyaml_constants @@ -55,6 +59,30 @@ class LogLevels(str, Enum, metaclass=MetaEnum): CRITICAL = "CRITICAL" +class ConfigSettingHelp(NamedTuple): + """Container to hold help information about a single configuration setting.""" + + description: str + value_type_message: str | None + required: bool = True + default: str | None = None + + @classmethod + def _selectable_required_format_message(cls, options: Iterable[str]) -> str: + return f"Must be one of: `{"`, `".join(options)}`." + + @classmethod + def _custom_required_format_message(cls, type_value: str, info_link: str | None = None) -> str: # noqa: E501 + return ( + f"Must be a valid { + type_value.lower().replace("discord", "Discord").replace( + "id", + "ID", + ).replace("url", "URL").strip(".") + }{f" (see <{info_link}>)" if info_link else ""}." + ) + + PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.resolve() MESSAGES_LOCALE_CODES: Final[frozenset[str]] = frozenset({"en-GB"}) @@ -107,9 +135,323 @@ class LogLevels(str, Enum, metaclass=MetaEnum): ] DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION: Final[str] = "24h" DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION: Final[str] = "DM" +DEFAULT_MESSAGE_LOCALE_CODE: Final[str] = "en-GB" DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED: Final[SendIntroductionRemindersFlagType] = "once" DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY: Final[str] = "40h" DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL: Final[str] = "6h" DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED: Final[bool] = True DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY: Final[str] = "40h" DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL: Final[str] = "6h" + +# noinspection PyProtectedMember +CONFIG_SETTINGS_HELPS: Mapping[str, ConfigSettingHelp] = { + "logging:console:log-level": ConfigSettingHelp( + description=( + "The minimum level that logs must meet in order to be logged " + "to the console output stream." + ), + value_type_message=ConfigSettingHelp._selectable_required_format_message(LogLevels), # noqa: SLF001 + required=False, + default=DEFAULT_CONSOLE_LOG_LEVEL, + ), + "logging:discord-channel:log-level": ConfigSettingHelp( + description=( + "The minimum level that logs must meet in order to be logged " + "to the Discord log channel." + ), + value_type_message=ConfigSettingHelp._selectable_required_format_message(LogLevels), # noqa: SLF001 + required=False, + default=DEFAULT_DISCORD_LOGGING_LOG_LEVEL, + ), + "logging:discord-channel:webhook-url": ConfigSettingHelp( + description=( + "The webhook URL of the Discord text channel where error logs should be sent.\n" + "Error logs will always be sent to the console, " + "this setting allows them to also be sent to a Discord log channel." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + "Discord webhook URL", + "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks", + ), + required=False, + default=None, + ), + "discord:bot-token": ConfigSettingHelp( + description=( + "The Discord token for the bot you created " + "(available on your bot page in the developer portal: )." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + "Discord bot token", + "https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts", + ), + required=True, + default=None, + ), + "discord:main-guild-id": ConfigSettingHelp( + description="The ID of your community group's main Discord guild.", + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + "Discord guild ID", + "https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id", + ), + required=True, + default=None, + ), + "community-group:full-name": ConfigSettingHelp( + description=( + "The full name of your community group, do **NOT** use an abbreviation.\n" + "This is substituted into many error/welcome messages " + "sent into your Discord guild, by **`@TeX-Bot`**.\n" + "If this is not set the group-full-name will be retrieved " + "from the name of your group's Discord guild." + ), + value_type_message=None, + required=False, + default=None, + ), + "community-group:short-name": ConfigSettingHelp( + description=( + "The short colloquial name of your community group, " + "it is recommended that you set this to be an abbreviation of your group's name.\n" + "If this is not set the group-short-name will be determined " + "from your group's full name." + ), + value_type_message=None, + required=False, + default=None, + ), + "community-group:links:purchase-membership": ConfigSettingHelp( + description=( + "The link to the page where guests can purchase a full membership " + "to join your community group." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message("URL"), # noqa: SLF001 + required=False, + default=None, + ), + "community-group:links:membership-perks": ConfigSettingHelp( + description=( + "The link to the page where guests can find out information " + "about the perks that they will receive " + "once they purchase a membership to your community group." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message("URL"), # noqa: SLF001 + required=False, + default=None, + ), + "community-group:links:moderation-document": ConfigSettingHelp( + description="The link to your group's Discord guild moderation document.", + value_type_message=ConfigSettingHelp._custom_required_format_message("URL"), # noqa: SLF001 + required=True, + default=None, + ), + "community-group:members-list:url": ConfigSettingHelp( + description=( + "The URL to retrieve the list of IDs of people that have purchased a membership " + "to your community group.\n" + "Ensure that all members are visible without pagination, " + "(for example, " + "if your members-list is found on the UoB Guild of Students website, " + "ensure the URL includes the \"sort by groups\" option)." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message("URL"), # noqa: SLF001 + required=True, + default=None, + ), + "community-group:members-list:auth-session-cookie": ConfigSettingHelp( + description=( + "The members-list authentication session cookie.\n" + "If your group's members-list is stored at a URL that requires authentication, " + "this session cookie should authenticate **`@TeX-Bot`** " + "to view your group's members-list, " + "as if it were logged in to the website as a Committee member.\n" + "If your members-list is found on the UoB Guild of Students website, " + "this can be extracted from your web-browser: " + "after manually logging in to view your members-list, " + "it will probably be listed as a cookie named `.ASPXAUTH`." + ), + value_type_message=None, + required=True, + default=None, + ), + "community-group:members-list:id-format": ConfigSettingHelp( + description="The format that IDs are stored in within your members-list.", + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + "regex matcher string", + ), + required=False, + default=DEFAULT_MEMBERS_LIST_ID_FORMAT, + ), + "commands:ping:easter-egg-probability": ConfigSettingHelp( + description=( + "The probability that the more rare ping command response will be sent " + "instead of the normal one." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + "float, inclusively between 1 & 0", + ), + required=False, + default=str(DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY), + ), + "commands:stats:lookback-days": ConfigSettingHelp( + description=( + "The number of days to look over messages sent, to generate statistics data." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + "float representing the number of days to look back through", + ), + required=False, + default=str(DEFAULT_STATS_COMMAND_LOOKBACK_DAYS), + ), + "commands:stats:displayed-roles": ConfigSettingHelp( + description=( + "The names of the roles to gather statistics about, " + "to display in bar chart graphs." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + "comma seperated list of strings of role names", + ), + required=False, + default=",".join(DEFAULT_STATS_COMMAND_DISPLAYED_ROLES), + ), + "commands:strike:timeout-duration": ConfigSettingHelp( + description=( + "The amount of time to timeout a user when using the **`/strike`** command." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + ( + "string of the seconds, minutes, hours, days or weeks " + "to timeout a user (format: `smhdw`)" + ), + ), + required=False, + default=DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, + ), + "commands:strike:performed-manually-warning-location": ConfigSettingHelp( + description=( + "The name of the channel, that warning messages will be sent to " + "when a committee-member manually applies a moderation action " + "(instead of using the `/strike` command).\n" + "This can be the name of **ANY** Discord channel " + "(so the offending person *will* be able to see these messages " + "if a public channel is chosen)." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + ( + "name of a Discord channel in your group's Discord guild, " + "or the value `DM` " + "(which indicates that the messages will be sent " + "in the committee-member's DMs)" + ), + ), + required=False, + default=DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION, + ), + "messages-locale-code": ConfigSettingHelp( + description=( + "The locale code used to select the language response messages will be given in." + ), + value_type_message=ConfigSettingHelp._selectable_required_format_message( # noqa: SLF001 + MESSAGES_LOCALE_CODES, + ), + required=False, + default=DEFAULT_MESSAGE_LOCALE_CODE, + ), + "reminders:send-introduction-reminders:enabled": ConfigSettingHelp( + description=( + "Whether introduction reminders will be sent to Discord members " + "that are not inducted, " + "saying that they need to send an introduction to be allowed access." + ), + value_type_message=ConfigSettingHelp._selectable_required_format_message( # noqa: SLF001 + ( + str(flag_value).lower() + for flag_value + in getattr(SendIntroductionRemindersFlagType, "__args__") # noqa: B009 + ), + ), + required=False, + default=str(DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED).lower(), + ), + "reminders:send-introduction-reminders:delay": ConfigSettingHelp( + description=( + "How long to wait after a user joins your guild " + "before sending them the first/only message " + "to remind them to send an introduction.\n" + "Is ignored if `reminders:send-introduction-reminders:enabled` **=** `false`.\n" + "The delay must be longer than or equal to 1 day (in any allowed format)." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + ( + "string of the seconds, minutes, hours, days or weeks " + "before the first/only reminder is sent " + "(format: `smhdw`)" + ), + ), + required=False, + default=DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, + ), + "reminders:send-introduction-reminders:interval": ConfigSettingHelp( + description=( + "The interval of time between sending out reminders " + "to Discord members that are not inducted, " + "saying that they need to send an introduction to be allowed access.\n" + "Is ignored if `reminders:send-introduction-reminders:enabled` **=** `false`." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + ( + "string of the seconds, minutes, or hours between reminders " + "(format: `smh`)" + ), + ), + required=False, + default=DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, + ), + "reminders:send-get-roles-reminders:enabled": ConfigSettingHelp( + description=( + "Whether reminders will be sent to Discord members that have been inducted, " + "saying that they can get opt-in roles. " + "(This message will be only sent once per Discord member)." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + "boolean value (either `true` or `false`)", + ), + required=False, + default=str(DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED).lower(), + ), + "reminders:send-get-roles-reminders:delay": ConfigSettingHelp( + description=( + "How long to wait after a user is inducted " + "before sending them the message to get some opt-in roles.\n" + "Is ignored if `reminders:send-get-roles-reminders:enabled` **=** `false`.\n" + "The delay must be longer than or equal to 1 day (in any allowed format)." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + ( + "string of the seconds, minutes, hours, days or weeks " + "before the first/only reminder is sent " + "(format: `smhdw`)" + ), + ), + required=False, + default=DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, + ), + "reminders:send-get-roles-reminders:interval": ConfigSettingHelp( + description=( + "The interval of time between sending out reminders " + "to Discord members that have been inducted, " + "saying that they can get opt-in roles. " + "(This message will be only sent once, " + "the interval is just how often to check for new guests).\n" + "Is ignored if `reminders:send-get-roles-reminders:enabled` **=** `false`." + ), + value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + ( + "string of the seconds, minutes, or hours between reminders " + "(format: `smh`)" + ), + ), + required=False, + default=DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, + ), +} From 7206f01d15f63413f31d6b993f0dcb27aa83a466 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 8 Jun 2024 12:31:53 +0100 Subject: [PATCH 032/128] Update dependencies --- poetry.lock | 187 ++++++++++++++++++++++++++-------------------------- 1 file changed, 93 insertions(+), 94 deletions(-) diff --git a/poetry.lock b/poetry.lock index 04afa06dd..4e69ea8fd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -197,13 +197,13 @@ lxml = ["lxml"] [[package]] name = "ccft-pymarkdown" -version = "1.1.1" +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.1-py3-none-any.whl", hash = "sha256:adeb27e94ef67ac368be7d24eac712480a9a6f7294e368253f0b3c516e6e1e8f"}, - {file = "ccft_pymarkdown-1.1.1.tar.gz", hash = "sha256:1cd3c5c797da87ad33ed40502a85703104f80f520f5ba33b0477cc171cff201f"}, + {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] @@ -213,13 +213,13 @@ setuptools = ">=70.0,<71.0" [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" 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.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -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] @@ -572,53 +573,53 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "fonttools" -version = "4.52.1" +version = "4.53.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.52.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:67a30b872e79577e5319ce660ede4a5131fa8a45de76e696746545e17db4437f"}, - {file = "fonttools-4.52.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a5bff35738f8f6607c4303561ee1d1e5f64d5b14cf3c472d3030566c82e763"}, - {file = "fonttools-4.52.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c9622593dfff042480a1b7e5b72c4d7dc00b96d2b4f98b0bf8acf071087e0db"}, - {file = "fonttools-4.52.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33cfc9fe27af5e113d157d5147e24fc8e5bda3c5aadb55bea9847ec55341ce30"}, - {file = "fonttools-4.52.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa5bec5027d947ee4b2242caecf7dc6e4ea03833e92e9b5211ebb6ab4eede8b2"}, - {file = "fonttools-4.52.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10e44bf8e5654050a332a79285bacd6bd3069084540aec46c0862391147a1daa"}, - {file = "fonttools-4.52.1-cp310-cp310-win32.whl", hash = "sha256:7fba390ac2ca18ebdd456f3a9acfb4557d6dcb2eaba5cc3eadce01003892a770"}, - {file = "fonttools-4.52.1-cp310-cp310-win_amd64.whl", hash = "sha256:15df3517eb95035422a5c953ca19aac99913c16aa0e4ef061aeaef5f3bcaf369"}, - {file = "fonttools-4.52.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40730aab9cf42286f314b985b483eea574f1bcf3a23e28223084cbb9e256457c"}, - {file = "fonttools-4.52.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a19bc2be3af5b22ff5c7fe858c380862e31052c74f62e2c6d565ed0855bed7a6"}, - {file = "fonttools-4.52.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f859066d8afde53f2ddabcd0705061e6d9d9868757c6ae28abe49bc885292df4"}, - {file = "fonttools-4.52.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74cd3e3e9ba501e87a391b62e91f7b1610e8b3f3d706a368e5aee51614c1674e"}, - {file = "fonttools-4.52.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:958957b81418647f66820480363cb617ba6b5bcf189ec6c4cea307d051048545"}, - {file = "fonttools-4.52.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56addf1f995d94dad13aaaf56eb6def3d9ca97c2fada5e27af8190b3141e8633"}, - {file = "fonttools-4.52.1-cp311-cp311-win32.whl", hash = "sha256:fea5456b2af42db8ecb1a6c2f144655ca6dcdcebd970f3145c56e668084ded7e"}, - {file = "fonttools-4.52.1-cp311-cp311-win_amd64.whl", hash = "sha256:228faab7638cd726cdde5e2ec9ee10f780fbf9de9aa38d7f1e56a270437dff36"}, - {file = "fonttools-4.52.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7c6aeb0d53e2ea92009b11c3d4ad9c03d0ecdfe602d547bed8537836e464f51e"}, - {file = "fonttools-4.52.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e871123d12c92e2c9bda6369b69ce2da9cef40b119cc340451e413e90355fa38"}, - {file = "fonttools-4.52.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ff8857dc9bb3e407c25aef3e025409cfbb23adb646a835636bebb1bdfc27a41"}, - {file = "fonttools-4.52.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7685fdc6e23267844eef2b9af585d7f171cca695e4eb369d7682544c3e2e1123"}, - {file = "fonttools-4.52.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1e1b2774485fbbb41a1beccc913b9c6f7971f78da61dd34207b9acc3cc2963e"}, - {file = "fonttools-4.52.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e2c415160397fd6ed3964155aeec4bfefceeee365ab17161a5b3fe3f8dab077"}, - {file = "fonttools-4.52.1-cp312-cp312-win32.whl", hash = "sha256:3ba2c4647e7decfb8e9cd346661c7d151dae1fba23d37b48bcf5fa8351f7b8c8"}, - {file = "fonttools-4.52.1-cp312-cp312-win_amd64.whl", hash = "sha256:d39b926f14a2f7a7f92ded7d266b18f0108d867364769ab59da88ac2fa90d288"}, - {file = "fonttools-4.52.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6e58d8097a269b6c43ec0abb3fa8d6c350ff0c7dfd23fc14d004610df88a4bb3"}, - {file = "fonttools-4.52.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:20f0fc969817c50539dc919ed8c4aef4de28c2d6e0111a064112301f157aede4"}, - {file = "fonttools-4.52.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d62e84d38969491c6c1f6fe3dd63108e99d02de01bb3d98c160a5d4d24120910"}, - {file = "fonttools-4.52.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb5a389bbdee6f4c422881de422ee0e7efdfcd9310b13d540b12aa8ae2c9e7b"}, - {file = "fonttools-4.52.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0caf05c969cbde6729dd97b64bea445ee152bb19215d5886f7b93bd0fb455468"}, - {file = "fonttools-4.52.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df08bee1dd29a767311b50c62c0cfe4d72ae8c793e567d4c60b8c16c7c63a4f0"}, - {file = "fonttools-4.52.1-cp38-cp38-win32.whl", hash = "sha256:82ffcf4782ceda09842b5b7875b36834c15d7cc0d5dd3d23a658ee9cf8819cd6"}, - {file = "fonttools-4.52.1-cp38-cp38-win_amd64.whl", hash = "sha256:26b43bab5a3bce55ed4d9699b16568795eef5597d154f52dcabef5b4804c4b21"}, - {file = "fonttools-4.52.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e8dbc13c4bc12e60df1b1f5e484112a5e96a6e8bba995e2965988ad73c5ea1b"}, - {file = "fonttools-4.52.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7352ba2226e45e8fba11c3fb416363faf1b06f3f2e80d07d2930401265f3bf9c"}, - {file = "fonttools-4.52.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8834d43763e9e92349ce8bb25dfb612aef6691eefefad885212d5e8f36a94a4"}, - {file = "fonttools-4.52.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2a8c1101d06cc8fca7851dceb67afd53dd6fc0288bacaa632e647bc5afff58"}, - {file = "fonttools-4.52.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a99b738227c0f6f2bbe381b45804a7c46653c95b9d7bf13f6f02884bc87e4930"}, - {file = "fonttools-4.52.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:75aa00a16b9a64d1550e2e70d3582c7fe1ef18560e0cf066a4087fe6d11908a2"}, - {file = "fonttools-4.52.1-cp39-cp39-win32.whl", hash = "sha256:c2f09b4aa699cfed4bbebc1829c5f044b41976707dac9230ed00d5a9fc6452c1"}, - {file = "fonttools-4.52.1-cp39-cp39-win_amd64.whl", hash = "sha256:78ea6e0d4c89f8e216995923b854dd10bd09e48d3a5a3ccb48bb68f436a409ad"}, - {file = "fonttools-4.52.1-py3-none-any.whl", hash = "sha256:faf5c83f83f7ddebdafdb453d02efdbea7fb494080d7a8d45a8a20db06ea8da5"}, - {file = "fonttools-4.52.1.tar.gz", hash = "sha256:8c9204435aa6e5e9479a5ba4e669f05dea28b0c61958e0c0923cb164296d9329"}, + {file = "fonttools-4.53.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20"}, + {file = "fonttools-4.53.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d"}, + {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6"}, + {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5"}, + {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9"}, + {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca"}, + {file = "fonttools-4.53.0-cp310-cp310-win32.whl", hash = "sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068"}, + {file = "fonttools-4.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68"}, + {file = "fonttools-4.53.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec"}, + {file = "fonttools-4.53.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749"}, + {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161"}, + {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee"}, + {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6"}, + {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e"}, + {file = "fonttools-4.53.0-cp311-cp311-win32.whl", hash = "sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005"}, + {file = "fonttools-4.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796"}, + {file = "fonttools-4.53.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a"}, + {file = "fonttools-4.53.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4"}, + {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06"}, + {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d"}, + {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109"}, + {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2"}, + {file = "fonttools-4.53.0-cp312-cp312-win32.whl", hash = "sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea"}, + {file = "fonttools-4.53.0-cp312-cp312-win_amd64.whl", hash = "sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380"}, + {file = "fonttools-4.53.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12"}, + {file = "fonttools-4.53.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4"}, + {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce"}, + {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f"}, + {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206"}, + {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd"}, + {file = "fonttools-4.53.0-cp38-cp38-win32.whl", hash = "sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af"}, + {file = "fonttools-4.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b"}, + {file = "fonttools-4.53.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac"}, + {file = "fonttools-4.53.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d"}, + {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1"}, + {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f"}, + {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64"}, + {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0"}, + {file = "fonttools-4.53.0-cp39-cp39-win32.whl", hash = "sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9"}, + {file = "fonttools-4.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2"}, + {file = "fonttools-4.53.0-py3-none-any.whl", hash = "sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4"}, + {file = "fonttools-4.53.0.tar.gz", hash = "sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002"}, ] [package.extras] @@ -1126,18 +1127,15 @@ 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" @@ -1361,13 +1359,13 @@ voice = ["PyNaCl (>=1.3.0,<1.6)"] [[package]] name = "pymarkdownlnt" -version = "0.9.19" +version = "0.9.20" 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.20-py3-none-any.whl", hash = "sha256:d9cd2db8e79dce67396236b7cdbaebe788bc8a07f95af9731433d353a91fe3d9"}, + {file = "pymarkdownlnt-0.9.20.tar.gz", hash = "sha256:75e3792e066c3ba53d71c2ef9b411cb3e429ad5d612ad02607f3c1ad120c5abc"}, ] [package.dependencies] @@ -1391,13 +1389,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] @@ -1482,6 +1480,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"}, @@ -1518,13 +1517,13 @@ files = [ [[package]] name = "requests" -version = "2.32.2" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, - {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -1539,28 +1538,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.5" +version = "0.4.8" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, - {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, - {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, - {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, - {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, - {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, + {file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"}, + {file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"}, + {file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"}, + {file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"}, + {file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"}, + {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"}, ] [[package]] @@ -1711,13 +1710,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.12.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.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, - {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, + {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]] From 9a18af9198e8ab78f9fc0e10bb6b8221b3d9fdb5 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 8 Jun 2024 14:02:47 +0100 Subject: [PATCH 033/128] Add initial changes for config setter command --- cogs/change_config.py | 74 ++++++++++++--- cogs/command_error.py | 6 +- config/__init__.py | 58 ++++-------- config/_settings/__init__.py | 92 ++++++++++++++++++- config/_settings/_yaml/__init__.py | 3 +- .../_yaml/custom_scalar_validators.py | 16 ++++ config/constants.py | 70 +++++++------- 7 files changed, 224 insertions(+), 95 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index 11787176b..eff2a628d 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -10,7 +10,7 @@ import discord import config -from config import ConfigSettingHelp +from config import ConfigSettingHelp, LogLevels from utils import ( CommandChecks, TeXBotApplicationContext, @@ -42,6 +42,20 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set in config.CONFIG_SETTINGS_HELPS } + @staticmethod + async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # noqa: E501 + """Autocomplete callable that generates example values for a configuration setting.""" + if not ctx.interaction.user or "setting" not in ctx.options: + return set() + + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + return set() + + if ":log-level" in ctx.options["setting"]: + return set(log_level.value for log_level in LogLevels) + + return {discord.OptionChoice("hi", "hi"), discord.OptionChoice("wow", "wow")} + @change_config.command( name="get", description="Display the current value of a configuration setting.", @@ -72,7 +86,7 @@ async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_n if isinstance(config_setting_value, str): config_setting_value = config_setting_value.strip() - CONFIG_SETTING_NEEDS_HIDING: Final[bool] = bool( + CONFIG_SETTING_IS_SECRET: Final[bool] = bool( "token" in config_setting_name or "cookie" in config_setting_name or "secret" in config_setting_name # noqa: COM812 @@ -82,17 +96,11 @@ async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ( f"`{config_setting_name.replace("`", "\\`")}` " f"{ - ( - f"**=** { - "||" if CONFIG_SETTING_NEEDS_HIDING else "" - }`{ - config_setting_value.replace("`", "\\`") - }`{ - "||" if CONFIG_SETTING_NEEDS_HIDING else "" - }" + "**cannot be viewed**." if CONFIG_SETTING_IS_SECRET else ( + f"**=** `{config_setting_value.replace("`", "\\`")}`" + if config_setting_value + else "**is not set**." ) - if config_setting_value - else "**is not set**." }" ), ephemeral=True, @@ -161,3 +169,45 @@ async def help_config_setting(self, ctx: TeXBotApplicationContext, config_settin ), ephemeral=True, ) + + @change_config.command( + name="set", + description="Assign a new value to the specified configuration setting.", + ) + @discord.option( # type: ignore[no-untyped-call, misc] + name="setting", + description="The name of the configuration setting to assign a new value to.", + input_type=str, + autocomplete=discord.utils.basic_autocomplete(autocomplete_get_settings_names), # type: ignore[arg-type] + required=True, + parameter_name="config_setting_name", + ) + @discord.option( # type: ignore[no-untyped-call, misc] + name="value", + description="The new value to assign to the specified configuration setting.", + input_type=str, + autocomplete=discord.utils.basic_autocomplete(autocomplete_get_example_setting_values), # type: ignore[arg-type] + required=True, + parameter_name="new_config_value", + ) + @CommandChecks.check_interaction_user_has_committee_role + @CommandChecks.check_interaction_user_in_main_guild + async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_name: str, new_config_value: str) -> None: # noqa: E501 + """Definition & callback response of the "set_config_value" command.""" + if config_setting_name not in config.CONFIG_SETTINGS_HELPS: + await self.command_send_error( + ctx, + f"Invalid setting: {config_setting_name!r}", + ) + return + + config.assign_single_config_setting_value(config_setting_name) + + await ctx.respond( + ( + f"Successfully updated setting: `{ + config_setting_name.replace("`", "\\`") + }`." + ), + ephemeral=True, + ) diff --git a/cogs/command_error.py b/cogs/command_error.py index 0ef47064c..1ec57f6bb 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -37,7 +37,11 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro if isinstance(error, discord.ApplicationCommandInvokeError): message = None logging_message = ( - None if isinstance(error.original, GuildDoesNotExistError) else error.original + None if isinstance(error.original, GuildDoesNotExistError) else ( + error.original + if str(error.original).strip() + else f"{error.original.__class__.__name__} was raised." + ) ) if isinstance(error.original, Forbidden): diff --git a/config/__init__.py b/config/__init__.py index a9a5bd763..b3d4acf29 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -16,23 +16,24 @@ "check_for_deprecated_environment_variables", "messages", "view_single_config_setting_value", + "assign_single_config_setting_value", "CONFIG_SETTINGS_HELPS", "ConfigSettingHelp", ) import contextlib +import functools import importlib import logging import os from collections.abc import Iterable from logging import Logger -from typing import Final - -from strictyaml import YAML +from typing import Final, Protocol from exceptions import BotRequiresRestartAfterConfigChange +from . import _settings from ._messages import MessagesAccessor from ._settings import SettingsAccessor from .constants import ( @@ -160,46 +161,19 @@ def check_for_deprecated_environment_variables() -> None: raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR -def _get_scalar_config_setting_value(config_setting_name: str, config_settings: YAML) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 - scalar_config_setting: YAML | None = config_settings.get(config_setting_name, None) # type: ignore[no-any-unimported] - - if scalar_config_setting is None: - return scalar_config_setting - - scalar_config_setting_value: object = scalar_config_setting.validator.to_yaml( - scalar_config_setting.data, - ) - - if isinstance(scalar_config_setting_value, str): - return scalar_config_setting_value - - if isinstance(scalar_config_setting_value, Iterable): - with contextlib.suppress(StopIteration): - if not isinstance(next(iter(scalar_config_setting_value)), str): - raise TypeError - - return ", ".join(scalar_config_setting_value) +class _SingleSettingValueViewerFunc(Protocol): + def __call__(self, config_setting_name: str) -> str | None: ... - raise NotImplementedError +class _SingleSettingAssignerFunc(Protocol): + def __call__(self, config_setting_name: str) -> None: ... -def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 - if ":" not in partial_config_setting_name: - return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) - key: str - remainder: str - key, _, remainder = partial_config_setting_name.partition(":") - - mapping_config_setting: YAML | None = config_settings.get(key, None) # type: ignore[no-any-unimported] - - if mapping_config_setting is not None and mapping_config_setting.is_mapping(): - return _get_mapping_config_setting_value(remainder, mapping_config_setting) - - return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) - - -def view_single_config_setting_value(config_setting_name: str) -> str | None: - """Return the value of a single configuration setting.""" - # noinspection PyProtectedMember - return _get_mapping_config_setting_value(config_setting_name, settings._most_recent_yaml) # noqa: SLF001 +view_single_config_setting_value: _SingleSettingValueViewerFunc = functools.partial( + _settings.view_single_config_setting_value, + settings_accessor=settings +) +assign_single_config_setting_value: _SingleSettingAssignerFunc = functools.partial( + _settings.assign_single_config_setting_value, + settings_accessor=settings +) diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index b220a18ea..1897879b4 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -7,17 +7,23 @@ from collections.abc import Sequence -__all__: Sequence[str] = ("SettingsAccessor",) +__all__: Sequence[str] = ( + "get_settings_file_path", + "SettingsAccessor", + "view_single_config_setting_value", + "assign_single_config_setting_value", +) import contextlib import logging import os import re +from collections.abc import Iterable from datetime import timedelta from logging import Logger from pathlib import Path -from typing import Any, ClassVar, Final, TextIO +from typing import Any, ClassVar, Final, Literal, TextIO, overload from discord_logging.handler import DiscordHandler from strictyaml import YAML @@ -35,7 +41,7 @@ logger: Final[Logger] = logging.getLogger("TeX-Bot") -def _get_settings_file_path() -> Path: +def get_settings_file_path() -> Path: settings_file_not_found_message: str = ( "No settings file was found. " "Please make sure you have created a `tex-bot-deployment.yaml` file." @@ -153,7 +159,7 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 @classmethod def reload(cls) -> None: - settings_file_path: Path = _get_settings_file_path() + settings_file_path: Path = get_settings_file_path() current_yaml: YAML = load_yaml( # type: ignore[no-any-unimported] # TODO: better error messages when loading yaml settings_file_path.read_text(), file_name=settings_file_path.name, @@ -897,3 +903,81 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte ) return {"reminders:send-get-roles-reminders:interval"} + + +def _get_scalar_config_setting_value(config_setting_name: str, config_settings: YAML) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 + scalar_config_setting: YAML | None = config_settings.get(config_setting_name, None) # type: ignore[no-any-unimported] + + if scalar_config_setting is None: + return scalar_config_setting + + scalar_config_setting_value: object = scalar_config_setting.validator.to_yaml( + scalar_config_setting.data, + ) + + if isinstance(scalar_config_setting_value, str): + return scalar_config_setting_value + + if isinstance(scalar_config_setting_value, Iterable): + with contextlib.suppress(StopIteration): + if not isinstance(next(iter(scalar_config_setting_value)), str): + raise TypeError + + return ", ".join(scalar_config_setting_value) + + raise NotImplementedError + + +@overload +def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML, *, use_setter: Literal[True]) -> None: ... # type: ignore[no-any-unimported,misc] # noqa: E501 + + +@overload +def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML, *, use_setter: Literal[False]) -> str | None: ... # type: ignore[no-any-unimported,misc] # noqa: E501 + + +@overload +def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML, *, use_setter: bool) -> str | None: ... # type: ignore[no-any-unimported,misc] # noqa: E501 + + +def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML, *, use_setter: bool = False) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 + if ":" not in partial_config_setting_name: + if use_setter: + raise NotImplementedError + + return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) + + key: str + remainder: str + key, _, remainder = partial_config_setting_name.partition(":") + + mapping_config_setting: YAML | None = config_settings.get(key, None) # type: ignore[no-any-unimported] + + if mapping_config_setting is not None and mapping_config_setting.is_mapping(): + return _get_mapping_config_setting_value( + remainder, + mapping_config_setting, + use_setter=use_setter, + ) + + return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) + + +def view_single_config_setting_value(config_setting_name: str, settings_accessor: SettingsAccessor) -> str | None: # noqa: E501 + """Return the value of a single configuration setting from the setting tree hierarchy.""" + # noinspection PyProtectedMember + return _get_mapping_config_setting_value( # noqa: SLF001 + config_setting_name, + settings_accessor._most_recent_yaml, # noqa: SLF001 + use_setter=False + ) + + +def assign_single_config_setting_value(config_setting_name: str, settings_accessor: SettingsAccessor) -> None: # noqa: E501 + """Set the value of a single configuration setting within the setting tree hierarchy.""" + # noinspection PyProtectedMember + _get_mapping_config_setting_value( # noqa: SLF001 + config_setting_name, + settings_accessor._most_recent_yaml, # noqa: SLF001 + use_setter=True, + ) diff --git a/config/_settings/_yaml/__init__.py b/config/_settings/_yaml/__init__.py index 20aaff5b1..bfaac581e 100644 --- a/config/_settings/_yaml/__init__.py +++ b/config/_settings/_yaml/__init__.py @@ -47,6 +47,7 @@ RegexMatcher, SendIntroductionRemindersFlagValidator, TimeDeltaValidator, + CustomBoolValidator, ) _DEFAULT_CONSOLE_LOGGING_SETTINGS: Final[Mapping[str, LogLevels]] = { @@ -204,7 +205,7 @@ ), strictyaml.Optional("send-get-roles-reminders", default=_DEFAULT_SEND_GET_ROLES_REMINDERS_SETTINGS): SlugKeyMap( # noqa: E501 { - "enabled": strictyaml.Bool(), + "enabled": CustomBoolValidator(), strictyaml.Optional("delay", default=DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY): ( # noqa: E501 TimeDeltaValidator(minutes=True, hours=True, days=True, weeks=True) ), diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index a5944f90d..e75667be1 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -8,6 +8,7 @@ "ProbabilityValidator", "TimeDeltaValidator", "SendIntroductionRemindersFlagValidator", + "CustomBoolValidator", ) @@ -329,3 +330,18 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return "false" return str(data).lower() + + +class CustomBoolValidator(strictyaml.Bool): # type: ignore[no-any-unimported,misc] + @override + def to_yaml(self, data: object) -> str: # type: ignore[misc] + if isinstance(data, bool): + return "true" if data else "false" + + if str(data).lower() in strictyaml_constants.TRUE_VALUES: + return "true" + + if str(data).lower() in strictyaml_constants.FALSE_VALUES: + return "false" + + raise YAMLSerializationError("Not a boolean") diff --git a/config/constants.py b/config/constants.py index 26db25503..8f8420d46 100644 --- a/config/constants.py +++ b/config/constants.py @@ -67,20 +67,20 @@ class ConfigSettingHelp(NamedTuple): required: bool = True default: str | None = None - @classmethod - def _selectable_required_format_message(cls, options: Iterable[str]) -> str: - return f"Must be one of: `{"`, `".join(options)}`." - @classmethod - def _custom_required_format_message(cls, type_value: str, info_link: str | None = None) -> str: # noqa: E501 - return ( - f"Must be a valid { - type_value.lower().replace("discord", "Discord").replace( - "id", - "ID", - ).replace("url", "URL").strip(".") - }{f" (see <{info_link}>)" if info_link else ""}." - ) +def _selectable_required_format_message(options: Iterable[str]) -> str: + return f"Must be one of: `{"`, `".join(options)}`." + + +def _custom_required_format_message(type_value: str, info_link: str | None = None) -> str: + return ( + f"Must be a valid { + type_value.lower().replace("discord", "Discord").replace( + "id", + "ID", + ).replace("url", "URL").strip(".") + }{f" (see <{info_link}>)" if info_link else ""}." + ) PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.resolve() @@ -150,7 +150,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "The minimum level that logs must meet in order to be logged " "to the console output stream." ), - value_type_message=ConfigSettingHelp._selectable_required_format_message(LogLevels), # noqa: SLF001 + value_type_message=_selectable_required_format_message(LogLevels), required=False, default=DEFAULT_CONSOLE_LOG_LEVEL, ), @@ -159,7 +159,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "The minimum level that logs must meet in order to be logged " "to the Discord log channel." ), - value_type_message=ConfigSettingHelp._selectable_required_format_message(LogLevels), # noqa: SLF001 + value_type_message=_selectable_required_format_message(LogLevels), required=False, default=DEFAULT_DISCORD_LOGGING_LOG_LEVEL, ), @@ -169,7 +169,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "Error logs will always be sent to the console, " "this setting allows them to also be sent to a Discord log channel." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( "Discord webhook URL", "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks", ), @@ -181,7 +181,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "The Discord token for the bot you created " "(available on your bot page in the developer portal: )." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( "Discord bot token", "https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts", ), @@ -190,7 +190,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None ), "discord:main-guild-id": ConfigSettingHelp( description="The ID of your community group's main Discord guild.", - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( "Discord guild ID", "https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id", ), @@ -225,7 +225,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "The link to the page where guests can purchase a full membership " "to join your community group." ), - value_type_message=ConfigSettingHelp._custom_required_format_message("URL"), # noqa: SLF001 + value_type_message=_custom_required_format_message("URL"), required=False, default=None, ), @@ -235,13 +235,13 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "about the perks that they will receive " "once they purchase a membership to your community group." ), - value_type_message=ConfigSettingHelp._custom_required_format_message("URL"), # noqa: SLF001 + value_type_message=_custom_required_format_message("URL"), required=False, default=None, ), "community-group:links:moderation-document": ConfigSettingHelp( description="The link to your group's Discord guild moderation document.", - value_type_message=ConfigSettingHelp._custom_required_format_message("URL"), # noqa: SLF001 + value_type_message=_custom_required_format_message("URL"), required=True, default=None, ), @@ -254,7 +254,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "if your members-list is found on the UoB Guild of Students website, " "ensure the URL includes the \"sort by groups\" option)." ), - value_type_message=ConfigSettingHelp._custom_required_format_message("URL"), # noqa: SLF001 + value_type_message=_custom_required_format_message("URL"), required=True, default=None, ), @@ -276,7 +276,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None ), "community-group:members-list:id-format": ConfigSettingHelp( description="The format that IDs are stored in within your members-list.", - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( "regex matcher string", ), required=False, @@ -287,7 +287,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "The probability that the more rare ping command response will be sent " "instead of the normal one." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( "float, inclusively between 1 & 0", ), required=False, @@ -297,7 +297,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None description=( "The number of days to look over messages sent, to generate statistics data." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( "float representing the number of days to look back through", ), required=False, @@ -308,7 +308,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "The names of the roles to gather statistics about, " "to display in bar chart graphs." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( "comma seperated list of strings of role names", ), required=False, @@ -318,7 +318,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None description=( "The amount of time to timeout a user when using the **`/strike`** command." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( ( "string of the seconds, minutes, hours, days or weeks " "to timeout a user (format: `smhdw`)" @@ -336,7 +336,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "(so the offending person *will* be able to see these messages " "if a public channel is chosen)." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( ( "name of a Discord channel in your group's Discord guild, " "or the value `DM` " @@ -351,7 +351,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None description=( "The locale code used to select the language response messages will be given in." ), - value_type_message=ConfigSettingHelp._selectable_required_format_message( # noqa: SLF001 + value_type_message=_selectable_required_format_message( MESSAGES_LOCALE_CODES, ), required=False, @@ -363,7 +363,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "that are not inducted, " "saying that they need to send an introduction to be allowed access." ), - value_type_message=ConfigSettingHelp._selectable_required_format_message( # noqa: SLF001 + value_type_message=_selectable_required_format_message( ( str(flag_value).lower() for flag_value @@ -381,7 +381,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "Is ignored if `reminders:send-introduction-reminders:enabled` **=** `false`.\n" "The delay must be longer than or equal to 1 day (in any allowed format)." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( ( "string of the seconds, minutes, hours, days or weeks " "before the first/only reminder is sent " @@ -398,7 +398,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "saying that they need to send an introduction to be allowed access.\n" "Is ignored if `reminders:send-introduction-reminders:enabled` **=** `false`." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( ( "string of the seconds, minutes, or hours between reminders " "(format: `smh`)" @@ -413,7 +413,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "saying that they can get opt-in roles. " "(This message will be only sent once per Discord member)." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( "boolean value (either `true` or `false`)", ), required=False, @@ -426,7 +426,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "Is ignored if `reminders:send-get-roles-reminders:enabled` **=** `false`.\n" "The delay must be longer than or equal to 1 day (in any allowed format)." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( ( "string of the seconds, minutes, hours, days or weeks " "before the first/only reminder is sent " @@ -445,7 +445,7 @@ def _custom_required_format_message(cls, type_value: str, info_link: str | None "the interval is just how often to check for new guests).\n" "Is ignored if `reminders:send-get-roles-reminders:enabled` **=** `false`." ), - value_type_message=ConfigSettingHelp._custom_required_format_message( # noqa: SLF001 + value_type_message=_custom_required_format_message( ( "string of the seconds, minutes, or hours between reminders " "(format: `smh`)" From c0b23af6ed8780ed86422237b4746e5f1be96fc8 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 9 Jun 2024 05:12:40 +0100 Subject: [PATCH 034/128] Fix bug in assigning new values to config settings --- cogs/change_config.py | 86 ++++- cogs/command_error.py | 17 +- cogs/edit_message.py | 4 +- cogs/ping.py | 2 +- cogs/startup.py | 6 +- cogs/stats.py | 7 +- config/__init__.py | 12 +- config/_messages/__init__.py | 2 +- config/_settings/__init__.py | 296 +++++++++++++----- config/_settings/_yaml/__init__.py | 8 +- .../_settings/_yaml/custom_map_validator.py | 8 +- .../_yaml/custom_scalar_validators.py | 88 +++--- config/constants.py | 53 +++- exceptions/__init__.py | 3 + exceptions/base.py | 5 +- exceptions/config_changes.py | 42 ++- exceptions/does_not_exist.py | 95 +++--- exceptions/guild.py | 12 +- exceptions/strike.py | 10 +- pyproject.toml | 2 +- utils/__init__.py | 2 +- utils/tex_bot.py | 2 +- utils/tex_bot_contexts.py | 2 +- 23 files changed, 539 insertions(+), 225 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index eff2a628d..393a97045 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -8,9 +8,11 @@ from typing import Final import discord +from strictyaml import StrictYAMLError import config -from config import ConfigSettingHelp, LogLevels +from config import CONFIG_SETTINGS_HELPS, ConfigSettingHelp, LogLevels +from exceptions import ChangingSettingWithRequiredSiblingError from utils import ( CommandChecks, TeXBotApplicationContext, @@ -45,16 +47,19 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set @staticmethod async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # noqa: E501 """Autocomplete callable that generates example values for a configuration setting.""" - if not ctx.interaction.user or "setting" not in ctx.options: + HAS_CONTEXT: Final[bool] = bool( + ctx.interaction.user and "setting" in ctx.options and ctx.options["setting"], + ) + if not HAS_CONTEXT: return set() - if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): # type: ignore[arg-type] return set() if ":log-level" in ctx.options["setting"]: - return set(log_level.value for log_level in LogLevels) + return {log_level.value for log_level in LogLevels} - return {discord.OptionChoice("hi", "hi"), discord.OptionChoice("wow", "wow")} + return set() # TODO: extra autocomplete suggestions @change_config.command( name="get", @@ -99,7 +104,16 @@ async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_n "**cannot be viewed**." if CONFIG_SETTING_IS_SECRET else ( f"**=** `{config_setting_value.replace("`", "\\`")}`" if config_setting_value - else "**is not set**." + else f"**is not set**.{ + f"\nThe default value is `{ + CONFIG_SETTINGS_HELPS[config_setting_name].default.replace( # type: ignore[union-attr] + "`", + "\\`", + ) + }`" + if CONFIG_SETTINGS_HELPS[config_setting_name].default is not None + else "" + }" ) }" ), @@ -201,13 +215,69 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ) return - config.assign_single_config_setting_value(config_setting_name) + # TODO: Are you sure, if config has no default + + yaml_error: StrictYAMLError + changing_setting_error: ChangingSettingWithRequiredSiblingError + try: + config.assign_single_config_setting_value(config_setting_name, new_config_value) + except StrictYAMLError as yaml_error: + if str(yaml_error) != yaml_error.context: + INCONCLUSIVE_YAML_ERROR_MESSAGE: Final[str] = ( + "Could not determine the error message from invalid YAML validation." + ) + raise NotImplementedError(INCONCLUSIVE_YAML_ERROR_MESSAGE) from None + + await self.command_send_error( + ctx, + message=( + f"Changing setting value failed: " + f"{str(yaml_error.context)[0].upper()}" + f"{str(yaml_error.context)[1:].strip(" .")}." + ), + ) + return + except ChangingSettingWithRequiredSiblingError as changing_setting_error: + await self.command_send_error( + ctx, + message=( + f"{changing_setting_error} " + f"It will be easier to make your changes " + f"directly within the \"tex-bot-deployment.yaml\" file." + ), + ) + return + + changed_config_setting_value: str | None = config.view_single_config_setting_value( + config_setting_name, + ) + + if isinstance(changed_config_setting_value, str): + changed_config_setting_value = changed_config_setting_value.strip() + + CONFIG_SETTING_IS_SECRET: Final[bool] = bool( + "token" in config_setting_name + or "cookie" in config_setting_name + or "secret" in config_setting_name # noqa: COM812 + ) await ctx.respond( ( f"Successfully updated setting: `{ config_setting_name.replace("`", "\\`") - }`." + }`" + f"{ + "" if CONFIG_SETTING_IS_SECRET else ( + ( + f"**=** `{ + changed_config_setting_value.replace("`", "\\`") + }`" + ) + if changed_config_setting_value + else "**to be not set**." + ) + }\n\n" + "Changes could take up to ??? to take effect." # TODO: Retrieve update time from task ), ephemeral=True, ) diff --git a/cogs/command_error.py b/cogs/command_error.py index 1ec57f6bb..c7526a204 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -37,10 +37,19 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro if isinstance(error, discord.ApplicationCommandInvokeError): message = None logging_message = ( - None if isinstance(error.original, GuildDoesNotExistError) else ( - error.original - if str(error.original).strip() - else f"{error.original.__class__.__name__} was raised." + None + if isinstance(error.original, GuildDoesNotExistError) + else ( + f"{error.original.__class__.__name__} was raised." + if not str(error.original).strip(". -\"'") + else ( + f"{error.original.__class__.__name__}: {error.original}" + if ( + str(error.original).startswith("\"") + or str(error.original).startswith("'") + ) + else error.original + ) ) ) diff --git a/cogs/edit_message.py b/cogs/edit_message.py index fb230b4bc..8bddf88b0 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -113,7 +113,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 +122,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/ping.py b/cogs/ping.py index f3f3d13aa..dc3fbd787 100644 --- a/cogs/ping.py +++ b/cogs/ping.py @@ -26,7 +26,7 @@ async def ping(self, ctx: TeXBotApplicationContext) -> None: "`64 bytes from TeX-Bot: icmp_seq=1 ttl=63 time=0.01 ms`", ], weights=( - 100 - settings["PING_COMMAND_EASTER_EGG_PROBABILITY"], + 1 - settings["PING_COMMAND_EASTER_EGG_PROBABILITY"], settings["PING_COMMAND_EASTER_EGG_PROBABILITY"], ), )[0], diff --git a/cogs/startup.py b/cogs/startup.py index 6de8c7164..c737375f2 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -38,7 +38,7 @@ def _setup_discord_log_channel(self) -> None: "so error logs will not be sent to the Discord log-channel." ) - discord_logging_handlers: set[DiscordHandler] = { # type: ignore[no-any-unimported] + discord_logging_handlers: set[DiscordHandler] = { handler for handler in logger.handlers if isinstance(handler, DiscordHandler) } @@ -49,12 +49,12 @@ def _setup_discord_log_channel(self) -> None: raise ValueError(CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE) if len(discord_logging_handlers) == 1: - existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() # type: ignore[no-any-unimported] + existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() logger.removeHandler(existing_discord_logging_handler) if settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"]: - new_discord_logging_handler: DiscordHandler = DiscordHandler( # type: ignore[no-any-unimported] + new_discord_logging_handler: DiscordHandler = DiscordHandler( ( existing_discord_logging_handler.name if existing_discord_logging_handler.name != DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME # noqa: E501 diff --git a/cogs/stats.py b/cogs/stats.py index 761b88e15..b85b1ae11 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 @@ -220,7 +221,7 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str if discord.utils.get(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 @@ -326,7 +327,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 @@ -463,7 +464,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 diff --git a/config/__init__.py b/config/__init__.py index b3d4acf29..c45f17c61 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -22,7 +22,6 @@ ) -import contextlib import functools import importlib import logging @@ -31,8 +30,6 @@ from logging import Logger from typing import Final, Protocol -from exceptions import BotRequiresRestartAfterConfigChange - from . import _settings from ._messages import MessagesAccessor from ._settings import SettingsAccessor @@ -54,8 +51,7 @@ def run_setup() -> None: """Execute the setup functions required, before other modules can be run.""" check_for_deprecated_environment_variables() - with contextlib.suppress(BotRequiresRestartAfterConfigChange): - settings.reload() + settings.reload() messages.load(settings["MESSAGES_LOCALE_CODE"]) @@ -166,14 +162,14 @@ def __call__(self, config_setting_name: str) -> str | None: ... class _SingleSettingAssignerFunc(Protocol): - def __call__(self, config_setting_name: str) -> None: ... + def __call__(self, config_setting_name: str, new_config_setting_value: str) -> None: ... view_single_config_setting_value: _SingleSettingValueViewerFunc = functools.partial( _settings.view_single_config_setting_value, - settings_accessor=settings + settings_accessor=settings, ) assign_single_config_setting_value: _SingleSettingAssignerFunc = functools.partial( _settings.assign_single_config_setting_value, - settings_accessor=settings + settings_accessor=settings, ) diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index 3e68d1793..332d69ac3 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -65,7 +65,7 @@ def load(cls, messages_locale_code: str) -> None: if messages_locale_code not in MESSAGES_LOCALE_CODES: INVALID_MESSAGES_LOCALE_CODE_MESSAGE: Final[str] = ( f"{"messages_locale_code"!r} must be one of " - f"\"{"\", \"".join(MESSAGES_LOCALE_CODES)}\"" + f"'{"', '".join(MESSAGES_LOCALE_CODES)}'" ) raise ValueError(INVALID_MESSAGES_LOCALE_CODE_MESSAGE) diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 1897879b4..87003139d 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -19,25 +19,30 @@ import logging import os import re -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from datetime import timedelta from logging import Logger from pathlib import Path -from typing import Any, ClassVar, Final, Literal, TextIO, overload +from typing import Any, ClassVar, Final, TextIO, TypeAlias +import strictyaml from discord_logging.handler import DiscordHandler from strictyaml import YAML from config.constants import ( DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME, PROJECT_ROOT, - REQUIRES_RESTART_SETTINGS, ) -from exceptions import BotRequiresRestartAfterConfigChange +from exceptions import ( + BotRequiresRestartAfterConfigChange, + ChangingSettingWithRequiredSiblingError, +) from . import utils from ._yaml import load_yaml +NestedMapping: TypeAlias = Mapping[str, "NestedMapping | str"] + logger: Final[Logger] = logging.getLogger("TeX-Bot") @@ -97,7 +102,7 @@ class SettingsAccessor: """ _settings: ClassVar[dict[str, object]] = {} - _most_recent_yaml: ClassVar[YAML | None] = None # type: ignore[no-any-unimported] + _most_recent_yaml: ClassVar[YAML | None] = None @classmethod def format_invalid_settings_key_message(cls, item: str) -> str: @@ -160,7 +165,7 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 @classmethod def reload(cls) -> None: settings_file_path: Path = get_settings_file_path() - current_yaml: YAML = load_yaml( # type: ignore[no-any-unimported] # TODO: better error messages when loading yaml + current_yaml: YAML = load_yaml( # TODO: better error messages when loading yaml settings_file_path.read_text(), file_name=settings_file_path.name, ) @@ -239,11 +244,8 @@ def reload(cls) -> None: cls._most_recent_yaml = current_yaml - if changed_settings_keys & REQUIRES_RESTART_SETTINGS: - raise BotRequiresRestartAfterConfigChange(changed_settings=changed_settings_keys) - @classmethod - def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # type: ignore[misc] """ Reload the console logging configuration with the new given log level. @@ -295,7 +297,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # return {"logging:console:log-level"} @classmethod - def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: YAML | None) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the Discord log channel logging configuration. @@ -318,7 +320,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y else discord_channel_logging_settings["webhook_url"].data ) - discord_logging_handlers: set[DiscordHandler] = { # type: ignore[no-any-unimported] + discord_logging_handlers: set[DiscordHandler] = { handler for handler in logger.handlers if isinstance(handler, DiscordHandler) } if len(discord_logging_handlers) > 1: @@ -333,7 +335,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y discord_logging_handler_avatar_url: str | None = None if len(discord_logging_handlers) == 1: - existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() # type: ignore[no-any-unimported] + existing_discord_logging_handler: DiscordHandler = discord_logging_handlers.pop() ONLY_DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = bool( discord_channel_logging_settings is not None @@ -406,7 +408,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y return changed_settings @classmethod - def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: # type: ignore[misc] """ Reload the Discord bot-token. @@ -425,7 +427,7 @@ def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: # type return {"discord:bot-token"} @classmethod - def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: # type: ignore[misc] """ Reload the Discord main-guild ID. @@ -444,7 +446,7 @@ def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: return {"discord:main-guild-id"} @classmethod - def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # type: ignore[misc] """ Reload the community-group full name. @@ -470,7 +472,7 @@ def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # t return {"community-group:full-name"} @classmethod - def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # type: ignore[misc] """ Reload the community-group short name. @@ -496,7 +498,7 @@ def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # return {"community-group:short-name"} @classmethod - def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the link to allow people to purchase a membership. @@ -521,7 +523,7 @@ def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) return {"community-group:links:purchase-membership"} @classmethod - def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> set[str]: # type: ignore[misc] """ Reload the link to view the perks of getting a membership to join your community group. @@ -546,7 +548,7 @@ def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> se return {"community-group:links:membership-perks"} @classmethod - def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set[str]: # type: ignore[misc] """ Reload the link to view your community group's moderation document. @@ -567,7 +569,7 @@ def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set return {"community-group:links:moderation-document"} @classmethod - def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: ignore[misc] """ Reload the url that points to the location of your community group's members-list. @@ -588,7 +590,7 @@ def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: return {"community-group:members-list:url"} @classmethod - def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cookie: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cookie: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the auth session cookie used to authenticate to access your members-list. @@ -611,7 +613,7 @@ def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cook return {"community-group:members-list:auth-session-cookie"} @classmethod - def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str]: # type: ignore[misc] """ Reload the format regex matcher for IDs in your community group's members-list. @@ -632,7 +634,7 @@ def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str return {"community-group:members-list:id-format"} @classmethod - def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_probability: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_probability: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the probability that the rarer response will show when using the ping command. @@ -655,7 +657,7 @@ def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_pro return {"commands:ping:easter-egg-probability"} @classmethod - def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the number of days to lookback for statistics. @@ -678,7 +680,7 @@ def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) return {"commands:stats:lookback-days"} @classmethod - def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the set of roles used to display statistics about. @@ -699,7 +701,7 @@ def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YA return {"commands:stats:displayed-roles"} @classmethod - def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the duration to use when applying a timeout action for a strike increase. @@ -720,7 +722,7 @@ def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration return {"commands:strike:timeout-duration"} @classmethod - def _reload_strike_performed_manually_warning_location(cls, strike_performed_manually_warning_location: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_strike_performed_manually_warning_location(cls, strike_performed_manually_warning_location: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the location to send warning messages when strikes are performed manually. @@ -743,7 +745,7 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man return {"commands:strike:performed-manually-warning-location"} @classmethod - def _reload_messages_locale_code(cls, messages_locale_code: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] + def _reload_messages_locale_code(cls, messages_locale_code: YAML) -> set[str]: # type: ignore[misc] """ Reload the selected locale for messages to be sent in. @@ -762,7 +764,7 @@ def _reload_messages_locale_code(cls, messages_locale_code: YAML) -> set[str]: return {"messages-locale-code"} @classmethod - def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders_enabled: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders_enabled: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the flag for whether the "send-introduction-reminders" task is enabled. @@ -785,7 +787,7 @@ def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders return {"reminders:send-introduction-reminders:enabled"} @classmethod - def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_delay: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_delay: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the amount of time to wait before sending introduction-reminders to a user. @@ -810,7 +812,7 @@ def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_d return {"reminders:send-introduction-reminders:delay"} @classmethod - def _reload_send_introduction_reminders_interval(cls, send_introduction_reminders_interval: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_send_introduction_reminders_interval(cls, send_introduction_reminders_interval: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the interval of time between executing the task to send introduction-reminders. @@ -833,7 +835,7 @@ def _reload_send_introduction_reminders_interval(cls, send_introduction_reminder return {"reminders:send-introduction-reminders:interval"} @classmethod - def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabled: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabled: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the flag for whether the "send-get-roles-reminders" task is enabled. @@ -856,7 +858,7 @@ def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabl return {"reminders:send-get-roles-reminders:enabled"} @classmethod - def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the amount of time to wait before sending get-roles-reminders to a user. @@ -882,7 +884,7 @@ def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: return {"reminders:send-get-roles-reminders:delay"} @classmethod - def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_interval: YAML) -> set[str]: # type: ignore[no-any-unimported,misc] # noqa: E501 + def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_interval: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 """ Reload the interval of time between executing the task to send get-roles-reminders. @@ -905,79 +907,223 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte return {"reminders:send-get-roles-reminders:interval"} -def _get_scalar_config_setting_value(config_setting_name: str, config_settings: YAML) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 - scalar_config_setting: YAML | None = config_settings.get(config_setting_name, None) # type: ignore[no-any-unimported] +def _get_scalar_config_setting_value(config_setting_name: str, yaml_settings_tree: YAML) -> str | None: # noqa: E501 + single_yaml_scalar_setting: YAML | None = yaml_settings_tree.get(config_setting_name, None) - if scalar_config_setting is None: - return scalar_config_setting + if single_yaml_scalar_setting is None: + return single_yaml_scalar_setting - scalar_config_setting_value: object = scalar_config_setting.validator.to_yaml( - scalar_config_setting.data, + CONFIG_SETTING_HAS_VALID_TYPE: Final[bool] = bool( + not single_yaml_scalar_setting.is_mapping() + and ( + single_yaml_scalar_setting.is_scalar() + or single_yaml_scalar_setting.is_sequence() + ) # noqa: COM812 + ) + if not CONFIG_SETTING_HAS_VALID_TYPE: + MAPPING_TYPE_MESSAGE: Final[str] = "Got config mapping when scalar expected." + raise RuntimeError(MAPPING_TYPE_MESSAGE) + + scalar_config_setting_value: object = single_yaml_scalar_setting.validator.to_yaml( + single_yaml_scalar_setting.data, ) if isinstance(scalar_config_setting_value, str): + if not single_yaml_scalar_setting.is_scalar(): + SCALAR_TYPE_MESSAGE: Final[str] = "Got invalid config type when scalar expected." + raise RuntimeError(SCALAR_TYPE_MESSAGE) + return scalar_config_setting_value if isinstance(scalar_config_setting_value, Iterable): - with contextlib.suppress(StopIteration): - if not isinstance(next(iter(scalar_config_setting_value)), str): - raise TypeError + if not single_yaml_scalar_setting.is_sequence(): + SEQUENCE_TYPE_MESSAGE: Final[str] = ( + "Got invalid config type when sequence expected." + ) + raise RuntimeError(SEQUENCE_TYPE_MESSAGE) + + if not all(inner_value.is_scalar() for inner_value in single_yaml_scalar_setting): + ONLY_SCALAR_SEQUENCES_SUPPORTED_MESSAGE: Final[str] = ( + "Only sequences of scalars are currently supported " + "to be used in configuration." + ) + raise NotImplementedError(ONLY_SCALAR_SEQUENCES_SUPPORTED_MESSAGE) - return ", ".join(scalar_config_setting_value) + return ",".join(scalar_config_setting_value) raise NotImplementedError -@overload -def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML, *, use_setter: Literal[True]) -> None: ... # type: ignore[no-any-unimported,misc] # noqa: E501 +def _get_mapping_config_setting_value(partial_config_setting_name: str, partial_yaml_settings_tree: YAML) -> str | None: # noqa: E501 + if ":" not in partial_config_setting_name: + return _get_scalar_config_setting_value( + partial_config_setting_name, + partial_yaml_settings_tree, + ) + + key: str + remainder: str + key, _, remainder = partial_config_setting_name.partition(":") + + single_yaml_mapping_setting: YAML | None = partial_yaml_settings_tree.get(key, None) + if single_yaml_mapping_setting is not None and single_yaml_mapping_setting.is_mapping(): + return _get_mapping_config_setting_value(remainder, single_yaml_mapping_setting) -@overload -def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML, *, use_setter: Literal[False]) -> str | None: ... # type: ignore[no-any-unimported,misc] # noqa: E501 + return _get_scalar_config_setting_value( + partial_config_setting_name, + partial_yaml_settings_tree, + ) -@overload -def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML, *, use_setter: bool) -> str | None: ... # type: ignore[no-any-unimported,misc] # noqa: E501 +def view_single_config_setting_value(config_setting_name: str, settings_accessor: SettingsAccessor) -> str | None: # noqa: E501 + """Return the value of a single configuration setting from the setting tree hierarchy.""" + # noinspection PyProtectedMember + current_yaml: YAML | None = settings_accessor._most_recent_yaml # noqa: SLF001 + if current_yaml is None: + YAML_NOT_LOADED_MESSAGE: Final[str] = ( + "Invalid state: Config YAML has not yet been loaded." + ) + raise RuntimeError(YAML_NOT_LOADED_MESSAGE) + # noinspection PyProtectedMember + return _get_mapping_config_setting_value(config_setting_name, current_yaml) -def _get_mapping_config_setting_value(partial_config_setting_name: str, config_settings: YAML, *, use_setter: bool = False) -> str | None: # type: ignore[no-any-unimported] # noqa: E501 - if ":" not in partial_config_setting_name: - if use_setter: - raise NotImplementedError - return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) +def _set_scalar_or_sequence_config_setting_value(config_setting_name: str, new_config_setting_value: str, yaml_settings_tree: YAML) -> YAML: # noqa: E501 + if config_setting_name not in yaml_settings_tree: + yaml_settings_tree[config_setting_name] = new_config_setting_value + return yaml_settings_tree + + if yaml_settings_tree[config_setting_name].is_mapping(): + INVALID_MAPPING_CONFIG_TYPE_MESSAGE: Final[str] = ( + "Got incongruent YAML object. Expected sequence or scalar, got mapping." + ) + raise TypeError(INVALID_MAPPING_CONFIG_TYPE_MESSAGE) + + if yaml_settings_tree[config_setting_name].is_scalar(): + yaml_settings_tree[config_setting_name] = new_config_setting_value + return yaml_settings_tree + + if yaml_settings_tree[config_setting_name].is_sequence(): + yaml_settings_tree[config_setting_name] = [ + sequence_value.strip() + for sequence_value + in new_config_setting_value.strip().split(",") + ] + return yaml_settings_tree + + UNKNOWN_CONFIG_TYPE_MESSAGE: Final[str] = ( + "Unknown YAML object type. Expected sequence or scalar." + ) + raise RuntimeError(UNKNOWN_CONFIG_TYPE_MESSAGE) + + +def _set_mapping_config_setting_value(partial_config_setting_name: str, new_config_setting_value: str, partial_yaml_settings_tree: YAML) -> YAML: # noqa: E501 + if ":" not in partial_config_setting_name: + return _set_scalar_or_sequence_config_setting_value( + partial_config_setting_name, + new_config_setting_value, + partial_yaml_settings_tree, + ) key: str remainder: str key, _, remainder = partial_config_setting_name.partition(":") - mapping_config_setting: YAML | None = config_settings.get(key, None) # type: ignore[no-any-unimported] + if key not in partial_yaml_settings_tree: + partial_yaml_settings_tree[key] = _set_required_value_from_validator_config( + remainder if ":" in partial_config_setting_name else None, + new_config_setting_value, + partial_yaml_settings_tree.validator.get_validator(key), + ) + return partial_yaml_settings_tree - if mapping_config_setting is not None and mapping_config_setting.is_mapping(): - return _get_mapping_config_setting_value( + if partial_yaml_settings_tree[key].is_mapping(): + partial_yaml_settings_tree[key] = _set_mapping_config_setting_value( remainder, - mapping_config_setting, - use_setter=use_setter, + new_config_setting_value, + partial_yaml_settings_tree[key], ) + return partial_yaml_settings_tree - return _get_scalar_config_setting_value(partial_config_setting_name, config_settings) + return _set_scalar_or_sequence_config_setting_value( + partial_config_setting_name, + new_config_setting_value, + partial_yaml_settings_tree, + ) -def view_single_config_setting_value(config_setting_name: str, settings_accessor: SettingsAccessor) -> str | None: # noqa: E501 - """Return the value of a single configuration setting from the setting tree hierarchy.""" - # noinspection PyProtectedMember - return _get_mapping_config_setting_value( # noqa: SLF001 - config_setting_name, - settings_accessor._most_recent_yaml, # noqa: SLF001 - use_setter=False +def _set_required_value_from_validator_config(partial_config_setting_name: str | None, new_config_setting_value: str, yaml_validator: strictyaml.Validator) -> "NestedMapping | str | Sequence[str]": # noqa: E501 + VALIDATOR_IS_SCALAR_TYPE: Final[bool] = bool( + isinstance(yaml_validator, strictyaml.ScalarValidator) + and (partial_config_setting_name is None or ":" not in partial_config_setting_name) # noqa: COM812 ) + if VALIDATOR_IS_SCALAR_TYPE: + return new_config_setting_value + + VALIDATOR_IS_SEQUENCE_TYPE: Final[bool] = bool( + isinstance(yaml_validator, strictyaml.validators.SeqValidator) + and (partial_config_setting_name is None or ":" not in partial_config_setting_name) # noqa: COM812 + ) + if VALIDATOR_IS_SEQUENCE_TYPE: + return [ + sequence_value.strip() + for sequence_value + in new_config_setting_value.strip().split(",") + ] + + VALIDATOR_IS_MAPPING_TYPE: Final[bool] = bool( + isinstance(yaml_validator, strictyaml.validators.MapValidator) + and hasattr(yaml_validator, "_required_keys") + and partial_config_setting_name is not None # noqa: COM812 + ) + if VALIDATOR_IS_MAPPING_TYPE: + key: str + remainder: str + key, _, remainder = partial_config_setting_name.partition(":") # type: ignore[union-attr] + + # noinspection PyProtectedMember,PyUnresolvedReferences + if set(yaml_validator._required_keys) - {key}: # noqa: SLF001 + raise ChangingSettingWithRequiredSiblingError + + # noinspection PyUnresolvedReferences + return { + key: _set_required_value_from_validator_config( # type: ignore[dict-item] + remainder if ":" in partial_config_setting_name else None, # type: ignore[operator] + new_config_setting_value, + yaml_validator.get_validator(key), + ), + } + + UNKNOWN_CONFIG_TYPE_MESSAGE: Final[str] = ( + "Unknown YAML validator type. Expected mapping, sequence or scalar." + ) + raise RuntimeError(UNKNOWN_CONFIG_TYPE_MESSAGE) -def assign_single_config_setting_value(config_setting_name: str, settings_accessor: SettingsAccessor) -> None: # noqa: E501 +def assign_single_config_setting_value(config_setting_name: str, new_config_setting_value: str, settings_accessor: SettingsAccessor) -> None: # noqa: E501 """Set the value of a single configuration setting within the setting tree hierarchy.""" # noinspection PyProtectedMember - _get_mapping_config_setting_value( # noqa: SLF001 - config_setting_name, - settings_accessor._most_recent_yaml, # noqa: SLF001 - use_setter=True, - ) + current_yaml: YAML | None = settings_accessor._most_recent_yaml # noqa: SLF001 + if current_yaml is None: + YAML_NOT_LOADED_MESSAGE: Final[str] = ( + "Invalid state: Config YAML has not yet been loaded." + ) + raise RuntimeError(YAML_NOT_LOADED_MESSAGE) + + config_setting_error: ChangingSettingWithRequiredSiblingError + try: + settings_accessor = _set_mapping_config_setting_value( + config_setting_name, + new_config_setting_value, + current_yaml, + ) + except ChangingSettingWithRequiredSiblingError as config_setting_error: + raise type(config_setting_error)( + config_setting_name=config_setting_name, + ) from config_setting_error + + print(settings_accessor.as_yaml()) + + # TODO: save yaml diff --git a/config/_settings/_yaml/__init__.py b/config/_settings/_yaml/__init__.py index bfaac581e..db0c269b5 100644 --- a/config/_settings/_yaml/__init__.py +++ b/config/_settings/_yaml/__init__.py @@ -40,6 +40,7 @@ from .custom_map_validator import SlugKeyMap from .custom_scalar_validators import ( + CustomBoolValidator, DiscordSnowflakeValidator, DiscordWebhookURLValidator, LogLevelValidator, @@ -47,7 +48,6 @@ RegexMatcher, SendIntroductionRemindersFlagValidator, TimeDeltaValidator, - CustomBoolValidator, ) _DEFAULT_CONSOLE_LOGGING_SETTINGS: Final[Mapping[str, LogLevels]] = { @@ -87,7 +87,7 @@ "send-get-roles-reminders": _DEFAULT_SEND_GET_ROLES_REMINDERS_SETTINGS, } -SETTINGS_YAML_SCHEMA: Final[strictyaml.Validator] = SlugKeyMap( # type: ignore[no-any-unimported] +SETTINGS_YAML_SCHEMA: Final[strictyaml.Validator] = SlugKeyMap( { strictyaml.Optional("logging", default=_DEFAULT_LOGGING_SETTINGS): SlugKeyMap( { @@ -220,8 +220,8 @@ ) -def load_yaml(raw_yaml: str, file_name: str = "tex-bot-deployment.yaml") -> YAML: # type: ignore[no-any-unimported] - parsed_yaml: YAML = strictyaml.load(raw_yaml, SETTINGS_YAML_SCHEMA, label=file_name) # type: ignore[no-any-unimported] +def load_yaml(raw_yaml: str, file_name: str = "tex-bot-deployment.yaml") -> YAML: + parsed_yaml: YAML = strictyaml.load(raw_yaml, SETTINGS_YAML_SCHEMA, label=file_name) # noinspection SpellCheckingInspection if "guildofstudents" in parsed_yaml["community-group"]["members-list"]["url"]: diff --git a/config/_settings/_yaml/custom_map_validator.py b/config/_settings/_yaml/custom_map_validator.py index 45037377d..d6d486a67 100644 --- a/config/_settings/_yaml/custom_map_validator.py +++ b/config/_settings/_yaml/custom_map_validator.py @@ -10,15 +10,15 @@ from strictyaml.yamllocation import YAMLChunk -class SlugKeyValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] +class SlugKeyValidator(strictyaml.ScalarValidator): # type: ignore[misc] @override - def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimported,misc] + def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[misc] return slugify.slugify(str(chunk.contents)) -class SlugKeyMap(strictyaml.Map): # type: ignore[no-any-unimported,misc] +class SlugKeyMap(strictyaml.Map): # type: ignore[misc] @override - def __init__(self, validator: dict[object, object], key_validator: strictyaml.Validator | None = None) -> None: # type: ignore[no-any-unimported,misc] # noqa: E501 + def __init__(self, validator: dict[object, object], key_validator: strictyaml.Validator | None = None) -> None: # type: ignore[misc] # noqa: E501 super().__init__( validator=validator, key_validator=key_validator if key_validator is not None else SlugKeyValidator(), diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index e75667be1..6e9b14140 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -33,16 +33,16 @@ ) -class LogLevelValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] +class LogLevelValidator(strictyaml.ScalarValidator): # type: ignore[misc] @override - def validate_scalar(self, chunk: YAMLChunk) -> LogLevels: # type: ignore[no-any-unimported,misc] - val: str = str(chunk.contents).upper().strip().strip("-").strip("_").strip(".") + def validate_scalar(self, chunk: YAMLChunk) -> LogLevels: # type: ignore[misc] + val: str = str(chunk.contents).upper().strip(" \n\t-_.") if val not in LogLevels: chunk.expecting_but_found( ( "when expecting a valid log-level " - f"(one of: \"{"\", \"".join(LogLevels)}\")" + f"(one of: '{"', '".join(LogLevels)}')" ), ) raise RuntimeError @@ -53,23 +53,24 @@ def validate_scalar(self, chunk: YAMLChunk) -> LogLevels: # type: ignore[no-any @override def to_yaml(self, data: object) -> str: # type: ignore[misc] self.should_be_string(data, "expected a valid log-level.") - str_data: str = data.upper().strip().strip("-").strip("_").strip(".") # type: ignore[attr-defined] + str_data: str = data.upper().strip(" \n\t-_.") # type: ignore[attr-defined] if str_data not in LogLevels: INVALID_DATA_MESSAGE: Final[str] = ( - f"Got {data} when expecting one of: \"{"\", \"".join(LogLevels)}\"." + f"Got '{data}' when expecting one of: '{"', '".join(LogLevels)}'." ) raise YAMLSerializationError(INVALID_DATA_MESSAGE) return str_data -class DiscordWebhookURLValidator(strictyaml.Url): # type: ignore[no-any-unimported,misc] +class DiscordWebhookURLValidator(strictyaml.Url): # type: ignore[misc] @override - def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimported,misc] + def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[misc] + # noinspection PyUnresolvedReferences CHUNK_IS_VALID: Final[bool] = bool( ( - self.__is_absolute_url(chunk.contents) + self._Url__is_absolute_url(chunk.contents) and chunk.contents.startswith("https://discord.com/api/webhooks/") ), ) @@ -83,9 +84,10 @@ def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimp def to_yaml(self, data: object) -> str: # type: ignore[misc] self.should_be_string(data, "expected a URL,") + # noinspection PyUnresolvedReferences DATA_IS_VALID: Final[bool] = bool( ( - self.__is_absolute_url(data) + self._Url__is_absolute_url(data) and data.startswith("https://discord.com/api/webhooks/") # type: ignore[attr-defined] ), ) @@ -96,9 +98,9 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return data # type: ignore[return-value] -class DiscordSnowflakeValidator(strictyaml.Int): # type: ignore[no-any-unimported,misc] +class DiscordSnowflakeValidator(strictyaml.Int): # type: ignore[misc] @override - def validate_scalar(self, chunk: YAMLChunk) -> int: # type: ignore[no-any-unimported,misc] + def validate_scalar(self, chunk: YAMLChunk) -> int: # type: ignore[misc] val: int = super().validate_scalar(chunk) if not re.match(r"\A\d{17,20}\Z", str(val)): @@ -123,11 +125,11 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return str(data) -class RegexMatcher(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] +class RegexMatcher(strictyaml.ScalarValidator): # type: ignore[misc] MATCHING_MESSAGE: str = "when expecting a regular expression matcher" @override - def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[no-any-unimported,misc] + def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[misc] try: re.compile(chunk.contents) except re.error: @@ -153,12 +155,12 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return data # type: ignore[return-value] -class ProbabilityValidator(strictyaml.Float): # type: ignore[no-any-unimported,misc] +class ProbabilityValidator(strictyaml.Float): # type: ignore[misc] @override - def validate_scalar(self, chunk: YAMLChunk) -> float: # type: ignore[no-any-unimported,misc] - val: float = 100 * super().validate_scalar(chunk) + def validate_scalar(self, chunk: YAMLChunk) -> float: # type: ignore[misc] + val: float = super().validate_scalar(chunk) - if not 0 <= val <= 100: + if not 0 <= val <= 1: chunk.expecting_but_found("when expecting a probability") raise RuntimeError @@ -166,35 +168,30 @@ def validate_scalar(self, chunk: YAMLChunk) -> float: # type: ignore[no-any-uni @override def to_yaml(self, data: object) -> str: # type: ignore[misc] - YAML_SERIALIZATION_ERROR: Final[YAMLSerializationError] = YAMLSerializationError( # type: ignore[no-any-unimported] + YAML_SERIALIZATION_ERROR: Final[YAMLSerializationError] = YAMLSerializationError( f"'{data}' is not a probability.", ) - if strictyaml_utils.has_number_type(data): - if not 0 <= data <= 100: # type: ignore[operator] - raise YAML_SERIALIZATION_ERROR - - if math.isnan(data): # type: ignore[arg-type] - return "nan" - if data == float("inf"): - return "inf" - if data == float("-inf"): - return "-inf" - - return str(data / 100) # type: ignore[operator] - if strictyaml_utils.is_string(data) and strictyaml_utils.is_decimal(data): - float_data: float = float(str(data)) + data = float(str(data)) - if not 0 <= float_data <= 100: - raise YAML_SERIALIZATION_ERROR + if not strictyaml_utils.has_number_type(data): + raise YAML_SERIALIZATION_ERROR - return str(float_data / 100) + if not 0 <= data <= 1: # type: ignore[operator] + raise YAML_SERIALIZATION_ERROR - raise YAML_SERIALIZATION_ERROR + if math.isnan(data): # type: ignore[arg-type] + return "nan" + if data == float("inf"): + return "inf" + if data == float("-inf"): + return "-inf" + + return str(data) -class TimeDeltaValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] +class TimeDeltaValidator(strictyaml.ScalarValidator): # type: ignore[misc] @override def __init__(self, *, minutes: bool = True, hours: bool = True, days: bool = False, weeks: bool = False) -> None: # noqa: E501 regex_matcher: str = r"\A" @@ -242,7 +239,7 @@ def _get_value_from_match(self, match: Match[str], key: str) -> float: raise float_conversion_error from float_conversion_error @override - def validate_scalar(self, chunk: YAMLChunk) -> timedelta: # type: ignore[no-any-unimported,misc] + def validate_scalar(self, chunk: YAMLChunk) -> timedelta: # type: ignore[misc] chunk_error_func: Callable[[], NoReturn] = functools.partial( chunk.expecting_but_found, expecting="when expecting a delay/interval string", @@ -289,16 +286,16 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return f"{total_seconds}s" -class SendIntroductionRemindersFlagValidator(strictyaml.ScalarValidator): # type: ignore[no-any-unimported,misc] +class SendIntroductionRemindersFlagValidator(strictyaml.ScalarValidator): # type: ignore[misc] @override - def validate_scalar(self, chunk: YAMLChunk) -> SendIntroductionRemindersFlagType: # type: ignore[no-any-unimported,misc] + def validate_scalar(self, chunk: YAMLChunk) -> SendIntroductionRemindersFlagType: # type: ignore[misc] val: str = str(chunk.contents).lower() if val not in VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES: chunk.expecting_but_found( ( "when expecting a send-introduction-reminders-flag " - "(one of: \"once\", \"interval\" or \"false\")" + "(one of: 'once', 'interval' or 'false')" ), ) raise RuntimeError @@ -319,7 +316,7 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] if str(data).lower() not in VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES: INVALID_DATA_MESSAGE: Final[str] = ( - f"Got {data} when expecting one of: \"once\", \"interval\" or \"false\"." + f"Got '{data}' when expecting one of: 'once', 'interval' or 'false'." ) raise YAMLSerializationError(INVALID_DATA_MESSAGE) @@ -332,7 +329,7 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return str(data).lower() -class CustomBoolValidator(strictyaml.Bool): # type: ignore[no-any-unimported,misc] +class CustomBoolValidator(strictyaml.Bool): # type: ignore[misc] @override def to_yaml(self, data: object) -> str: # type: ignore[misc] if isinstance(data, bool): @@ -344,4 +341,5 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] if str(data).lower() in strictyaml_constants.FALSE_VALUES: return "false" - raise YAMLSerializationError("Not a boolean") + INVALID_TYPE_MESSAGE: Final[str] = "Not a boolean" + raise YAMLSerializationError(INVALID_TYPE_MESSAGE) diff --git a/config/constants.py b/config/constants.py index 8f8420d46..e59c0cd62 100644 --- a/config/constants.py +++ b/config/constants.py @@ -9,7 +9,6 @@ "PROJECT_ROOT", "VALID_SEND_INTRODUCTION_REMINDERS_RAW_VALUES", "MESSAGES_LOCALE_CODES", - "REQUIRES_RESTART_SETTINGS", "DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME", "DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY", "DEFAULT_DISCORD_LOGGING_LOG_LEVEL", @@ -64,6 +63,7 @@ class ConfigSettingHelp(NamedTuple): description: str value_type_message: str | None + requires_restart_after_changed: bool required: bool = True default: str | None = None @@ -92,20 +92,6 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non ({"once", "interval"} | set(strictyaml_constants.BOOL_VALUES)), ) -REQUIRES_RESTART_SETTINGS: Final[frozenset[str]] = frozenset( - { - "discord:bot-token", - "discord:guild-id", - "messages-locale-code", - "reminders:send-introduction-reminders:enabled", - "reminders:send-introduction-reminders:delay", - "reminders:send-introduction-reminders:interval", - "reminders:send-get-roles-reminders:enabled", - "reminders:send-get-roles-reminders:delay", - "reminders:send-get-roles-reminders:interval", - }, -) - DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME: Final[str] = "TeX-Bot" @@ -151,6 +137,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "to the console output stream." ), value_type_message=_selectable_required_format_message(LogLevels), + requires_restart_after_changed=False, required=False, default=DEFAULT_CONSOLE_LOG_LEVEL, ), @@ -160,6 +147,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "to the Discord log channel." ), value_type_message=_selectable_required_format_message(LogLevels), + requires_restart_after_changed=False, required=False, default=DEFAULT_DISCORD_LOGGING_LOG_LEVEL, ), @@ -173,6 +161,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "Discord webhook URL", "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks", ), + requires_restart_after_changed=False, required=False, default=None, ), @@ -185,6 +174,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "Discord bot token", "https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts", ), + requires_restart_after_changed=True, required=True, default=None, ), @@ -194,6 +184,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "Discord guild ID", "https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id", ), + requires_restart_after_changed=True, required=True, default=None, ), @@ -205,6 +196,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "If this is not set the group-full-name will be retrieved " "from the name of your group's Discord guild." ), + requires_restart_after_changed=False, value_type_message=None, required=False, default=None, @@ -216,6 +208,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "If this is not set the group-short-name will be determined " "from your group's full name." ), + requires_restart_after_changed=False, value_type_message=None, required=False, default=None, @@ -225,6 +218,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "The link to the page where guests can purchase a full membership " "to join your community group." ), + requires_restart_after_changed=False, value_type_message=_custom_required_format_message("URL"), required=False, default=None, @@ -235,6 +229,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "about the perks that they will receive " "once they purchase a membership to your community group." ), + requires_restart_after_changed=False, value_type_message=_custom_required_format_message("URL"), required=False, default=None, @@ -242,6 +237,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "community-group:links:moderation-document": ConfigSettingHelp( description="The link to your group's Discord guild moderation document.", value_type_message=_custom_required_format_message("URL"), + requires_restart_after_changed=False, required=True, default=None, ), @@ -254,6 +250,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "if your members-list is found on the UoB Guild of Students website, " "ensure the URL includes the \"sort by groups\" option)." ), + requires_restart_after_changed=False, value_type_message=_custom_required_format_message("URL"), required=True, default=None, @@ -270,15 +267,20 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "after manually logging in to view your members-list, " "it will probably be listed as a cookie named `.ASPXAUTH`." ), + requires_restart_after_changed=False, value_type_message=None, required=True, default=None, ), "community-group:members-list:id-format": ConfigSettingHelp( - description="The format that IDs are stored in within your members-list.", + description=( + "The format that IDs are stored in within your members-list.\n" + "Remember to double escape `\\` characters where necessary." + ), value_type_message=_custom_required_format_message( "regex matcher string", ), + requires_restart_after_changed=False, required=False, default=DEFAULT_MEMBERS_LIST_ID_FORMAT, ), @@ -290,6 +292,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non value_type_message=_custom_required_format_message( "float, inclusively between 1 & 0", ), + requires_restart_after_changed=False, required=False, default=str(DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY), ), @@ -300,6 +303,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non value_type_message=_custom_required_format_message( "float representing the number of days to look back through", ), + requires_restart_after_changed=False, required=False, default=str(DEFAULT_STATS_COMMAND_LOOKBACK_DAYS), ), @@ -311,6 +315,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non value_type_message=_custom_required_format_message( "comma seperated list of strings of role names", ), + requires_restart_after_changed=False, required=False, default=",".join(DEFAULT_STATS_COMMAND_DISPLAYED_ROLES), ), @@ -324,6 +329,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "to timeout a user (format: `smhdw`)" ), ), + requires_restart_after_changed=False, required=False, default=DEFAULT_STRIKE_COMMAND_TIMEOUT_DURATION, ), @@ -344,6 +350,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "in the committee-member's DMs)" ), ), + requires_restart_after_changed=False, required=False, default=DEFAULT_STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION, ), @@ -354,6 +361,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non value_type_message=_selectable_required_format_message( MESSAGES_LOCALE_CODES, ), + requires_restart_after_changed=False, required=False, default=DEFAULT_MESSAGE_LOCALE_CODE, ), @@ -370,6 +378,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non in getattr(SendIntroductionRemindersFlagType, "__args__") # noqa: B009 ), ), + requires_restart_after_changed=True, required=False, default=str(DEFAULT_SEND_INTRODUCTION_REMINDERS_ENABLED).lower(), ), @@ -388,6 +397,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "(format: `smhdw`)" ), ), + requires_restart_after_changed=True, required=False, default=DEFAULT_SEND_INTRODUCTION_REMINDERS_DELAY, ), @@ -404,6 +414,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "(format: `smh`)" ), ), + requires_restart_after_changed=True, required=False, default=DEFAULT_SEND_INTRODUCTION_REMINDERS_INTERVAL, ), @@ -416,6 +427,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non value_type_message=_custom_required_format_message( "boolean value (either `true` or `false`)", ), + requires_restart_after_changed=True, required=False, default=str(DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED).lower(), ), @@ -433,6 +445,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "(format: `smhdw`)" ), ), + requires_restart_after_changed=True, required=False, default=DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY, ), @@ -451,7 +464,15 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non "(format: `smh`)" ), ), + requires_restart_after_changed=True, required=False, default=DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, ), } + +# { # TODO: Use in config reloader +# config_setting_name +# for config_setting_name, config_setting_help +# in CONFIG_SETTINGS_HELPS.items() +# if config_setting_help.requires_restart_after_changed +# } diff --git a/exceptions/__init__.py b/exceptions/__init__.py index 4c81e9f71..6a7693790 100644 --- a/exceptions/__init__.py +++ b/exceptions/__init__.py @@ -18,10 +18,13 @@ "StrikeTrackingError", "NoAuditLogsStrikeTrackingError", "BotRequiresRestartAfterConfigChange", + "ChangingSettingWithRequiredSiblingError", ) + from .config_changes import ( BotRequiresRestartAfterConfigChange, + ChangingSettingWithRequiredSiblingError, ) from .does_not_exist import ( ArchivistRoleDoesNotExistError, diff --git a/exceptions/base.py b/exceptions/base.py index bc018c4f7..baec0d29d 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 @@ -24,12 +24,14 @@ class BaseTeXBotError(BaseException, abc.ABC): 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.""" 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 @@ -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 2e6e33768..2a31606d0 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -2,25 +2,61 @@ from collections.abc import Sequence -__all__: Sequence[str] = ("BotRequiresRestartAfterConfigChange",) +__all__: Sequence[str] = ( + "BotRequiresRestartAfterConfigChange", + "ChangingSettingWithRequiredSiblingError", +) +from typing import override + from classproperties import classproperty from .base import BaseTeXBotError -class BotRequiresRestartAfterConfigChange(BaseTeXBotError): +class BotRequiresRestartAfterConfigChange(BaseTeXBotError, Exception): """Exception class to raise to enforce handling of bot restarts after config changes.""" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "TeX-Bot requires a restart due to configuration changes." + @override def __init__(self, message: str | None = None, changed_settings: set[str] | None = None) -> None: # noqa: E501 """Initialize a ValueError exception for a non-existent user ID.""" self.changed_settings: set[str] | None = changed_settings super().__init__(message) + + +class ChangingSettingWithRequiredSiblingError(BaseTeXBotError, ValueError): + """Exception class for when a setting cannot be changed because of required siblings.""" + + # noinspection PyMethodParameters,PyPep8Naming + @classproperty + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 + """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 + return ( + "The given setting cannot be changed " + "because it has one or more required sibling settings that must be set first." + ) + + @override + def __init__(self, message: str | None = None, config_setting_name: str | None = None) -> None: # noqa: E501 + """Initialize a ValueError exception for a non-existent user ID.""" + self.config_setting_name: str | None = config_setting_name + + super().__init__( + message + or ( + f"Cannot assign value to config setting '{config_setting_name}' " + f"because it has one or more required sibling settings that must be set first." + if config_setting_name + else message + ) # noqa: COM812 + ) diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index 61b9c46d0..89b6ae7d2 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -17,7 +17,7 @@ import abc -from typing import Final +from typing import Final, override from classproperties import classproperty @@ -29,7 +29,8 @@ class RulesChannelDoesNotExistError(BaseTeXBotError, ValueError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "There is no channel marked as the rules channel." @@ -39,28 +40,32 @@ class GuildDoesNotExistError(BaseDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 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 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1011" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N802,N805 + @override + def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 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.""" 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) @@ -70,13 +75,15 @@ class RoleDoesNotExistError(BaseDoesNotExistError, abc.ABC): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 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 + @override + def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 return "role" @@ -86,6 +93,7 @@ def DOES_NOT_EXIST_TYPE(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.""" HAS_DEPENDANTS: Final[bool] = bool( @@ -103,13 +111,15 @@ class CommitteeRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1021" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 """ The set of names of bot commands that require this Discord entity. @@ -132,7 +142,8 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802,N805 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 """The name of the Discord role that does not exist.""" # noqa: D401 return "Committee" @@ -142,13 +153,15 @@ class GuestRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1022" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 """ The set of names of bot commands that require this Discord entity. @@ -160,7 +173,8 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N802,N805 + @override + def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N805 """ The set of names of bot tasks that require this Discord entity. @@ -172,7 +186,8 @@ def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N802,N805 # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802,N805 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 """The name of the Discord role that does not exist.""" # noqa: D401 return "Guest" @@ -182,13 +197,15 @@ class MemberRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1023" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 """ The set of names of bot commands that require this Discord entity. @@ -200,7 +217,8 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802,N805 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 """The name of the Discord role that does not exist.""" # noqa: D401 return "Member" @@ -210,13 +228,15 @@ class ArchivistRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1024" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 """ The set of names of bot commands that require this Discord entity. @@ -228,7 +248,8 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ROLE_NAME(cls) -> str: # noqa: N802,N805 + @override + def ROLE_NAME(cls) -> str: # noqa: N805 """The name of the Discord role that does not exist.""" # noqa: D401 return "Archivist" @@ -238,13 +259,15 @@ class ChannelDoesNotExistError(BaseDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 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 + @override + def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 return "channel" @@ -254,6 +277,7 @@ def DOES_NOT_EXIST_TYPE(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.""" HAS_DEPENDANTS: Final[bool] = bool( @@ -273,13 +297,15 @@ class RolesChannelDoesNotExistError(ChannelDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1031" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 """ The set of names of bot commands that require this Discord entity. @@ -291,7 +317,8 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 # noinspection PyMethodParameters,PyPep8Naming @classproperty - def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 + @override + def CHANNEL_NAME(cls) -> str: # noqa: N805 """The name of the Discord channel that does not exist.""" # noqa: D401 return "roles" @@ -301,13 +328,15 @@ class GeneralChannelDoesNotExistError(ChannelDoesNotExistError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1032" # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 """ The set of names of bot commands that require this Discord entity. @@ -319,13 +348,7 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 # noinspection PyMethodParameters,PyPep8Naming @classproperty - def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 + @override + def CHANNEL_NAME(cls) -> str: # noqa: N805 """The name of the Discord channel that does not exist.""" # noqa: D401 return "general" - - - - - - - diff --git a/exceptions/guild.py b/exceptions/guild.py index a0013945e..3e252698c 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,7 +20,8 @@ class DiscordMemberNotInMainGuildError(BaseTeXBotError, ValueError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "Given user ID does not represent any member of your group's Discord guild." @@ -33,12 +37,14 @@ class EveryoneRoleCouldNotBeRetrievedError(BaseErrorWithErrorCode, ValueError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "The reference to the \"@everyone\" role could not be correctly retrieved." # noinspection PyMethodParameters,PyPep8Naming @classproperty - def ERROR_CODE(cls) -> str: # noqa: N802,N805 + @override + def ERROR_CODE(cls) -> str: # noqa: N805 """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1042" diff --git a/exceptions/strike.py b/exceptions/strike.py index e4c0bc226..643332363 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,7 +25,8 @@ class StrikeTrackingError(BaseTeXBotError, RuntimeError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "An error occurred while trying to track manually applied moderation actions." @@ -37,7 +41,7 @@ class NoAuditLogsStrikeTrackingError(BaseTeXBotError, RuntimeError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "Unable to retrieve audit log entry after possible manual moderation action." - diff --git a/pyproject.toml b/pyproject.toml index 36caaaac8..61e1c3d9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,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 diff --git a/utils/__init__.py b/utils/__init__.py index 0c28029e1..d53ad82f0 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -67,5 +67,5 @@ def is_member_inducted(member: discord.Member) -> bool: 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/tex_bot.py b/utils/tex_bot.py index 3c7455e11..dcb03a786 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -470,7 +470,7 @@ async def get_member_from_str_id(self, str_member_id: str) -> discord.Member: if not re.match(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_contexts.py b/utils/tex_bot_contexts.py index 99f652730..28c265862 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. """ From 94d8b030a602768c151cbb6111468b883a317951 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 9 Jun 2024 05:15:44 +0100 Subject: [PATCH 035/128] Add TODO --- config/_settings/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 87003139d..9d1b9c094 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -907,6 +907,8 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte return {"reminders:send-get-roles-reminders:interval"} +# TODO: Move to change_config cog + def _get_scalar_config_setting_value(config_setting_name: str, yaml_settings_tree: YAML) -> str | None: # noqa: E501 single_yaml_scalar_setting: YAML | None = yaml_settings_tree.get(config_setting_name, None) From 9865120b9941043360c09193aa96f2d762018103 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 9 Jun 2024 19:49:33 +0100 Subject: [PATCH 036/128] Fix saving config to file --- cogs/change_config.py | 6 +- cogs/command_error.py | 59 ++-- cogs/induct.py | 14 +- config/__init__.py | 54 ++-- config/_messages/__init__.py | 20 +- config/_settings/__init__.py | 548 ++++++++++++++++------------------- config/_settings/utils.py | 60 +++- config/constants.py | 1 - poetry.lock | 90 +++++- pyproject.toml | 2 + 10 files changed, 485 insertions(+), 369 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index 393a97045..93f7699ee 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -147,7 +147,6 @@ async def help_config_setting(self, ctx: TeXBotApplicationContext, config_settin config_setting_name ] - # noinspection PyProtectedMember await ctx.respond( ( f"## `{ @@ -220,7 +219,10 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n yaml_error: StrictYAMLError changing_setting_error: ChangingSettingWithRequiredSiblingError try: - config.assign_single_config_setting_value(config_setting_name, new_config_value) + await config.assign_single_config_setting_value( + config_setting_name, + new_config_value, + ) except StrictYAMLError as yaml_error: if str(yaml_error) != yaml_error.context: INCONCLUSIVE_YAML_ERROR_MESSAGE: Final[str] = ( diff --git a/cogs/command_error.py b/cogs/command_error.py index c7526a204..cade4d0fc 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -48,7 +48,16 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro str(error.original).startswith("\"") or str(error.original).startswith("'") ) - else error.original + else ( + f"{ + f"{error.original.__class__.__name__}: " + if isinstance( + error.original, + RuntimeError | NotImplementedError, + ) + else "" + }{error.original}" + ) ) ) ) @@ -80,23 +89,31 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro logging_message=logging_message, ) - if isinstance(error, discord.ApplicationCommandInvokeError) and isinstance(error.original, GuildDoesNotExistError): # noqa: E501 - command_name: str = ( - ctx.command.callback.__name__ - if (hasattr(ctx.command, "callback") - and not ctx.command.callback.__name__.startswith("_")) - else ctx.command.qualified_name - ) - logger.critical( - " ".join( - message_part - for message_part - in ( - error.original.ERROR_CODE, - f"({command_name})" if command_name in self.ERROR_ACTIVITIES else "", - str(error.original).rstrip(".:"), - ) - if message_part - ), - ) - await self.bot.close() + if isinstance(error, discord.ApplicationCommandInvokeError): + if isinstance(error.original, RuntimeError | NotImplementedError): + await self.bot.close() + + elif isinstance(error.original, GuildDoesNotExistError): + command_name: str = ( + ctx.command.callback.__name__ + if (hasattr(ctx.command, "callback") + and not ctx.command.callback.__name__.startswith("_")) + else ctx.command.qualified_name + ) + logger.critical( + " ".join( + message_part + for message_part + in ( + error.original.ERROR_CODE, + ( + f"({command_name})" + if command_name in self.ERROR_ACTIVITIES + else "" + ), + str(error.original).rstrip(".:"), + ) + if message_part + ), + ) + await self.bot.close() diff --git a/cogs/induct.py b/cogs/induct.py index a759b1b11..a4e3800c4 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -117,12 +117,20 @@ async def on_member_update(self, before: discord.Member, after: discord.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_LINK"]}. """ + f"{ + f"Just head to {settings["PURCHASE_MEMBERSHIP_LINK"]}. " + if settings["PURCHASE_MEMBERSHIP_LINK"] + else "" + }" "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" + f"and a cool green name on the {self.bot.group_short_name} Discord server" ":green_square:! " - f"Checkout all the perks at {settings["MEMBERSHIP_PERKS_LINKS"]}", + f"{ + f"Checkout all the perks at {settings["MEMBERSHIP_PERKS_LINK"]}" + if settings["MEMBERSHIP_PERKS_LINK"] + else "" + }", ) except discord.Forbidden: logger.info( diff --git a/config/__init__.py b/config/__init__.py index c45f17c61..f980f3d4b 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -15,24 +15,26 @@ "settings", "check_for_deprecated_environment_variables", "messages", - "view_single_config_setting_value", - "assign_single_config_setting_value", "CONFIG_SETTINGS_HELPS", "ConfigSettingHelp", + "get_settings_file_path", + "view_single_config_setting_value", + "assign_single_config_setting_value", ) -import functools import importlib import logging import os from collections.abc import Iterable from logging import Logger -from typing import Final, Protocol +from typing import Final -from . import _settings -from ._messages import MessagesAccessor -from ._settings import SettingsAccessor +from asgiref.sync import async_to_sync + +from ._messages import MessagesAccessor as _MessagesAccessor +from ._settings import SettingsAccessor as _SettingsAccessor +from ._settings import get_settings_file_path, utils from .constants import ( CONFIG_SETTINGS_HELPS, MESSAGES_LOCALE_CODES, @@ -43,17 +45,16 @@ logger: Final[Logger] = logging.getLogger("TeX-Bot") -settings: Final[SettingsAccessor] = SettingsAccessor() -messages: Final[MessagesAccessor] = MessagesAccessor() +settings: Final[_SettingsAccessor] = _SettingsAccessor() +messages: Final[_MessagesAccessor] = _MessagesAccessor() def run_setup() -> None: """Execute the setup functions required, before other modules can be run.""" check_for_deprecated_environment_variables() - settings.reload() - - messages.load(settings["MESSAGES_LOCALE_CODE"]) + async_to_sync(settings.reload)() + async_to_sync(messages.load)(settings["MESSAGES_LOCALE_CODE"]) logger.debug("Begin database setup") @@ -67,6 +68,12 @@ def run_setup() -> None: def check_for_deprecated_environment_variables() -> None: """Raise an error if the old method of configuration (environment variables) is used.""" + if utils.is_running_in_async(): + RUNNING_IN_ASYNC_MESSAGE: Final[str] = ( + "Cannot check for deprecated environment variables while TeX-Bot is running." + ) + raise RuntimeError(RUNNING_IN_ASYNC_MESSAGE) + CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR: Final[DeprecationWarning] = ( DeprecationWarning( ( @@ -157,19 +164,14 @@ def check_for_deprecated_environment_variables() -> None: raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR -class _SingleSettingValueViewerFunc(Protocol): - def __call__(self, config_setting_name: str) -> str | None: ... - - -class _SingleSettingAssignerFunc(Protocol): - def __call__(self, config_setting_name: str, new_config_setting_value: str) -> None: ... +def view_single_config_setting_value(config_setting_name: str) -> str | None: + """Return the value of a single configuration setting from settings tree hierarchy.""" + return settings.view_single_raw_value(config_setting_name=config_setting_name) -view_single_config_setting_value: _SingleSettingValueViewerFunc = functools.partial( - _settings.view_single_config_setting_value, - settings_accessor=settings, -) -assign_single_config_setting_value: _SingleSettingAssignerFunc = functools.partial( - _settings.assign_single_config_setting_value, - settings_accessor=settings, -) +async def assign_single_config_setting_value(config_setting_name: str, new_config_setting_value: str) -> None: # noqa: E501 + """Set the value of a single configuration setting within settings tree hierarchy.""" + return await settings.assign_single_raw_value( + config_setting_name=config_setting_name, + new_config_setting_value=new_config_setting_value, + ) diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index 332d69ac3..26401e3da 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -5,12 +5,11 @@ import json import re -from typing import TYPE_CHECKING, Any, ClassVar, Final +from typing import Any, ClassVar, Final -from config.constants import MESSAGES_LOCALE_CODES, PROJECT_ROOT +from aiopath import AsyncPath -if TYPE_CHECKING: - from pathlib import Path +from config.constants import MESSAGES_LOCALE_CODES, PROJECT_ROOT class MessagesAccessor: @@ -61,7 +60,7 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 raise KeyError(key_error_message) from None @classmethod - def load(cls, messages_locale_code: str) -> None: + async def load(cls, messages_locale_code: str) -> None: if messages_locale_code not in MESSAGES_LOCALE_CODES: INVALID_MESSAGES_LOCALE_CODE_MESSAGE: Final[str] = ( f"{"messages_locale_code"!r} must be one of " @@ -78,21 +77,22 @@ def load(cls, messages_locale_code: str) -> None: ) try: - messages_locale_file_path: Path = next( + # noinspection PyTypeChecker + messages_locale_file_path: AsyncPath = await anext( path - for path - in (PROJECT_ROOT / "config/_messages/locales/").iterdir() + async for path + in (AsyncPath(PROJECT_ROOT) / "config/_messages/locales/").iterdir() if path.stem == messages_locale_code ) except StopIteration: raise NO_MESSAGES_FILE_FOUND_ERROR from None - if not messages_locale_file_path.is_file(): + if not await messages_locale_file_path.is_file(): raise NO_MESSAGES_FILE_FOUND_ERROR messages_load_error: Exception try: - raw_messages: object = json.loads(messages_locale_file_path.read_text()) + raw_messages: object = json.loads(await messages_locale_file_path.read_text()) if not hasattr(raw_messages, "__getitem__"): raise TypeError diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 9d1b9c094..267deb132 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -7,93 +7,34 @@ from collections.abc import Sequence -__all__: Sequence[str] = ( - "get_settings_file_path", - "SettingsAccessor", - "view_single_config_setting_value", - "assign_single_config_setting_value", -) +__all__: Sequence[str] = ("get_settings_file_path", "SettingsAccessor") + -import contextlib import logging -import os import re from collections.abc import Iterable, Mapping from datetime import timedelta from logging import Logger -from pathlib import Path from typing import Any, ClassVar, Final, TextIO, TypeAlias import strictyaml +from aiopath import AsyncPath # noqa: TCH002 from discord_logging.handler import DiscordHandler from strictyaml import YAML -from config.constants import ( - DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME, - PROJECT_ROOT, -) -from exceptions import ( - BotRequiresRestartAfterConfigChange, - ChangingSettingWithRequiredSiblingError, -) +from config.constants import DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME +from exceptions import ChangingSettingWithRequiredSiblingError from . import utils from ._yaml import load_yaml +from .utils import get_settings_file_path NestedMapping: TypeAlias = Mapping[str, "NestedMapping | str"] logger: Final[Logger] = logging.getLogger("TeX-Bot") -def get_settings_file_path() -> Path: - settings_file_not_found_message: str = ( - "No settings file was found. " - "Please make sure you have created a `tex-bot-deployment.yaml` file." - ) - - raw_settings_file_path: str | None = ( - os.getenv("TEX_BOT_SETTINGS_FILE_PATH", None) - or os.getenv("TEX_BOT_SETTINGS_FILE", None) - or os.getenv("TEX_BOT_SETTINGS_PATH", None) - or os.getenv("TEX_BOT_SETTINGS", None) - or os.getenv("TEX_BOT_CONFIG_FILE_PATH", None) - or os.getenv("TEX_BOT_CONFIG_FILE", None) - or os.getenv("TEX_BOT_CONFIG_PATH", None) - or os.getenv("TEX_BOT_CONFIG", None) - or os.getenv("TEX_BOT_DEPLOYMENT_FILE_PATH", None) - or os.getenv("TEX_BOT_DEPLOYMENT_FILE", None) - or os.getenv("TEX_BOT_DEPLOYMENT_PATH", None) - or os.getenv("TEX_BOT_DEPLOYMENT", None) - ) - - if raw_settings_file_path: - settings_file_not_found_message = ( - "A path to the settings file location was provided by environment variable, " - "however this path does not refer to an existing file." - ) - else: - logger.debug( - ( - "Settings file location not supplied by environment variable, " - "falling back to `Tex-Bot-deployment.yaml`." - ), - ) - raw_settings_file_path = "tex-bot-deployment.yaml" - if not (PROJECT_ROOT / raw_settings_file_path).exists(): - raw_settings_file_path = "tex-bot-settings.yaml" - - if not (PROJECT_ROOT / raw_settings_file_path).exists(): - raw_settings_file_path = "tex-bot-config.yaml" - - settings_file_path: Path = Path(raw_settings_file_path) - - if not settings_file_path.is_file(): - raise FileNotFoundError(settings_file_not_found_message) - - return settings_file_path - - class SettingsAccessor: """ Settings class that provides access to all settings values. @@ -122,8 +63,10 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) if self._most_recent_yaml is None: - with contextlib.suppress(BotRequiresRestartAfterConfigChange): - self.reload() + YAML_NOT_LOADED_MESSAGE: Final[str] = ( + "Configuration cannot be accessed before it is loaded." + ) + raise RuntimeError(YAML_NOT_LOADED_MESSAGE) if item not in self._settings: INVALID_SETTINGS_KEY_MESSAGE: Final[str] = ( @@ -163,10 +106,10 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 raise KeyError(key_error_message) from None @classmethod - def reload(cls) -> None: - settings_file_path: Path = get_settings_file_path() - current_yaml: YAML = load_yaml( # TODO: better error messages when loading yaml - settings_file_path.read_text(), + async def reload(cls) -> None: + settings_file_path: AsyncPath = await utils.get_settings_file_path() + current_yaml: YAML = load_yaml( + await settings_file_path.read_text(), file_name=settings_file_path.name, ) @@ -253,7 +196,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # """ CONSOLE_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or console_logging_settings != cls._most_recent_yaml["logging"]["console"], + or console_logging_settings != cls._most_recent_yaml["logging"]["console"] # noqa: COM812 ) if not CONSOLE_LOGGING_SETTINGS_CHANGED: return set() @@ -305,11 +248,11 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y """ DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "DISCORD_LOG_CHANNEL_WEBHOOK_URL" not in cls._settings or discord_channel_logging_settings != cls._most_recent_yaml["logging"].get( "discord-channel", None, - ) - or "DISCORD_LOG_CHANNEL_WEBHOOK_URL" not in cls._settings, + ) # noqa: COM812 ) if not DISCORD_CHANNEL_LOGGING_SETTINGS_CHANGED: return set() @@ -346,13 +289,13 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y for key, value in discord_channel_logging_settings.items() if key != "log-level" - ), + ) # noqa: COM812 ) if ONLY_DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: DISCORD_LOG_CHANNEL_LOG_LEVEL_IS_SAME: Final[bool] = bool( discord_channel_logging_settings["log-level"] == cls._most_recent_yaml[ # type: ignore[index] "logging" - ]["discord-channel"]["log-level"], + ]["discord-channel"]["log-level"] # noqa: COM812 ) if DISCORD_LOG_CHANNEL_LOG_LEVEL_IS_SAME: LOG_LEVEL_DIDNT_CHANGE_MESSAGE: Final[str] = ( @@ -400,7 +343,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y or cls._most_recent_yaml["logging"].get("discord-channel", None) is None or discord_channel_logging_settings["log-level"] != cls._most_recent_yaml[ "logging" - ]["discord-channel"]["log-level"], + ]["discord-channel"]["log-level"] # noqa: COM812 ) if DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: changed_settings.add("logging:discord-channel:log-level") @@ -416,8 +359,8 @@ def _reload_discord_bot_token(cls, discord_bot_token: YAML) -> set[str]: # type """ DISCORD_BOT_TOKEN_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or discord_bot_token != cls._most_recent_yaml["discord"]["bot-token"] - or "DISCORD_BOT_TOKEN" not in cls._settings, + or "DISCORD_BOT_TOKEN" not in cls._settings + or discord_bot_token != cls._most_recent_yaml["discord"]["bot-token"] # noqa: COM812 ) if not DISCORD_BOT_TOKEN_CHANGED: return set() @@ -435,8 +378,8 @@ def _reload_discord_main_guild_id(cls, discord_main_guild_id: YAML) -> set[str]: """ DISCORD_MAIN_GUILD_ID_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or discord_main_guild_id != cls._most_recent_yaml["discord"]["main-guild-id"] - or "_DISCORD_MAIN_GUILD_ID" not in cls._settings, + or "_DISCORD_MAIN_GUILD_ID" not in cls._settings + or discord_main_guild_id != cls._most_recent_yaml["discord"]["main-guild-id"] # noqa: COM812 ) if not DISCORD_MAIN_GUILD_ID_CHANGED: return set() @@ -454,11 +397,11 @@ def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # t """ GROUP_FULL_NAME_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "_GROUP_FULL_NAME" not in cls._settings or group_full_name != cls._most_recent_yaml["community-group"].get( "full-name", None, - ) - or "_GROUP_FULL_NAME" not in cls._settings, + ) # noqa: COM812 ) if not GROUP_FULL_NAME_CHANGED: return set() @@ -480,11 +423,11 @@ def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # """ GROUP_SHORT_NAME_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "_GROUP_SHORT_NAME" not in cls._settings or group_short_name != cls._most_recent_yaml["community-group"].get( "short-name", None, - ) - or "_GROUP_SHORT_NAME" not in cls._settings, + ) # noqa: COM812 ) if not GROUP_SHORT_NAME_CHANGED: return set() @@ -506,10 +449,10 @@ def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) """ PURCHASE_MEMBERSHIP_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "PURCHASE_MEMBERSHIP_LINK" not in cls._settings or purchase_membership_link != cls._most_recent_yaml["community-group"][ "links" - ].get("purchase-membership", None) - or "PURCHASE_MEMBERSHIP_LINK" not in cls._settings, + ].get("purchase-membership", None) # noqa: COM812 ) if not PURCHASE_MEMBERSHIP_LINK_CHANGED: return set() @@ -531,10 +474,10 @@ def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> se """ MEMBERSHIP_PERKS_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "MEMBERSHIP_PERKS_LINK" not in cls._settings or membership_perks_link != cls._most_recent_yaml["community-group"][ "links" - ].get("membership-perks", None) - or "MEMBERSHIP_PERKS_LINK" not in cls._settings, + ].get("membership-perks", None) # noqa: COM812 ) if not MEMBERSHIP_PERKS_LINK_CHANGED: return set() @@ -556,10 +499,10 @@ def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set """ MODERATION_DOCUMENT_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "MODERATION_DOCUMENT_LINK" not in cls._settings or moderation_document_link != cls._most_recent_yaml["community-group"]["links"][ "moderation-document" - ] - or "MODERATION_DOCUMENT_LINK" not in cls._settings, + ] # noqa: COM812 ) if not MODERATION_DOCUMENT_LINK_CHANGED: return set() @@ -577,10 +520,10 @@ def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: """ MEMBERS_LIST_URL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "MEMBERS_LIST_URL" not in cls._settings or members_list_url != cls._most_recent_yaml["community-group"]["members-list"][ "url" - ] - or "MEMBERS_LIST_URL" not in cls._settings, + ] # noqa: COM812 ) if not MEMBERS_LIST_URL_CHANGED: return set() @@ -598,10 +541,10 @@ def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cook """ MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "MEMBERS_LIST_AUTH_SESSION_COOKIE" not in cls._settings or members_list_auth_session_cookie != cls._most_recent_yaml["community-group"][ "members-list" - ]["auth-session-cookie"] - or "MEMBERS_LIST_AUTH_SESSION_COOKIE" not in cls._settings, + ]["auth-session-cookie"] # noqa: COM812 ) if not MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: return set() @@ -621,10 +564,10 @@ def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str """ MEMBERS_LIST_ID_FORMAT_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "MEMBERS_LIST_ID_FORMAT" not in cls._settings or members_list_id_format != cls._most_recent_yaml["community-group"][ "members-list" - ]["id-format"] - or "MEMBERS_LIST_ID_FORMAT" not in cls._settings, + ]["id-format"] # noqa: COM812 ) if not MEMBERS_LIST_ID_FORMAT_CHANGED: return set() @@ -642,10 +585,10 @@ def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_pro """ PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "PING_COMMAND_EASTER_EGG_PROBABILITY" not in cls._settings or ping_command_easter_egg_probability != cls._most_recent_yaml["commands"][ "ping" - ]["easter-egg-probability"] - or "PING_COMMAND_EASTER_EGG_PROBABILITY" not in cls._settings, + ]["easter-egg-probability"] # noqa: COM812 ) if not PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: return set() @@ -665,10 +608,10 @@ def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) """ STATS_COMMAND_LOOKBACK_DAYS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "STATS_COMMAND_LOOKBACK_DAYS" not in cls._settings or stats_command_lookback_days != cls._most_recent_yaml["commands"][ "stats" - ]["lookback-days"] - or "STATS_COMMAND_LOOKBACK_DAYS" not in cls._settings, + ]["lookback-days"] # noqa: COM812 ) if not STATS_COMMAND_LOOKBACK_DAYS_CHANGED: return set() @@ -688,10 +631,10 @@ def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YA """ STATS_COMMAND_DISPLAYED_ROLES_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "STATS_COMMAND_DISPLAYED_ROLES" not in cls._settings or stats_command_displayed_roles != cls._most_recent_yaml["commands"][ "stats" - ]["displayed-roles"] - or "STATS_COMMAND_DISPLAYED_ROLES" not in cls._settings, + ]["displayed-roles"] # noqa: COM812 ) if not STATS_COMMAND_DISPLAYED_ROLES_CHANGED: return set() @@ -709,10 +652,10 @@ def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration """ STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "STRIKE_COMMAND_TIMEOUT_DURATION" not in cls._settings or strike_command_timeout_duration != cls._most_recent_yaml["commands"][ "strike" - ]["timeout-duration"] - or "STRIKE_COMMAND_TIMEOUT_DURATION" not in cls._settings, + ]["timeout-duration"] # noqa: COM812 ) if not STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: return set() @@ -730,10 +673,10 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man """ STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION" not in cls._settings or strike_performed_manually_warning_location != cls._most_recent_yaml["commands"][ "strike" - ]["performed-manually-warning-location"] - or "STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION" not in cls._settings, + ]["performed-manually-warning-location"] # noqa: COM812 ) if not STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: return set() @@ -753,8 +696,8 @@ def _reload_messages_locale_code(cls, messages_locale_code: YAML) -> set[str]: """ MESSAGES_LOCALE_CODE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None - or messages_locale_code != cls._most_recent_yaml["messages-locale-code"] - or "MESSAGES_LOCALE_CODE" not in cls._settings, + or "MESSAGES_LOCALE_CODE" not in cls._settings + or messages_locale_code != cls._most_recent_yaml["messages-locale-code"] # noqa: COM812 ) if not MESSAGES_LOCALE_CODE_CHANGED: return set() @@ -772,10 +715,10 @@ def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders """ SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "SEND_INTRODUCTION_REMINDERS_ENABLED" not in cls._settings or send_introduction_reminders_enabled != cls._most_recent_yaml["reminders"][ "send-introduction-reminders" - ]["enabled"] - or "SEND_INTRODUCTION_REMINDERS_ENABLED" not in cls._settings, + ]["enabled"] # noqa: COM812 ) if not SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: return set() @@ -797,10 +740,10 @@ def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_d """ SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "SEND_INTRODUCTION_REMINDERS_DELAY" not in cls._settings or send_introduction_reminders_delay != cls._most_recent_yaml["reminders"][ "send-introduction-reminders" - ]["delay"] - or "SEND_INTRODUCTION_REMINDERS_DELAY" not in cls._settings, + ]["delay"] # noqa: COM812 ) if not SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: return set() @@ -820,10 +763,10 @@ def _reload_send_introduction_reminders_interval(cls, send_introduction_reminder """ SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS" not in cls._settings or send_introduction_reminders_interval != cls._most_recent_yaml["reminders"][ "send-introduction-reminders" - ]["interval"] - or "SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS" not in cls._settings, + ]["interval"] # noqa: COM812 ) if not SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: return set() @@ -843,10 +786,10 @@ def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabl """ SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "SEND_GET_ROLES_REMINDERS_ENABLED" not in cls._settings or send_get_roles_reminders_enabled != cls._most_recent_yaml["reminders"][ "send-get-roles-reminders" - ]["enabled"] - or "SEND_GET_ROLES_REMINDERS_ENABLED" not in cls._settings, + ]["enabled"] # noqa: COM812 ) if not SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: return set() @@ -869,10 +812,10 @@ def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: """ SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "SEND_GET_ROLES_REMINDERS_DELAY" not in cls._settings or send_get_roles_reminders_delay != cls._most_recent_yaml["reminders"][ "send-get-roles-reminders" - ]["delay"] - or "SEND_GET_ROLES_REMINDERS_DELAY" not in cls._settings, + ]["delay"] # noqa: COM812 ) if not SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: return set() @@ -892,10 +835,10 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte """ SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None + or "SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS" not in cls._settings or send_get_roles_reminders_interval != cls._most_recent_yaml["reminders"][ "send-get-roles-reminders" - ]["interval"] - or "SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS" not in cls._settings, + ]["interval"] # noqa: COM812 ) if not SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: return set() @@ -906,226 +849,225 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte return {"reminders:send-get-roles-reminders:interval"} + @classmethod + def _get_scalar_value(cls, config_setting_name: str, yaml_settings_tree: YAML) -> str | None: # type: ignore[misc] # noqa: E501 + single_yaml_scalar_setting: YAML | None = yaml_settings_tree.get( + config_setting_name, + None, + ) -# TODO: Move to change_config cog + if single_yaml_scalar_setting is None: + return single_yaml_scalar_setting -def _get_scalar_config_setting_value(config_setting_name: str, yaml_settings_tree: YAML) -> str | None: # noqa: E501 - single_yaml_scalar_setting: YAML | None = yaml_settings_tree.get(config_setting_name, None) + CONFIG_SETTING_HAS_VALID_TYPE: Final[bool] = bool( + not single_yaml_scalar_setting.is_mapping() + and ( + single_yaml_scalar_setting.is_scalar() + or single_yaml_scalar_setting.is_sequence() + ) # noqa: COM812 + ) + if not CONFIG_SETTING_HAS_VALID_TYPE: + MAPPING_TYPE_MESSAGE: Final[str] = "Got config mapping when scalar expected." + raise RuntimeError(MAPPING_TYPE_MESSAGE) - if single_yaml_scalar_setting is None: - return single_yaml_scalar_setting + scalar_config_setting_value: object = single_yaml_scalar_setting.validator.to_yaml( + single_yaml_scalar_setting.data, + ) - CONFIG_SETTING_HAS_VALID_TYPE: Final[bool] = bool( - not single_yaml_scalar_setting.is_mapping() - and ( - single_yaml_scalar_setting.is_scalar() - or single_yaml_scalar_setting.is_sequence() - ) # noqa: COM812 - ) - if not CONFIG_SETTING_HAS_VALID_TYPE: - MAPPING_TYPE_MESSAGE: Final[str] = "Got config mapping when scalar expected." - raise RuntimeError(MAPPING_TYPE_MESSAGE) + if isinstance(scalar_config_setting_value, str): + if not single_yaml_scalar_setting.is_scalar(): + SCALAR_TYPE_MESSAGE: Final[str] = ( + "Got invalid config type when scalar expected." + ) + raise RuntimeError(SCALAR_TYPE_MESSAGE) - scalar_config_setting_value: object = single_yaml_scalar_setting.validator.to_yaml( - single_yaml_scalar_setting.data, - ) + return scalar_config_setting_value - if isinstance(scalar_config_setting_value, str): - if not single_yaml_scalar_setting.is_scalar(): - SCALAR_TYPE_MESSAGE: Final[str] = "Got invalid config type when scalar expected." - raise RuntimeError(SCALAR_TYPE_MESSAGE) + if isinstance(scalar_config_setting_value, Iterable): + if not single_yaml_scalar_setting.is_sequence(): + SEQUENCE_TYPE_MESSAGE: Final[str] = ( + "Got invalid config type when sequence expected." + ) + raise RuntimeError(SEQUENCE_TYPE_MESSAGE) - return scalar_config_setting_value + if not all(inner_value.is_scalar() for inner_value in single_yaml_scalar_setting): + ONLY_SCALAR_SEQUENCES_SUPPORTED_MESSAGE: Final[str] = ( + "Only sequences of scalars are currently supported " + "to be used in configuration." + ) + raise NotImplementedError(ONLY_SCALAR_SEQUENCES_SUPPORTED_MESSAGE) - if isinstance(scalar_config_setting_value, Iterable): - if not single_yaml_scalar_setting.is_sequence(): - SEQUENCE_TYPE_MESSAGE: Final[str] = ( - "Got invalid config type when sequence expected." - ) - raise RuntimeError(SEQUENCE_TYPE_MESSAGE) + return ",".join(scalar_config_setting_value) - if not all(inner_value.is_scalar() for inner_value in single_yaml_scalar_setting): - ONLY_SCALAR_SEQUENCES_SUPPORTED_MESSAGE: Final[str] = ( - "Only sequences of scalars are currently supported " - "to be used in configuration." - ) - raise NotImplementedError(ONLY_SCALAR_SEQUENCES_SUPPORTED_MESSAGE) + raise NotImplementedError - return ",".join(scalar_config_setting_value) + @classmethod + def _get_mapping_value(cls, partial_config_setting_name: str, partial_yaml_settings_tree: YAML) -> str | None: # type: ignore[misc] # noqa: E501 + if ":" not in partial_config_setting_name: + return cls._get_scalar_value( + partial_config_setting_name, + partial_yaml_settings_tree, + ) - raise NotImplementedError + key: str + remainder: str + key, _, remainder = partial_config_setting_name.partition(":") + single_yaml_mapping_setting: YAML | None = partial_yaml_settings_tree.get(key, None) -def _get_mapping_config_setting_value(partial_config_setting_name: str, partial_yaml_settings_tree: YAML) -> str | None: # noqa: E501 - if ":" not in partial_config_setting_name: - return _get_scalar_config_setting_value( - partial_config_setting_name, - partial_yaml_settings_tree, + YAML_CHILD_IS_MAPPING: Final[bool] = bool( + single_yaml_mapping_setting is not None + and single_yaml_mapping_setting.is_mapping(), ) + if YAML_CHILD_IS_MAPPING: + return cls._get_mapping_value(remainder, single_yaml_mapping_setting) - key: str - remainder: str - key, _, remainder = partial_config_setting_name.partition(":") - - single_yaml_mapping_setting: YAML | None = partial_yaml_settings_tree.get(key, None) - - if single_yaml_mapping_setting is not None and single_yaml_mapping_setting.is_mapping(): - return _get_mapping_config_setting_value(remainder, single_yaml_mapping_setting) - - return _get_scalar_config_setting_value( - partial_config_setting_name, - partial_yaml_settings_tree, - ) + return cls._get_scalar_value(partial_config_setting_name, partial_yaml_settings_tree) + @classmethod + def view_single_raw_value(cls, config_setting_name: str) -> str | None: + """Return the value of a single configuration setting from settings tree hierarchy.""" + current_yaml: YAML | None = cls._most_recent_yaml + if current_yaml is None: + YAML_NOT_LOADED_MESSAGE: Final[str] = ( + "Invalid state: Config YAML has not yet been loaded." + ) + raise RuntimeError(YAML_NOT_LOADED_MESSAGE) -def view_single_config_setting_value(config_setting_name: str, settings_accessor: SettingsAccessor) -> str | None: # noqa: E501 - """Return the value of a single configuration setting from the setting tree hierarchy.""" - # noinspection PyProtectedMember - current_yaml: YAML | None = settings_accessor._most_recent_yaml # noqa: SLF001 - if current_yaml is None: - YAML_NOT_LOADED_MESSAGE: Final[str] = ( - "Invalid state: Config YAML has not yet been loaded." - ) - raise RuntimeError(YAML_NOT_LOADED_MESSAGE) + return cls._get_mapping_value(config_setting_name, current_yaml) - # noinspection PyProtectedMember - return _get_mapping_config_setting_value(config_setting_name, current_yaml) + @classmethod + def _set_scalar_or_sequence_value(cls, config_setting_name: str, new_config_setting_value: str, yaml_settings_tree: YAML) -> YAML: # type: ignore[misc] # noqa: E501 + if config_setting_name not in yaml_settings_tree: + yaml_settings_tree[config_setting_name] = new_config_setting_value + return yaml_settings_tree + + if yaml_settings_tree[config_setting_name].is_mapping(): + INVALID_MAPPING_CONFIG_TYPE_MESSAGE: Final[str] = ( + "Got incongruent YAML object. Expected sequence or scalar, got mapping." + ) + raise TypeError(INVALID_MAPPING_CONFIG_TYPE_MESSAGE) + if yaml_settings_tree[config_setting_name].is_scalar(): + yaml_settings_tree[config_setting_name] = new_config_setting_value + return yaml_settings_tree -def _set_scalar_or_sequence_config_setting_value(config_setting_name: str, new_config_setting_value: str, yaml_settings_tree: YAML) -> YAML: # noqa: E501 - if config_setting_name not in yaml_settings_tree: - yaml_settings_tree[config_setting_name] = new_config_setting_value - return yaml_settings_tree + if yaml_settings_tree[config_setting_name].is_sequence(): + yaml_settings_tree[config_setting_name] = [ + sequence_value.strip() + for sequence_value + in new_config_setting_value.strip().split(",") + ] + return yaml_settings_tree - if yaml_settings_tree[config_setting_name].is_mapping(): - INVALID_MAPPING_CONFIG_TYPE_MESSAGE: Final[str] = ( - "Got incongruent YAML object. Expected sequence or scalar, got mapping." + UNKNOWN_CONFIG_TYPE_MESSAGE: Final[str] = ( + "Unknown YAML object type. Expected sequence or scalar." ) - raise TypeError(INVALID_MAPPING_CONFIG_TYPE_MESSAGE) + raise RuntimeError(UNKNOWN_CONFIG_TYPE_MESSAGE) - if yaml_settings_tree[config_setting_name].is_scalar(): - yaml_settings_tree[config_setting_name] = new_config_setting_value - return yaml_settings_tree + @classmethod + def _set_mapping_value(cls, partial_config_setting_name: str, new_config_setting_value: str, partial_yaml_settings_tree: YAML) -> YAML: # type: ignore[misc] # noqa: E501 + if ":" not in partial_config_setting_name: + return cls._set_scalar_or_sequence_value( + partial_config_setting_name, + new_config_setting_value, + partial_yaml_settings_tree, + ) - if yaml_settings_tree[config_setting_name].is_sequence(): - yaml_settings_tree[config_setting_name] = [ - sequence_value.strip() - for sequence_value - in new_config_setting_value.strip().split(",") - ] - return yaml_settings_tree + key: str + remainder: str + key, _, remainder = partial_config_setting_name.partition(":") - UNKNOWN_CONFIG_TYPE_MESSAGE: Final[str] = ( - "Unknown YAML object type. Expected sequence or scalar." - ) - raise RuntimeError(UNKNOWN_CONFIG_TYPE_MESSAGE) + if key not in partial_yaml_settings_tree: + partial_yaml_settings_tree[key] = cls._set_required_value_from_validator( + remainder if ":" in partial_config_setting_name else None, + new_config_setting_value, + partial_yaml_settings_tree.validator.get_validator(key), + ) + return partial_yaml_settings_tree + if partial_yaml_settings_tree[key].is_mapping(): + partial_yaml_settings_tree[key] = cls._set_mapping_value( + remainder, + new_config_setting_value, + partial_yaml_settings_tree[key], + ) + return partial_yaml_settings_tree -def _set_mapping_config_setting_value(partial_config_setting_name: str, new_config_setting_value: str, partial_yaml_settings_tree: YAML) -> YAML: # noqa: E501 - if ":" not in partial_config_setting_name: - return _set_scalar_or_sequence_config_setting_value( + return cls._set_scalar_or_sequence_value( partial_config_setting_name, new_config_setting_value, partial_yaml_settings_tree, ) - key: str - remainder: str - key, _, remainder = partial_config_setting_name.partition(":") - - if key not in partial_yaml_settings_tree: - partial_yaml_settings_tree[key] = _set_required_value_from_validator_config( - remainder if ":" in partial_config_setting_name else None, - new_config_setting_value, - partial_yaml_settings_tree.validator.get_validator(key), + @classmethod + def _set_required_value_from_validator(cls, partial_config_setting_name: str | None, new_config_setting_value: str, yaml_validator: strictyaml.Validator) -> "NestedMapping | str | Sequence[str]": # type: ignore[misc] # noqa: E501 + VALIDATOR_IS_SCALAR_TYPE: Final[bool] = bool( + isinstance(yaml_validator, strictyaml.ScalarValidator) + and (partial_config_setting_name is None or ":" not in partial_config_setting_name) # noqa: COM812 ) - return partial_yaml_settings_tree + if VALIDATOR_IS_SCALAR_TYPE: + return new_config_setting_value - if partial_yaml_settings_tree[key].is_mapping(): - partial_yaml_settings_tree[key] = _set_mapping_config_setting_value( - remainder, - new_config_setting_value, - partial_yaml_settings_tree[key], + VALIDATOR_IS_SEQUENCE_TYPE: Final[bool] = bool( + isinstance(yaml_validator, strictyaml.validators.SeqValidator) + and (partial_config_setting_name is None or ":" not in partial_config_setting_name) # noqa: COM812 ) - return partial_yaml_settings_tree - - return _set_scalar_or_sequence_config_setting_value( - partial_config_setting_name, - new_config_setting_value, - partial_yaml_settings_tree, - ) - - -def _set_required_value_from_validator_config(partial_config_setting_name: str | None, new_config_setting_value: str, yaml_validator: strictyaml.Validator) -> "NestedMapping | str | Sequence[str]": # noqa: E501 - VALIDATOR_IS_SCALAR_TYPE: Final[bool] = bool( - isinstance(yaml_validator, strictyaml.ScalarValidator) - and (partial_config_setting_name is None or ":" not in partial_config_setting_name) # noqa: COM812 - ) - if VALIDATOR_IS_SCALAR_TYPE: - return new_config_setting_value - - VALIDATOR_IS_SEQUENCE_TYPE: Final[bool] = bool( - isinstance(yaml_validator, strictyaml.validators.SeqValidator) - and (partial_config_setting_name is None or ":" not in partial_config_setting_name) # noqa: COM812 - ) - if VALIDATOR_IS_SEQUENCE_TYPE: - return [ - sequence_value.strip() - for sequence_value - in new_config_setting_value.strip().split(",") - ] - - VALIDATOR_IS_MAPPING_TYPE: Final[bool] = bool( - isinstance(yaml_validator, strictyaml.validators.MapValidator) - and hasattr(yaml_validator, "_required_keys") - and partial_config_setting_name is not None # noqa: COM812 - ) - if VALIDATOR_IS_MAPPING_TYPE: - key: str - remainder: str - key, _, remainder = partial_config_setting_name.partition(":") # type: ignore[union-attr] - - # noinspection PyProtectedMember,PyUnresolvedReferences - if set(yaml_validator._required_keys) - {key}: # noqa: SLF001 - raise ChangingSettingWithRequiredSiblingError - - # noinspection PyUnresolvedReferences - return { - key: _set_required_value_from_validator_config( # type: ignore[dict-item] - remainder if ":" in partial_config_setting_name else None, # type: ignore[operator] - new_config_setting_value, - yaml_validator.get_validator(key), - ), - } - - UNKNOWN_CONFIG_TYPE_MESSAGE: Final[str] = ( - "Unknown YAML validator type. Expected mapping, sequence or scalar." - ) - raise RuntimeError(UNKNOWN_CONFIG_TYPE_MESSAGE) - + if VALIDATOR_IS_SEQUENCE_TYPE: + return [ + sequence_value.strip() + for sequence_value + in new_config_setting_value.strip().split(",") + ] -def assign_single_config_setting_value(config_setting_name: str, new_config_setting_value: str, settings_accessor: SettingsAccessor) -> None: # noqa: E501 - """Set the value of a single configuration setting within the setting tree hierarchy.""" - # noinspection PyProtectedMember - current_yaml: YAML | None = settings_accessor._most_recent_yaml # noqa: SLF001 - if current_yaml is None: - YAML_NOT_LOADED_MESSAGE: Final[str] = ( - "Invalid state: Config YAML has not yet been loaded." + VALIDATOR_IS_MAPPING_TYPE: Final[bool] = bool( + isinstance(yaml_validator, strictyaml.validators.MapValidator) + and hasattr(yaml_validator, "_required_keys") + and partial_config_setting_name is not None # noqa: COM812 ) - raise RuntimeError(YAML_NOT_LOADED_MESSAGE) + if VALIDATOR_IS_MAPPING_TYPE: + key: str + remainder: str + key, _, remainder = partial_config_setting_name.partition(":") # type: ignore[union-attr] + + # noinspection PyProtectedMember,PyUnresolvedReferences + if set(yaml_validator._required_keys) - {key}: # noqa: SLF001 + raise ChangingSettingWithRequiredSiblingError + + # noinspection PyUnresolvedReferences + return { + key: cls._set_required_value_from_validator( # type: ignore[dict-item] + remainder if ":" in partial_config_setting_name else None, # type: ignore[operator] + new_config_setting_value, + yaml_validator.get_validator(key), + ), + } - config_setting_error: ChangingSettingWithRequiredSiblingError - try: - settings_accessor = _set_mapping_config_setting_value( - config_setting_name, - new_config_setting_value, - current_yaml, + UNKNOWN_CONFIG_TYPE_MESSAGE: Final[str] = ( + "Unknown YAML validator type. Expected mapping, sequence or scalar." ) - except ChangingSettingWithRequiredSiblingError as config_setting_error: - raise type(config_setting_error)( - config_setting_name=config_setting_name, - ) from config_setting_error + raise RuntimeError(UNKNOWN_CONFIG_TYPE_MESSAGE) - print(settings_accessor.as_yaml()) + @classmethod + async def assign_single_raw_value(cls, config_setting_name: str, new_config_setting_value: str) -> None: # noqa: E501 + """Set the value of a single configuration setting within settings tree hierarchy.""" + current_yaml: YAML | None = cls._most_recent_yaml + if current_yaml is None: + YAML_NOT_LOADED_MESSAGE: Final[str] = ( + "Invalid state: Config YAML has not yet been loaded." + ) + raise RuntimeError(YAML_NOT_LOADED_MESSAGE) + + config_setting_error: ChangingSettingWithRequiredSiblingError + try: + current_yaml = cls._set_mapping_value( + config_setting_name, + new_config_setting_value, + current_yaml, + ) + except ChangingSettingWithRequiredSiblingError as config_setting_error: + raise type(config_setting_error)( + config_setting_name=config_setting_name, + ) from config_setting_error - # TODO: save yaml + await (await utils.get_settings_file_path()).write_text(current_yaml.as_yaml()) diff --git a/config/_settings/utils.py b/config/_settings/utils.py index 574dfb600..57f490256 100644 --- a/config/_settings/utils.py +++ b/config/_settings/utils.py @@ -1,9 +1,67 @@ from collections.abc import Sequence -__all__: Sequence[str] = ("is_running_in_async",) +__all__: Sequence[str] = ("is_running_in_async", "get_settings_file_path") import asyncio +import logging +import os +from logging import Logger +from typing import Final + +from aiopath import AsyncPath + +from config.constants import PROJECT_ROOT + +logger: Final[Logger] = logging.getLogger("TeX-Bot") + + +async def get_settings_file_path() -> AsyncPath: + settings_file_not_found_message: str = ( + "No settings file was found. " + "Please make sure you have created a `tex-bot-deployment.yaml` file." + ) + + raw_settings_file_path: str | None = ( + os.getenv("TEX_BOT_SETTINGS_FILE_PATH", None) + or os.getenv("TEX_BOT_SETTINGS_FILE", None) + or os.getenv("TEX_BOT_SETTINGS_PATH", None) + or os.getenv("TEX_BOT_SETTINGS", None) + or os.getenv("TEX_BOT_CONFIG_FILE_PATH", None) + or os.getenv("TEX_BOT_CONFIG_FILE", None) + or os.getenv("TEX_BOT_CONFIG_PATH", None) + or os.getenv("TEX_BOT_CONFIG", None) + or os.getenv("TEX_BOT_DEPLOYMENT_FILE_PATH", None) + or os.getenv("TEX_BOT_DEPLOYMENT_FILE", None) + or os.getenv("TEX_BOT_DEPLOYMENT_PATH", None) + or os.getenv("TEX_BOT_DEPLOYMENT", None) + ) + + if raw_settings_file_path: + settings_file_not_found_message = ( + "A path to the settings file location was provided by environment variable, " + "however this path does not refer to an existing file." + ) + else: + logger.debug( + ( + "Settings file location not supplied by environment variable, " + "falling back to `Tex-Bot-deployment.yaml`." + ), + ) + raw_settings_file_path = "tex-bot-deployment.yaml" + if not await (AsyncPath(PROJECT_ROOT) / raw_settings_file_path).exists(): + raw_settings_file_path = "tex-bot-settings.yaml" + + if not await (AsyncPath(PROJECT_ROOT) / raw_settings_file_path).exists(): + raw_settings_file_path = "tex-bot-config.yaml" + + settings_file_path: AsyncPath = AsyncPath(raw_settings_file_path) + + if not await settings_file_path.is_file(): + raise FileNotFoundError(settings_file_not_found_message) + + return settings_file_path def is_running_in_async() -> bool: diff --git a/config/constants.py b/config/constants.py index e59c0cd62..758df6bfe 100644 --- a/config/constants.py +++ b/config/constants.py @@ -129,7 +129,6 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY: Final[str] = "40h" DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL: Final[str] = "6h" -# noinspection PyProtectedMember CONFIG_SETTINGS_HELPS: Mapping[str, ConfigSettingHelp] = { "logging:console:log-level": ConfigSettingHelp( description=( diff --git a/poetry.lock b/poetry.lock index 4e69ea8fd..4970da44e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,22 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "aiofile" +version = "3.8.8" +description = "Asynchronous file operations." +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "aiofile-3.8.8-py3-none-any.whl", hash = "sha256:41e8845cce055779cd77713d949a339deb012eab605b857765e8f8e52a5ed811"}, + {file = "aiofile-3.8.8.tar.gz", hash = "sha256:41f3dc40bd730459d58610476e82e5efb2f84ae6e9fa088a9545385d838b8a43"}, +] + +[package.dependencies] +caio = ">=0.9.0,<0.10.0" + +[package.extras] +develop = ["aiomisc-pytest", "coveralls", "pytest", "pytest-cov", "pytest-rst"] + [[package]] name = "aiohttp" version = "3.9.5" @@ -95,6 +112,21 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns", "brotlicffi"] +[[package]] +name = "aiopath" +version = "0.7.7" +description = "📁 Async pathlib for Python" +optional = false +python-versions = ">=3.12" +files = [ + {file = "aiopath-0.7.7-py2.py3-none-any.whl", hash = "sha256:cd5d18de8ede167e1db659f02ee448fe085f923cb8e194407ccc568bffc4fe4e"}, + {file = "aiopath-0.7.7.tar.gz", hash = "sha256:ad4b9d09ae08ddf6d39dd06e7b0a353939e89528da571c0cd4f3fe071aefad4f"}, +] + +[package.dependencies] +aiofile = ">=3.8.8,<4" +anyio = ">=4.0.0,<5" + [[package]] name = "aiosignal" version = "1.3.1" @@ -109,6 +141,26 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "application-properties" version = "0.8.2" @@ -195,6 +247,30 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "caio" +version = "0.9.17" +description = "Asynchronous file IO for Linux MacOS or Windows." +optional = false +python-versions = "<4,>=3.7" +files = [ + {file = "caio-0.9.17-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3f69395fdd45c115b2ef59732e3c8664722a2b51de2d6eedb3d354b2f5f3be3c"}, + {file = "caio-0.9.17-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3028b746e9ec7f6d6ebb386a7fd8caf0eebed5d6e6b4f18c8ef25861934b1673"}, + {file = "caio-0.9.17-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:079730a353bbde03796fab681e969472eace09ffbe5000e584868a7fe389ba6f"}, + {file = "caio-0.9.17-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:549caa51b475877fe32856a26fe937366ae7a1c23a9727005b441db9abb12bcc"}, + {file = "caio-0.9.17-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0ddb253b145a53ecca76381677ce465bc5efeaecb6aaf493fac43ae79659f0fb"}, + {file = "caio-0.9.17-cp312-cp312-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e320b0ea371c810359934f8e8fe81777c493cc5fb4d41de44277cbe7336e74"}, + {file = "caio-0.9.17-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a39a49e279f82aa022f0786339d45d9550b5aa3e46eec7d08e0f351c503df0a5"}, + {file = "caio-0.9.17-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e96925b9f15f43e6ef1d42a83edfd937eb11a984cb6ef7c10527e963595497"}, + {file = "caio-0.9.17-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fca916240597005d2b734f1442fa3c3cfb612bf46e0978b5232e5492a371de38"}, + {file = "caio-0.9.17-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40bd0afbd3491d1e407bcf74e3a9e9cc67a7f290ed29518325194184d63cc2b6"}, + {file = "caio-0.9.17-py3-none-any.whl", hash = "sha256:c55d4dc6b3a36f93237ecd6360e1c131c3808bc47d4191a130148a99b80bb311"}, + {file = "caio-0.9.17.tar.gz", hash = "sha256:8f30511526814d961aeef389ea6885273abe6c655f1e08abbadb95d12fdd9b4f"}, +] + +[package.extras] +develop = ["aiomisc-pytest", "pytest", "pytest-cov"] + [[package]] name = "ccft-pymarkdown" version = "1.1.2" @@ -1480,7 +1556,6 @@ 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"}, @@ -1599,6 +1674,17 @@ files = [ {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "soupsieve" version = "2.5" @@ -1884,4 +1970,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "44ce0b84cbfab8b8953534c8702fbb5e5e50be9337be6b93cf923ed44d26090d" +content-hash = "e99e50d35eeb083936eaaf70a8509f57ec7bc7f7a6998e50bd773ba1b50fe816" diff --git a/pyproject.toml b/pyproject.toml index 61e1c3d9e..9c6814465 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ asyncstdlib = "~3.12" setuptools = "^70.0" strictyaml = "^1.7.3" python-slugify = "^8.0" +aiopath = "^0.7" [tool.poetry.group.dev.dependencies] pre-commit = "^3.7" @@ -88,6 +89,7 @@ module = [ "strictyaml", "strictyaml.exceptions", "strictyaml.yamllocation", + "aiopath", ] ignore_missing_imports = true From 4aae5cdadbd8409f64672c9a82300347f2857863 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 10 Jun 2024 02:06:16 +0100 Subject: [PATCH 037/128] Update regex functions used & add config value autocompletes --- cogs/archive.py | 10 +- cogs/change_config.py | 194 +++++++++++++++++- cogs/edit_message.py | 14 +- cogs/induct.py | 4 +- cogs/make_member.py | 2 +- cogs/remind_me.py | 16 +- cogs/stats.py | 2 +- cogs/strike.py | 4 +- config/_messages/__init__.py | 2 +- config/_settings/__init__.py | 2 +- config/_settings/_yaml/__init__.py | 2 +- .../_yaml/custom_scalar_validators.py | 4 +- config/constants.py | 2 +- db/core/models/__init__.py | 2 +- db/core/models/utils.py | 2 +- tests/test_utils.py | 2 +- utils/tex_bot.py | 2 +- utils/tex_bot_base_cog.py | 4 +- 18 files changed, 220 insertions(+), 50 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index f309d751d..20b2a9a1f 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -28,7 +28,7 @@ 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 | str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable categories. @@ -39,6 +39,9 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> set[dis return set() try: + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + return set() + main_guild: discord.Guild = ctx.bot.main_guild interaction_user: discord.Member = await ctx.bot.get_main_guild_member( ctx.interaction.user, @@ -46,9 +49,6 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> set[dis 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 @@ -88,7 +88,7 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> 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): + 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.", diff --git a/cogs/change_config.py b/cogs/change_config.py index 93f7699ee..4e23c1c8c 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -5,6 +5,9 @@ __all__: Sequence[str] = ("ConfigChangeCommandsCog",) +import itertools +import re +import urllib.parse from typing import Final import discord @@ -12,7 +15,8 @@ import config from config import CONFIG_SETTINGS_HELPS, ConfigSettingHelp, LogLevels -from exceptions import ChangingSettingWithRequiredSiblingError +from exceptions import ChangingSettingWithRequiredSiblingError, DiscordMemberNotInMainGuildError +from exceptions.base import BaseDoesNotExistError from utils import ( CommandChecks, TeXBotApplicationContext, @@ -30,19 +34,18 @@ class ConfigChangeCommandsCog(TeXBotBaseCog): ) @staticmethod - async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice]: # noqa: E501 + async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # noqa: E501 """Autocomplete callable that generates the set of available settings names.""" if not ctx.interaction.user: return set() - if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + try: + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + return set() + except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() - return { - discord.OptionChoice(name=setting_name, value=setting_name) - for setting_name - in config.CONFIG_SETTINGS_HELPS - } + return set(config.CONFIG_SETTINGS_HELPS) @staticmethod async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # noqa: E501 @@ -53,12 +56,170 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext if not HAS_CONTEXT: return set() - if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): # type: ignore[arg-type] + try: + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): # type: ignore[arg-type] + return set() + except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): + return set() + + setting_name: str = ctx.options["setting"] + + if "token" in setting_name or "cookie" in setting_name or "secret" in setting_name: return set() - if ":log-level" in ctx.options["setting"]: + if ":log-level" in setting_name: return {log_level.value for log_level in LogLevels} + if "discord" in setting_name and ":webhook-url" in setting_name: + return {"https://discord.com/api/webhooks/"} + + try: + main_guild: discord.Guild = ctx.bot.main_guild + except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): + return set() + + if "community" in setting_name: + SIMPLIFIED_FULL_NAME: Final[str] = ( + main_guild.name.strip().removeprefix("the").removeprefix("The").strip() + ) + + if ":full-name" in setting_name: + return { + main_guild.name.strip(), + main_guild.name.strip().title(), + SIMPLIFIED_FULL_NAME.split()[0].rsplit("'")[0], + SIMPLIFIED_FULL_NAME.split()[0].rsplit("'")[0].capitalize(), + } + + if ":short-name" in setting_name: + return { + "".join(word[0].upper() for word in SIMPLIFIED_FULL_NAME.split()), + "".join(word[0].lower() for word in SIMPLIFIED_FULL_NAME.split()), + } + + if re.search(r":links:[^:]+\Z", setting_name) or setting_name.endswith(":url"): + if "purchase-membership" in setting_name or "membership-perks" in setting_name: + return { + "https://", + "https://www.guildofstudents.com/studentgroups/societies/", + "https://www.guildofstudents.com/organisation/", + } + + if "document" in setting_name: + return { + "https://", + "https://drive.google.com/file/d/", + "https://docs.google.com/document/d/", + "https://onedrive.live.com/edit.aspx?resid=", + "https://1drv.ms/p/", + } | { + f"https://{domain}.com/{path}" + for domain, path + in itertools.product( + ("github", "raw.githubusercontent"), + (f"{urllib.parse.quote(ctx.bot.group_short_name)}/", ""), + ) + } | { + f"https://{subdomain}dropbox{domain_suffix}.com/{path}" + for subdomain, domain_suffix, path + in itertools.product( + ("dl.", ""), + ("usercontent", ""), + ("shared/", "", "s/", "scl/fi/"), + ) + } + + return {"https://"} + + if "members-list:id-format" in setting_name: + return ( + {r"\A[a-z0-9]{" + str(2 ** id_length) + r"}\Z" for id_length in range(2, 9)} + | {r"\A[A-F0-9]{" + str(2 ** id_length) + r"}\Z" for id_length in range(2, 9)} + | {r"\A[0-9]{" + str(2 ** id_length) + r"}\Z" for id_length in range(2, 9)} + ) + + if "probability" in setting_name: + return { + "0", + "0.01", + "0.025", + "0.05", + "0.1", + "0.125", + "0.15", + "0.20", + "0.25", + "0.4", + "0.45", + "0.5", + "0.6", + "0.65", + "0.7", + "0.75", + "0.8", + "0.85", + "0.9", + "0.95", + "0.975", + "0.99", + "0.999", + "1", + } + + if ":lookback-days" in setting_name: + return { + "5", + "7", + "10", + "20", + "25", + "27", + "28", + "30", + "31", + "50", + "75", + "100", + "150", + "200", + "250", + "500", + "750", + "1000", + "1250", + "1500", + "1826", + } + + if ":displayed-roles" in setting_name: + return { + "Committee,Member,Guest", + ( + "Foundation Year,First Year,Second Year,Final Year,Year In Industry," + "Year Abroad,PGT,PGR,Alumnus/Alumna,Postdoc" + ), + } + + try: + interaction_user: discord.Member = await ctx.bot.get_main_guild_member( + ctx.interaction.user, + ) + except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): + return set() + + if ":performed-manually-warning-location" in setting_name: + return ( + {"DM"} + | { + channel.name + for channel + in main_guild.text_channels + if channel.permissions_for(interaction_user).is_superset( + discord.Permissions(send_messages=True) + ) + } + ) + return set() # TODO: extra autocomplete suggestions @change_config.command( @@ -214,6 +375,10 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ) return + previous_config_setting_value: str | None = config.view_single_config_setting_value( + config_setting_name, + ) + # TODO: Are you sure, if config has no default yaml_error: StrictYAMLError @@ -254,6 +419,13 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n config_setting_name, ) + if changed_config_setting_value == previous_config_setting_value: + await ctx.respond( + "No changes made. Provided value was the same as the previous value.", + ephemeral=True, + ) + return + if isinstance(changed_config_setting_value, str): changed_config_setting_value = changed_config_setting_value.strip() @@ -283,3 +455,5 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ), ephemeral=True, ) + + # TODO: Command to unset value (if it is optional) diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 8bddf88b0..170847d4b 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -24,7 +24,7 @@ class EditMessageCommandCog(TeXBotBaseCog): """Cog class that defines the "/editmessage" 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 | str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable channels. @@ -35,15 +35,11 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[ return set() try: - interaction_user: discord.Member = await ctx.bot.get_main_guild_member( - ctx.interaction.user, - ) + if not await ctx.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 @@ -88,7 +84,7 @@ async def edit_message(self, ctx: TeXBotApplicationContext, str_channel_id: str, # 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 - 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 +93,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.", diff --git a/cogs/induct.py b/cogs/induct.py index a4e3800c4..a07241ccb 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -289,7 +289,7 @@ class InductCommandCog(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 | str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable members. @@ -310,7 +310,7 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discor else: members = {member for member in members if guest_role not in member.roles} - 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 diff --git a/cogs/make_member.py b/cogs/make_member.py index 2ecc82cc3..09ffc3e7a 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -116,7 +116,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) ) return - if not re.match(settings["MEMBERS_LIST_ID_FORMAT"], group_member_id): + if not re.fullmatch(settings["MEMBERS_LIST_ID_FORMAT"], group_member_id): await self.command_send_error( ctx, message=( diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 705d54e7d..34a825d66 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -69,7 +69,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 +89,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 +102,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 +124,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 +142,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 +154,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")):]) diff --git a/cogs/stats.py b/cogs/stats.py index b85b1ae11..31b7d0e3b 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -190,7 +190,7 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str 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.", diff --git a/cogs/strike.py b/cogs/strike.py index ebb3c97ee..e5c043e85 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -721,7 +721,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 strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable members. @@ -735,7 +735,7 @@ async def strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set members: set[discord.Member] = {member for member in 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 diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index 26401e3da..a6ab1e61d 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -30,7 +30,7 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) - if not re.match(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item): + if not re.fullmatch(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item): raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) if item not in self._messages: diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 267deb132..3c8faaddf 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -59,7 +59,7 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) - if not re.match(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item): + if not re.fullmatch(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item): raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) if self._most_recent_yaml is None: diff --git a/config/_settings/_yaml/__init__.py b/config/_settings/_yaml/__init__.py index db0c269b5..d6de98ddc 100644 --- a/config/_settings/_yaml/__init__.py +++ b/config/_settings/_yaml/__init__.py @@ -160,7 +160,7 @@ SlugKeyMap( { strictyaml.Optional("lookback-days", default=DEFAULT_STATS_COMMAND_LOOKBACK_DAYS): ( # noqa: E501 - strictyaml.Float() + strictyaml.Float() # TODO: Change to bounded float (min 5, max 1826) ), strictyaml.Optional("displayed-roles", default=DEFAULT_STATS_COMMAND_DISPLAYED_ROLES): ( # noqa: E501 strictyaml.UniqueSeq(strictyaml.Str()) diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index 6e9b14140..cf5f9e3ac 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -103,7 +103,7 @@ class DiscordSnowflakeValidator(strictyaml.Int): # type: ignore[misc] def validate_scalar(self, chunk: YAMLChunk) -> int: # type: ignore[misc] val: int = super().validate_scalar(chunk) - if not re.match(r"\A\d{17,20}\Z", str(val)): + if not re.fullmatch(r"\A\d{17,20}\Z", str(val)): chunk.expecting_but_found("when expecting a Discord snowflake ID") raise RuntimeError @@ -115,7 +115,7 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] ( (strictyaml_utils.is_string(data) or isinstance(data, int)) and strictyaml_utils.is_integer(str(data)) - and re.match(r"\A\d{17,20}\Z", str(data)) + and re.fullmatch(r"\A\d{17,20}\Z", str(data)) ), ) if not DATA_IS_VALID: diff --git a/config/constants.py b/config/constants.py index 758df6bfe..6e606785b 100644 --- a/config/constants.py +++ b/config/constants.py @@ -78,7 +78,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non type_value.lower().replace("discord", "Discord").replace( "id", "ID", - ).replace("url", "URL").strip(".") + ).replace("url", "URL").replace("dm", "DM").strip(".") }{f" (see <{info_link}>)" if info_link else ""}." ) diff --git a/db/core/models/__init__.py b/db/core/models/__init__.py index 7127cabcb..33bed40a6 100644 --- a/db/core/models/__init__.py +++ b/db/core/models/__init__.py @@ -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/utils.py b/db/core/models/utils.py index 454bd0581..8a0229c9d 100644 --- a/db/core/models/utils.py +++ b/db/core/models/utils.py @@ -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/tests/test_utils.py b/tests/test_utils.py index 847ccd155..d43b8616f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -91,7 +91,7 @@ def test_url_generates() -> None: DISCORD_BOT_APPLICATION_ID, DISCORD_MAIN_GUILD_ID, ) - assert re.match( + assert re.fullmatch( f"https://discord.com/.*={DISCORD_BOT_APPLICATION_ID}.*={DISCORD_MAIN_GUILD_ID}", invite_url, ) diff --git a/utils/tex_bot.py b/utils/tex_bot.py index dcb03a786..997d3322b 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -468,7 +468,7 @@ 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." ) diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 0d18cd27d..e71aa8e53 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -149,7 +149,7 @@ 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 | str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable channels. @@ -171,7 +171,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[ 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 From 370a7892ec903827db6fd97b363ad4e87c7e1fbe Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 10 Jun 2024 02:33:58 +0100 Subject: [PATCH 038/128] Fix typing & linting errors --- cogs/archive.py | 2 +- cogs/change_config.py | 136 ++++++++++++++++++----------------- cogs/edit_message.py | 2 +- cogs/induct.py | 2 +- cogs/strike.py | 2 +- config/_messages/__init__.py | 5 +- config/_settings/__init__.py | 5 +- poetry.lock | 8 +-- utils/tex_bot_base_cog.py | 2 +- 9 files changed, 88 insertions(+), 76 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index 20b2a9a1f..a480a0484 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -28,7 +28,7 @@ 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 | str]: # 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. diff --git a/cogs/change_config.py b/cogs/change_config.py index 4e23c1c8c..0ff00a197 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -15,7 +15,10 @@ import config from config import CONFIG_SETTINGS_HELPS, ConfigSettingHelp, LogLevels -from exceptions import ChangingSettingWithRequiredSiblingError, DiscordMemberNotInMainGuildError +from exceptions import ( + ChangingSettingWithRequiredSiblingError, + DiscordMemberNotInMainGuildError, +) from exceptions.base import BaseDoesNotExistError from utils import ( CommandChecks, @@ -34,7 +37,7 @@ class ConfigChangeCommandsCog(TeXBotBaseCog): ) @staticmethod - async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # noqa: E501 + async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice] | set[str]: # noqa: E501 """Autocomplete callable that generates the set of available settings names.""" if not ctx.interaction.user: return set() @@ -48,7 +51,7 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set return set(config.CONFIG_SETTINGS_HELPS) @staticmethod - async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # noqa: E501 + async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice] | set[str]: # noqa: C901,PLR0911,PLR0912,E501 """Autocomplete callable that generates example values for a configuration setting.""" HAS_CONTEXT: Final[bool] = bool( ctx.interaction.user and "setting" in ctx.options and ctx.options["setting"], @@ -73,64 +76,6 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext if "discord" in setting_name and ":webhook-url" in setting_name: return {"https://discord.com/api/webhooks/"} - try: - main_guild: discord.Guild = ctx.bot.main_guild - except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): - return set() - - if "community" in setting_name: - SIMPLIFIED_FULL_NAME: Final[str] = ( - main_guild.name.strip().removeprefix("the").removeprefix("The").strip() - ) - - if ":full-name" in setting_name: - return { - main_guild.name.strip(), - main_guild.name.strip().title(), - SIMPLIFIED_FULL_NAME.split()[0].rsplit("'")[0], - SIMPLIFIED_FULL_NAME.split()[0].rsplit("'")[0].capitalize(), - } - - if ":short-name" in setting_name: - return { - "".join(word[0].upper() for word in SIMPLIFIED_FULL_NAME.split()), - "".join(word[0].lower() for word in SIMPLIFIED_FULL_NAME.split()), - } - - if re.search(r":links:[^:]+\Z", setting_name) or setting_name.endswith(":url"): - if "purchase-membership" in setting_name or "membership-perks" in setting_name: - return { - "https://", - "https://www.guildofstudents.com/studentgroups/societies/", - "https://www.guildofstudents.com/organisation/", - } - - if "document" in setting_name: - return { - "https://", - "https://drive.google.com/file/d/", - "https://docs.google.com/document/d/", - "https://onedrive.live.com/edit.aspx?resid=", - "https://1drv.ms/p/", - } | { - f"https://{domain}.com/{path}" - for domain, path - in itertools.product( - ("github", "raw.githubusercontent"), - (f"{urllib.parse.quote(ctx.bot.group_short_name)}/", ""), - ) - } | { - f"https://{subdomain}dropbox{domain_suffix}.com/{path}" - for subdomain, domain_suffix, path - in itertools.product( - ("dl.", ""), - ("usercontent", ""), - ("shared/", "", "s/", "scl/fi/"), - ) - } - - return {"https://"} - if "members-list:id-format" in setting_name: return ( {r"\A[a-z0-9]{" + str(2 ** id_length) + r"}\Z" for id_length in range(2, 9)} @@ -200,9 +145,70 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext ), } + if "locale-code" in setting_name: + raise NotImplementedError # TODO: retrieve from constant + + if re.search(r":links:[^:]+\Z", setting_name) or setting_name.endswith(":url"): + if "purchase-membership" in setting_name or "membership-perks" in setting_name: + return { + "https://", + "https://www.guildofstudents.com/studentgroups/societies/", + "https://www.guildofstudents.com/organisation/", + } + + if "document" in setting_name: + return { + "https://", + "https://drive.google.com/file/d/", + "https://docs.google.com/document/d/", + "https://onedrive.live.com/edit.aspx?resid=", + "https://1drv.ms/p/", + } | { + f"https://{domain}.com/{path}" + for domain, path + in itertools.product( + ("github", "raw.githubusercontent"), + (f"{urllib.parse.quote(ctx.bot.group_short_name)}/", ""), + ) + } | { + f"https://{subdomain}dropbox{domain_suffix}.com/{path}" + for subdomain, domain_suffix, path + in itertools.product( + ("dl.", ""), + ("usercontent", ""), + ("shared/", "", "s/", "scl/fi/"), + ) + } + + return {"https://"} + + try: + main_guild: discord.Guild = ctx.bot.main_guild + except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): + return set() + + if "community" in setting_name: + SIMPLIFIED_FULL_NAME: Final[str] = ( + main_guild.name.strip().removeprefix("the").removeprefix("The").strip() + ) + + if ":full-name" in setting_name: + return { + main_guild.name.strip(), + main_guild.name.strip().title(), + SIMPLIFIED_FULL_NAME.split()[0].rsplit("'")[0], + SIMPLIFIED_FULL_NAME.split()[0].rsplit("'")[0].capitalize(), + } + + if ":short-name" in setting_name: + return { + "".join(word[0].upper() for word in SIMPLIFIED_FULL_NAME.split()), + "".join(word[0].lower() for word in SIMPLIFIED_FULL_NAME.split()), + } + try: - interaction_user: discord.Member = await ctx.bot.get_main_guild_member( - ctx.interaction.user, + interaction_member: discord.Member = await ctx.bot.get_main_guild_member( + ctx.interaction.user, # type: ignore[arg-type] ) except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -214,8 +220,8 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext channel.name for channel in main_guild.text_channels - if channel.permissions_for(interaction_user).is_superset( - discord.Permissions(send_messages=True) + if channel.permissions_for(interaction_member).is_superset( + discord.Permissions(send_messages=True), ) } ) diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 170847d4b..23ee51aab 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -24,7 +24,7 @@ class EditMessageCommandCog(TeXBotBaseCog): """Cog class that defines the "/editmessage" command and its call-back method.""" @staticmethod - async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # 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. diff --git a/cogs/induct.py b/cogs/induct.py index a07241ccb..8e34ec1c1 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -289,7 +289,7 @@ class InductCommandCog(BaseInductCog): """Cog class that defines the "/induct" command and its call-back method.""" @staticmethod - async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # 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. diff --git a/cogs/strike.py b/cogs/strike.py index e5c043e85..52939e948 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -721,7 +721,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 | str]: # noqa: E501 + async def strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice] | set[str]: # noqa: E501 """ Autocomplete callable that generates the set of available selectable members. diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index a6ab1e61d..671a432ec 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -30,7 +30,10 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) - if not re.fullmatch(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item): + IN_MESSAGE_KEY_FORMAT: Final[bool] = bool( + re.fullmatch(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item) # noqa: COM812 + ) + if not IN_MESSAGE_KEY_FORMAT: raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) if item not in self._messages: diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 3c8faaddf..48523cd59 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -59,7 +59,10 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if "_pytest" in item or item in ("__bases__", "__test__"): # NOTE: Overriding __getattr__() leads to many edge-case issues where external libraries will attempt to call getattr() with peculiar values raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) - if not re.fullmatch(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item): + IN_SETTING_KEY_FORMAT: Final[bool] = bool( + re.fullmatch(r"\A(?!.*__.*)(?:[A-Z]|[A-Z_][A-Z]|[A-Z_][A-Z][A-Z_]*[A-Z])\Z", item) # noqa: COM812 + ) + if not IN_SETTING_KEY_FORMAT: raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) if self._most_recent_yaml is None: diff --git a/poetry.lock b/poetry.lock index 4970da44e..49d40cdb8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1259,13 +1259,13 @@ files = [ [[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]] diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index e71aa8e53..dfdc9436a 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -149,7 +149,7 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interac ) @staticmethod - async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice | str]: # 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. From f25ac5d0961d4b58cfce003766a67b440252617d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 10 Jun 2024 02:40:46 +0100 Subject: [PATCH 039/128] Remove `exceptions/messages.py` --- exceptions/messages.py | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 exceptions/messages.py diff --git a/exceptions/messages.py b/exceptions/messages.py deleted file mode 100644 index 3700e5295..000000000 --- a/exceptions/messages.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Custom exception classes raised when errors occur with retrieving messages from the file.""" - -from collections.abc import Sequence - -__all__: Sequence[str] = () From 6215428e11e0b9446549fdcdba8c0ff3fd2f67d4 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 10 Jun 2024 18:56:54 +0100 Subject: [PATCH 040/128] Small fixes --- cogs/__init__.py | 12 +++---- cogs/edit_message.py | 4 +-- cogs/induct.py | 37 +++++++++++---------- cogs/remind_me.py | 4 +-- cogs/stats.py | 2 +- cogs/write_roles.py | 4 +-- exceptions/config_changes.py | 2 -- exceptions/does_not_exist.py | 62 ------------------------------------ 8 files changed, 34 insertions(+), 93 deletions(-) diff --git a/cogs/__init__.py b/cogs/__init__.py index 6daeb0b48..93fffff7b 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -14,9 +14,9 @@ "DeleteAllCommandsCog", "EditMessageCommandCog", "EnsureMembersInductedCommandCog", - "InductCommandCog", + "InductSlashCommandCog", "InductSendMessageCog", - "InductUserCommandsCog", + "InductContextCommandsCog", "KillCommandCog", "MakeMemberCommandCog", "PingCommandCog", @@ -44,9 +44,9 @@ from cogs.edit_message import EditMessageCommandCog from cogs.induct import ( EnsureMembersInductedCommandCog, - InductCommandCog, + InductContextCommandsCog, InductSendMessageCog, - InductUserCommandsCog, + InductSlashCommandCog, ) from cogs.kill import KillCommandCog from cogs.make_member import MakeMemberCommandCog @@ -76,9 +76,9 @@ def setup(bot: TeXBot) -> None: DeleteAllCommandsCog, EditMessageCommandCog, EnsureMembersInductedCommandCog, - InductCommandCog, + InductSlashCommandCog, InductSendMessageCog, - InductUserCommandsCog, + InductContextCommandsCog, KillCommandCog, MakeMemberCommandCog, PingCommandCog, diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 23ee51aab..f313164b3 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -21,7 +21,7 @@ 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] | set[str]: # noqa: E501 @@ -44,7 +44,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[ # 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] diff --git a/cogs/induct.py b/cogs/induct.py index 8e34ec1c1..a91d81f46 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -5,8 +5,8 @@ __all__: Sequence[str] = ( "InductSendMessageCog", "BaseInductCog", - "InductCommandCog", - "InductUserCommandsCog", + "InductSlashCommandCog", + "InductContextCommandsCog", "EnsureMembersInductedCommandCog", ) @@ -285,7 +285,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb await initial_response.edit(content=":white_check_mark: User inducted successfully.") -class InductCommandCog(BaseInductCog): +class InductSlashCommandCog(BaseInductCog): """Cog class that defines the "/induct" command and its call-back method.""" @staticmethod @@ -366,33 +366,34 @@ async def induct(self, ctx: TeXBotApplicationContext, str_induct_member_id: str, await self._perform_induction(ctx, induct_member, silent=silent) -class InductUserCommandsCog(BaseInductCog): +class InductContextCommandsCog(BaseInductCog): """Cog class that defines the context menu induction commands & their call-back methods.""" @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) @@ -403,8 +404,10 @@ 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( @@ -430,8 +433,10 @@ 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( diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 34a825d66..2cf3a12fb 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -31,7 +31,7 @@ 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 @@ -173,7 +173,7 @@ 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] diff --git a/cogs/stats.py b/cogs/stats.py index 31b7d0e3b..72dd9a54a 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -509,7 +509,7 @@ async def user_stats(self, ctx: TeXBotApplicationContext) -> None: # 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: diff --git a/cogs/write_roles.py b/cogs/write_roles.py index 657edd05f..700cc062a 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 diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index 2a31606d0..7d1de98fa 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -22,7 +22,6 @@ class BotRequiresRestartAfterConfigChange(BaseTeXBotError, Exception): @classproperty @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "TeX-Bot requires a restart due to configuration changes." @override @@ -48,7 +47,6 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 @override def __init__(self, message: str | None = None, config_setting_name: str | None = None) -> None: # noqa: E501 - """Initialize a ValueError exception for a non-existent user ID.""" self.config_setting_name: str | None = config_setting_name super().__init__( diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index 89b6ae7d2..0cd00ac28 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -31,7 +31,6 @@ class RulesChannelDoesNotExistError(BaseTeXBotError, ValueError): @classproperty @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "There is no channel marked as the rules channel." @@ -42,21 +41,18 @@ class GuildDoesNotExistError(BaseDoesNotExistError): @classproperty @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "Server with given ID does not exist or is not accessible to the bot." # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def ERROR_CODE(cls) -> str: # noqa: N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1011" # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 - """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 return "guild" @override @@ -77,14 +73,12 @@ class RoleDoesNotExistError(BaseDoesNotExistError, abc.ABC): @classproperty @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return f"Role with name \"{cls.ROLE_NAME}\" does not exist." # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 - """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 return "role" # noinspection PyMethodParameters,PyPep8Naming @@ -113,19 +107,12 @@ class CommitteeRoleDoesNotExistError(RoleDoesNotExistError): @classproperty @override def ERROR_CODE(cls) -> str: # noqa: N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1021" # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: 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 # noinspection SpellCheckingInspection return frozenset( { @@ -144,7 +131,6 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 @classproperty @override def ROLE_NAME(cls) -> str: # noqa: N805 - """The name of the Discord role that does not exist.""" # noqa: D401 return "Committee" @@ -155,19 +141,12 @@ class GuestRoleDoesNotExistError(RoleDoesNotExistError): @classproperty @override def ERROR_CODE(cls) -> str: # noqa: N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1022" # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: 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 # noinspection SpellCheckingInspection return frozenset({"induct", "stats", "archive", "ensure-members-inducted"}) @@ -175,12 +154,6 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 @classproperty @override def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: 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 # noinspection SpellCheckingInspection return frozenset({"send_get_roles_reminders"}) @@ -188,7 +161,6 @@ def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N805 @classproperty @override def ROLE_NAME(cls) -> str: # noqa: N805 - """The name of the Discord role that does not exist.""" # noqa: D401 return "Guest" @@ -199,19 +171,12 @@ class MemberRoleDoesNotExistError(RoleDoesNotExistError): @classproperty @override def ERROR_CODE(cls) -> str: # noqa: N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1023" # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: 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 # noinspection SpellCheckingInspection return frozenset({"makemember", "ensure-members-inducted"}) @@ -219,7 +184,6 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 @classproperty @override def ROLE_NAME(cls) -> str: # noqa: N805 - """The name of the Discord role that does not exist.""" # noqa: D401 return "Member" @@ -230,19 +194,12 @@ class ArchivistRoleDoesNotExistError(RoleDoesNotExistError): @classproperty @override def ERROR_CODE(cls) -> str: # noqa: N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1024" # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: 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 # noinspection SpellCheckingInspection return frozenset({"archive"}) @@ -250,7 +207,6 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 @classproperty @override def ROLE_NAME(cls) -> str: # noqa: N805 - """The name of the Discord role that does not exist.""" # noqa: D401 return "Archivist" @@ -261,14 +217,12 @@ class ChannelDoesNotExistError(BaseDoesNotExistError): @classproperty @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return f"Channel with name \"{cls.CHANNEL_NAME}\" does not exist." # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 - """The name of the Discord entity that this `DoesNotExistError` is associated with.""" # noqa: D401 return "channel" # noinspection PyMethodParameters,PyPep8Naming @@ -299,19 +253,12 @@ class RolesChannelDoesNotExistError(ChannelDoesNotExistError): @classproperty @override def ERROR_CODE(cls) -> str: # noqa: N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1031" # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: 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 # noinspection SpellCheckingInspection return frozenset({"writeroles"}) @@ -319,7 +266,6 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 @classproperty @override def CHANNEL_NAME(cls) -> str: # noqa: N805 - """The name of the Discord channel that does not exist.""" # noqa: D401 return "roles" @@ -330,19 +276,12 @@ class GeneralChannelDoesNotExistError(ChannelDoesNotExistError): @classproperty @override def ERROR_CODE(cls) -> str: # noqa: N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1032" # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: 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 # noinspection SpellCheckingInspection return frozenset({"induct"}) @@ -350,5 +289,4 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 @classproperty @override def CHANNEL_NAME(cls) -> str: # noqa: N805 - """The name of the Discord channel that does not exist.""" # noqa: D401 return "general" From abc6023b55b9b0368f0129f2ee6e38f23cee0665 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 12 Jun 2024 01:58:40 +0100 Subject: [PATCH 041/128] Finish adding autocomplete suggestions for setting values --- cogs/archive.py | 3 +- cogs/change_config.py | 75 ++++++++++++++++++++++++++++++++++++--- cogs/edit_message.py | 3 +- cogs/induct.py | 3 +- cogs/remind_me.py | 3 +- config/_settings/utils.py | 2 +- utils/tex_bot_base_cog.py | 4 +-- 7 files changed, 81 insertions(+), 12 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index a480a0484..2794023a1 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -7,6 +7,7 @@ import logging import re +from collections.abc import Set from logging import Logger from typing import Final @@ -28,7 +29,7 @@ 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] | set[str]: # 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. diff --git a/cogs/change_config.py b/cogs/change_config.py index 0ff00a197..27f39ee17 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -6,8 +6,10 @@ import itertools +import random import re import urllib.parse +from collections.abc import Set, MutableSequence from typing import Final import discord @@ -15,6 +17,7 @@ import config from config import CONFIG_SETTINGS_HELPS, ConfigSettingHelp, LogLevels +from config.constants import MESSAGES_LOCALE_CODES, SendIntroductionRemindersFlagType from exceptions import ( ChangingSettingWithRequiredSiblingError, DiscordMemberNotInMainGuildError, @@ -37,7 +40,7 @@ class ConfigChangeCommandsCog(TeXBotBaseCog): ) @staticmethod - async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice] | set[str]: # noqa: E501 + async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """Autocomplete callable that generates the set of available settings names.""" if not ctx.interaction.user: return set() @@ -51,7 +54,7 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> set return set(config.CONFIG_SETTINGS_HELPS) @staticmethod - async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice] | set[str]: # noqa: C901,PLR0911,PLR0912,E501 + async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: C901,PLR0911,PLR0912,E501 """Autocomplete callable that generates example values for a configuration setting.""" HAS_CONTEXT: Final[bool] = bool( ctx.interaction.user and "setting" in ctx.options and ctx.options["setting"], @@ -146,9 +149,71 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext } if "locale-code" in setting_name: - raise NotImplementedError # TODO: retrieve from constant + return MESSAGES_LOCALE_CODES - if re.search(r":links:[^:]+\Z", setting_name) or setting_name.endswith(":url"): + if "send-introduction-reminders:enable" in setting_name: + return { + str(flag_value).lower() + for flag_value + in getattr(SendIntroductionRemindersFlagType, "__args__") # noqa: B009 + } + + if "send-get-roles-reminders:enable" in setting_name: + return {"true", "false"} + + SETTING_NAME_IS_TIMEDELTA: Final[bool] = ( + ":timeout-duration" in setting_name + or ":delay" in setting_name + or ":interval" in setting_name # noqa: COM812 + ) + if SETTING_NAME_IS_TIMEDELTA: + timedelta_scales: MutableSequence[str] = ["s", "m", "h"] + + if ":timeout-duration" in setting_name or ":delay" in setting_name: + timedelta_scales.extend(["d", "w"]) + + return { + "".join( + ( + ( + f"{ + ( + f"{ + str( + random.choice( + ( + random.randint(1, 110), + round( + random.random() * 110, + random.randint(1, 3), + ), + ), + ), + ).removesuffix(".0").removesuffix(".00").removesuffix( + ".000", + ) + }{ + selected_timedelta_scale + }" + ) + if selected_timedelta_scale + else "" + }" + ) + for selected_timedelta_scale + in selected_timedelta_scales + ), + ) + for _ + in range(4) + for selected_timedelta_scales + in itertools.product( + *(("", timedelta_scale) for timedelta_scale in timedelta_scales), + ) + if any(selected_timedelta_scales) + } + + if setting_name.endswith(":url") or re.search(r":links:[^:]+\Z", setting_name): if "purchase-membership" in setting_name or "membership-perks" in setting_name: return { "https://", @@ -226,7 +291,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext } ) - return set() # TODO: extra autocomplete suggestions + return set() @change_config.command( name="get", diff --git a/cogs/edit_message.py b/cogs/edit_message.py index f313164b3..3cf2a20c2 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -6,6 +6,7 @@ import re +from collections.abc import Set import discord @@ -24,7 +25,7 @@ class EditMessageCommandCog(TeXBotBaseCog): """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] | set[str]: # 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. diff --git a/cogs/induct.py b/cogs/induct.py index f123c5adc..7be478423 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 @@ -288,7 +289,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] | set[str]: # 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. diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 2cf3a12fb..622210b59 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -10,6 +10,7 @@ import itertools import logging import re +from collections.abc import Set from logging import Logger from typing import TYPE_CHECKING, Final @@ -34,7 +35,7 @@ class RemindMeCommandCog(TeXBotBaseCog): """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 """ Autocomplete callable that generates the common delay input values. diff --git a/config/_settings/utils.py b/config/_settings/utils.py index 57f490256..427227e01 100644 --- a/config/_settings/utils.py +++ b/config/_settings/utils.py @@ -46,7 +46,7 @@ async def get_settings_file_path() -> AsyncPath: logger.debug( ( "Settings file location not supplied by environment variable, " - "falling back to `Tex-Bot-deployment.yaml`." + "falling back to `tex-bot-deployment.yaml`." ), ) raw_settings_file_path = "tex-bot-deployment.yaml" diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index dfdc9436a..3a582db0c 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -8,7 +8,7 @@ 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 @@ -149,7 +149,7 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interac ) @staticmethod - async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[discord.OptionChoice] | set[str]: # 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. From 55735d2ba135015467b6d57d3c1235c277b8b764 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 12 Jun 2024 02:04:07 +0100 Subject: [PATCH 042/128] Fix failing test --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index d43b8616f..6002e1094 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -92,6 +92,6 @@ def test_url_generates() -> None: ) assert re.fullmatch( - f"https://discord.com/.*={DISCORD_BOT_APPLICATION_ID}.*={DISCORD_MAIN_GUILD_ID}", + f"\Ahttps://discord.com/.*={DISCORD_BOT_APPLICATION_ID}.*={DISCORD_MAIN_GUILD_ID}.*\Z", invite_url, ) From 75281336ca18960c0c0ac229eb2b4b8121c922fc Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 12 Jun 2024 02:05:40 +0100 Subject: [PATCH 043/128] Fix incorrect string types used --- tests/test_utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 6002e1094..7a7a1054a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -92,6 +92,12 @@ def test_url_generates() -> None: ) assert re.fullmatch( - f"\Ahttps://discord.com/.*={DISCORD_BOT_APPLICATION_ID}.*={DISCORD_MAIN_GUILD_ID}.*\Z", + ( + r"\Ahttps://discord.com/.*=" + + str(DISCORD_BOT_APPLICATION_ID) + + r".*=" + + str(DISCORD_MAIN_GUILD_ID) + + r".*\Z" + ), invite_url, ) From ea55c51ec5392486a433068dcea6c52b446096d0 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 12 Jun 2024 02:08:57 +0100 Subject: [PATCH 044/128] Fix linting errors --- cogs/change_config.py | 4 ++-- cogs/remind_me.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index 27f39ee17..340d4c9f7 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -9,7 +9,7 @@ import random import re import urllib.parse -from collections.abc import Set, MutableSequence +from collections.abc import MutableSequence, Set from typing import Final import discord @@ -164,7 +164,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext SETTING_NAME_IS_TIMEDELTA: Final[bool] = ( ":timeout-duration" in setting_name or ":delay" in setting_name - or ":interval" in setting_name # noqa: COM812 + or ":interval" in setting_name ) if SETTING_NAME_IS_TIMEDELTA: timedelta_scales: MutableSequence[str] = ["s", "m", "h"] diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 622210b59..2ddddf13e 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -35,7 +35,7 @@ class RemindMeCommandCog(TeXBotBaseCog): """Cog class that defines the "/remind-me" command and its call-back method.""" @staticmethod - async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | 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. From 9478d633e6b8e2c457578693a8b34f12a6d08763 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 13 Jun 2024 12:50:05 +0100 Subject: [PATCH 045/128] Improve config stuffs --- cogs/change_config.py | 27 +++++++++++++++++++ config/_settings/_yaml/__init__.py | 8 +++--- .../_yaml/custom_scalar_validators.py | 27 ++++++++++++++----- poetry.lock | 8 +++--- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index 340d4c9f7..0702cdf38 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -6,13 +6,16 @@ import itertools +import logging import random import re import urllib.parse from collections.abc import MutableSequence, Set from typing import Final +from logging import Logger import discord +from discord.ui import View from strictyaml import StrictYAMLError import config @@ -30,6 +33,30 @@ TeXBotBaseCog, ) +logger: Final[Logger] = logging.getLogger("TeX-Bot") + + +class ConfirmSetConfigSettingValueView(View): + """A discord.View containing two buttons to confirm setting a given config setting.""" + + @discord.ui.button( # type: ignore[misc] + label="Yes", + style=discord.ButtonStyle.red, + custom_id="set_config_confirm", + ) + async def confirm_set_config_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 + """When the yes button is pressed, delete the message.""" + logger.debug("Yes button pressed. %s", interaction) + + @discord.ui.button( # type: ignore[misc] + label="No", + style=discord.ButtonStyle.green, + custom_id="set_config_cancel", + ) + async def cancel_set_config_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 + """When the no button is pressed, delete the message.""" + logger.debug("No button pressed. %s", interaction) + class ConfigChangeCommandsCog(TeXBotBaseCog): """Cog class that defines the "/config" command group and command call-back methods.""" diff --git a/config/_settings/_yaml/__init__.py b/config/_settings/_yaml/__init__.py index d6de98ddc..38cc9cc26 100644 --- a/config/_settings/_yaml/__init__.py +++ b/config/_settings/_yaml/__init__.py @@ -4,7 +4,7 @@ "DiscordWebhookURLValidator", "LogLevelValidator", "DiscordSnowflakeValidator", - "ProbabilityValidator", + "BoundedFloatValidator", "SendIntroductionRemindersFlagValidator", "SETTINGS_YAML_SCHEMA", "load_yaml", @@ -40,11 +40,11 @@ from .custom_map_validator import SlugKeyMap from .custom_scalar_validators import ( + BoundedFloatValidator, CustomBoolValidator, DiscordSnowflakeValidator, DiscordWebhookURLValidator, LogLevelValidator, - ProbabilityValidator, RegexMatcher, SendIntroductionRemindersFlagValidator, TimeDeltaValidator, @@ -152,7 +152,7 @@ strictyaml.Optional("ping", default=_DEFAULT_PING_COMMAND_SETTINGS): SlugKeyMap( # noqa: E501 { strictyaml.Optional("easter-egg-probability", default=DEFAULT_PING_COMMAND_EASTER_EGG_PROBABILITY): ( # noqa: E501 - ProbabilityValidator() + BoundedFloatValidator(0, 1) ), }, ), @@ -160,7 +160,7 @@ SlugKeyMap( { strictyaml.Optional("lookback-days", default=DEFAULT_STATS_COMMAND_LOOKBACK_DAYS): ( # noqa: E501 - strictyaml.Float() # TODO: Change to bounded float (min 5, max 1826) + BoundedFloatValidator(5, 1826) ), strictyaml.Optional("displayed-roles", default=DEFAULT_STATS_COMMAND_DISPLAYED_ROLES): ( # noqa: E501 strictyaml.UniqueSeq(strictyaml.Str()) diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index cf5f9e3ac..784626192 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -5,7 +5,7 @@ "LogLevelValidator", "DiscordSnowflakeValidator", "RegexMatcher", - "ProbabilityValidator", + "BoundedFloatValidator", "TimeDeltaValidator", "SendIntroductionRemindersFlagValidator", "CustomBoolValidator", @@ -155,13 +155,25 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] return data # type: ignore[return-value] -class ProbabilityValidator(strictyaml.Float): # type: ignore[misc] +class BoundedFloatValidator(strictyaml.Float): # type: ignore[misc] + @override + def __init__(self, inclusive_minimum: float, inclusive_maximum: float) -> None: + self.inclusive_minimum: float = inclusive_minimum + self.inclusive_maximum: float = inclusive_maximum + + super().__init__() + @override def validate_scalar(self, chunk: YAMLChunk) -> float: # type: ignore[misc] val: float = super().validate_scalar(chunk) - if not 0 <= val <= 1: - chunk.expecting_but_found("when expecting a probability") + if not self.inclusive_minimum <= val <= self.inclusive_maximum: + chunk.expecting_but_found( + ( + "when expecting a float " + f"between {self.inclusive_minimum} & {self.inclusive_maximum}" + ), + ) raise RuntimeError return val @@ -169,7 +181,10 @@ def validate_scalar(self, chunk: YAMLChunk) -> float: # type: ignore[misc] @override def to_yaml(self, data: object) -> str: # type: ignore[misc] YAML_SERIALIZATION_ERROR: Final[YAMLSerializationError] = YAMLSerializationError( - f"'{data}' is not a probability.", + ( + f"'{data}' is not a float " + f"between {self.inclusive_minimum} & {self.inclusive_maximum}." + ), ) if strictyaml_utils.is_string(data) and strictyaml_utils.is_decimal(data): @@ -178,7 +193,7 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] if not strictyaml_utils.has_number_type(data): raise YAML_SERIALIZATION_ERROR - if not 0 <= data <= 1: # type: ignore[operator] + if not self.inclusive_minimum <= data <= self.inclusive_maximum: # type: ignore[operator] raise YAML_SERIALIZATION_ERROR if math.isnan(data): # type: ignore[arg-type] diff --git a/poetry.lock b/poetry.lock index 49d40cdb8..411d4929d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -633,18 +633,18 @@ dev = ["coverage", "pytest (>=7.4.4)"] [[package]] name = "filelock" -version = "3.14.0" +version = "3.15.1" 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.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac"}, + {file = "filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8"}, ] [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)"] typing = ["typing-extensions (>=4.8)"] [[package]] From 30be8638e0c5c1cc056dac0f201f1c896c0a4d7a Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 13 Jun 2024 15:15:05 +0100 Subject: [PATCH 046/128] Add confirmation button for overwriting config value --- cogs/change_config.py | 86 ++++++++++++++++++++++++++---- cogs/strike.py | 6 +-- utils/__init__.py | 12 ++++- utils/message_sender_components.py | 78 +++++++++++++++++++++++++-- utils/tex_bot_base_cog.py | 38 ++++++++----- 5 files changed, 187 insertions(+), 33 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index 0702cdf38..628acda0f 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -5,14 +5,15 @@ __all__: Sequence[str] = ("ConfigChangeCommandsCog",) +import contextlib import itertools import logging import random import re import urllib.parse from collections.abc import MutableSequence, Set -from typing import Final from logging import Logger +from typing import Final import discord from discord.ui import View @@ -23,11 +24,15 @@ from config.constants import MESSAGES_LOCALE_CODES, SendIntroductionRemindersFlagType from exceptions import ( ChangingSettingWithRequiredSiblingError, + CommitteeRoleDoesNotExistError, DiscordMemberNotInMainGuildError, ) from exceptions.base import BaseDoesNotExistError from utils import ( CommandChecks, + EditorResponseComponent, + GenericResponderComponent, + SenderResponseComponent, TeXBotApplicationContext, TeXBotAutocompleteContext, TeXBotBaseCog, @@ -46,7 +51,7 @@ class ConfirmSetConfigSettingValueView(View): ) async def confirm_set_config_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 """When the yes button is pressed, delete the message.""" - logger.debug("Yes button pressed. %s", interaction) + logger.debug("\"Yes\" button pressed. %s", interaction) @discord.ui.button( # type: ignore[misc] label="No", @@ -55,7 +60,7 @@ async def confirm_set_config_button_callback(self, _: discord.Button, interactio ) async def cancel_set_config_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501 """When the no button is pressed, delete the message.""" - logger.debug("No button pressed. %s", interaction) + logger.debug("\"No\" button pressed. %s", interaction) class ConfigChangeCommandsCog(TeXBotBaseCog): @@ -473,11 +478,69 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ) return + if config.CONFIG_SETTINGS_HELPS[config_setting_name].default is None: + response: discord.Message | discord.Interaction = await ctx.respond( + content=( + f"Setting {config_setting_name.replace("`", "\\`")} " + "has no default value." + "If you overwrite it with a new value the old one will be lost " + "and cannot be restored.\n" + "Are you sure you want to overwrite the old value?\n\n" + "Please confirm using the buttons below." + ), + view=ConfirmSetConfigSettingValueView(), + ephemeral=True, + ) + + committee_role: discord.Role | None = None + with contextlib.suppress(CommitteeRoleDoesNotExistError): + committee_role = await self.bot.committee_role + + confirmation_message: discord.Message = ( + response + if isinstance(response, discord.Message) + else await response.original_response() + ) + button_interaction: discord.Interaction = await self.bot.wait_for( + "interaction", + check=lambda interaction: ( + interaction.type == discord.InteractionType.component + and interaction.message.id == confirmation_message.id + and ( + (committee_role in interaction.user.roles) + if committee_role + else True + ) + and "custom_id" in interaction.data + and interaction.data["custom_id"] in { + "shutdown_confirm", + "shutdown_cancel", + } + ), + ) + + if button_interaction.data["custom_id"] == "set_config_cancel": # type: ignore[index, typeddict-item] + await confirmation_message.edit( + content=( + "Aborting editing config setting: " + f"{config_setting_name.replace("`", "\\`")}" + ), + view=None, + ) + return + + if button_interaction.data["custom_id"] != "set_config_confirm": # type: ignore[index, typeddict-item] + raise ValueError + previous_config_setting_value: str | None = config.view_single_config_setting_value( config_setting_name, ) - # TODO: Are you sure, if config has no default + responder: GenericResponderComponent = ( + EditorResponseComponent(ctx.interaction) + if config.CONFIG_SETTINGS_HELPS[config_setting_name].default is None + else SenderResponseComponent(ctx.interaction, ephemeral=True) + ) yaml_error: StrictYAMLError changing_setting_error: ChangingSettingWithRequiredSiblingError @@ -486,6 +549,7 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n config_setting_name, new_config_value, ) + except StrictYAMLError as yaml_error: if str(yaml_error) != yaml_error.context: INCONCLUSIVE_YAML_ERROR_MESSAGE: Final[str] = ( @@ -500,8 +564,10 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n f"{str(yaml_error.context)[0].upper()}" f"{str(yaml_error.context)[1:].strip(" .")}." ), + responder_component=responder, ) return + except ChangingSettingWithRequiredSiblingError as changing_setting_error: await self.command_send_error( ctx, @@ -510,6 +576,7 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n f"It will be easier to make your changes " f"directly within the \"tex-bot-deployment.yaml\" file." ), + responder_component=responder, ) return @@ -518,9 +585,9 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ) if changed_config_setting_value == previous_config_setting_value: - await ctx.respond( + await responder.respond( "No changes made. Provided value was the same as the previous value.", - ephemeral=True, + view=None, ) return @@ -532,9 +599,8 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n or "cookie" in config_setting_name or "secret" in config_setting_name # noqa: COM812 ) - - await ctx.respond( - ( + await responder.respond( + content=( f"Successfully updated setting: `{ config_setting_name.replace("`", "\\`") }`" @@ -551,7 +617,7 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n }\n\n" "Changes could take up to ??? to take effect." # TODO: Retrieve update time from task ), - ephemeral=True, + view=None, ) # TODO: Command to unset value (if it is optional) diff --git a/cogs/strike.py b/cogs/strike.py index 52939e948..29da670b9 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -49,7 +49,7 @@ ) from utils.message_sender_components import ( ChannelMessageSender, - MessageSenderComponent, + MessageSavingSenderComponent, ResponseMessageSender, ) @@ -244,7 +244,7 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me "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(), @@ -288,7 +288,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." diff --git a/utils/__init__.py b/utils/__init__.py index d53ad82f0..65cfc57f3 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -7,7 +7,10 @@ __all__: Sequence[str] = ( # noqa: PLE0604 "CommandChecks", - "MessageSenderComponent", + "MessageSavingSenderComponent", + "GenericResponderComponent", + "SenderResponseComponent", + "EditorResponseComponent", "SuppressTraceback", "TeXBot", "TeXBotExitReason", @@ -26,7 +29,12 @@ from config._settings.utils import * # noqa: F403 from .command_checks import CommandChecks -from .message_sender_components import MessageSenderComponent +from .message_sender_components import ( + EditorResponseComponent, + GenericResponderComponent, + MessageSavingSenderComponent, + SenderResponseComponent, +) from .suppress_traceback import SuppressTraceback from .tex_bot import TeXBot, TeXBotExitReason from .tex_bot_base_cog import TeXBotBaseCog diff --git a/utils/message_sender_components.py b/utils/message_sender_components.py index b6c8c9762..a7599dfaa 100644 --- a/utils/message_sender_components.py +++ b/utils/message_sender_components.py @@ -3,7 +3,10 @@ from collections.abc import Sequence __all__: Sequence[str] = ( - "MessageSenderComponent", + "GenericResponderComponent", + "SenderResponseComponent", + "EditorResponseComponent", + "MessageSavingSenderComponent", "ChannelMessageSender", "ResponseMessageSender", ) @@ -18,9 +21,74 @@ from .tex_bot_contexts import TeXBotApplicationContext -class MessageSenderComponent(abc.ABC): +class _VIEW_NOT_PROVIDED: # noqa: N801 + pass + + +class GenericResponderComponent(abc.ABC): + """Abstract protocol definition of a component that responds in some way.""" + + @override + def __init__(self, interaction: discord.Interaction) -> None: + self.interaction: discord.Interaction = interaction + + super().__init__() + + @abc.abstractmethod + async def respond(self, content: str, *, view: View | None | type[_VIEW_NOT_PROVIDED] = _VIEW_NOT_PROVIDED) -> None: # noqa: E501 + """Respond in some way to the user with the given content & view.""" + + +class SenderResponseComponent(GenericResponderComponent): + """ + Concrete definition of a message sending response component. + + Defines the way to send a provided message content & optional view. + """ + + @override + def __init__(self, interaction: discord.Interaction, *, ephemeral: bool) -> None: + self.ephemeral: bool = ephemeral + + super().__init__(interaction=interaction) + + @override + async def respond(self, content: str, *, view: View | None | type[_VIEW_NOT_PROVIDED] = _VIEW_NOT_PROVIDED) -> None: # noqa: E501 + if view is _VIEW_NOT_PROVIDED: + await self.interaction.respond(content=content, ephemeral=self.ephemeral) + return + + if view is not None and not isinstance(view, View): + raise TypeError + + await self.interaction.respond( + content=content, + view=view, + ephemeral=self.ephemeral, + ) + + +class EditorResponseComponent(GenericResponderComponent): + """ + Concrete definition of a message editing response component. + + Defines the way to edit a previous message to the given content & optional view. + """ + + @override + async def respond(self, content: str, *, view: View | None | type[_VIEW_NOT_PROVIDED] = _VIEW_NOT_PROVIDED) -> None: # noqa: E501 + if view is _VIEW_NOT_PROVIDED: + await self.interaction.edit_original_response(content=content) + + if view is not None and not isinstance(view, View): + raise TypeError + + await self.interaction.edit_original_response(content=content, view=view) + + +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 +132,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 +168,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/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 3a582db0c..d4eb4a34c 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -20,6 +20,7 @@ BaseDoesNotExistError, ) +from .message_sender_components import GenericResponderComponent, SenderResponseComponent from .tex_bot import TeXBot from .tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext @@ -66,25 +67,25 @@ def __init__(self, bot: TeXBot) -> None: """Initialize a new cog instance, storing a reference to the bot object.""" self.bot: TeXBot = 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 + async def command_send_error(self, ctx: TeXBotApplicationContext, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, responder_component: GenericResponderComponent | None = None) -> None: # noqa: E501 """ Construct & format an error message from the given details. The constructed error message is then sent as the response to the given application command context. """ - COMMAND_NAME: Final[str] = ( - ctx.command.callback.__name__ - if ( - hasattr(ctx.command, "callback") - and not ctx.command.callback.__name__.startswith("_") - ) else ctx.command.qualified_name - ) - - await self.send_error( + await self._respond_with_error( self.bot, - ctx.interaction, - interaction_name=COMMAND_NAME, + responder=( + responder_component or SenderResponseComponent(ctx.interaction, ephemeral=True) + ), + interaction_name=( + ctx.command.callback.__name__ + if ( + hasattr(ctx.command, "callback") + and not ctx.command.callback.__name__.startswith("_") + ) else ctx.command.qualified_name + ), error_code=error_code, message=message, logging_message=logging_message, @@ -97,6 +98,17 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interac The constructed error message is then sent as the response to the given interaction. """ + await cls._respond_with_error( + bot=bot, + responder=SenderResponseComponent(interaction, ephemeral=True), + interaction_name=interaction_name, + error_code=error_code, + message=message, + logging_message=logging_message, + ) + + @classmethod + async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderComponent, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None) -> None: # noqa: E501 construct_error_message: str = ":warning:There was an error" if error_code: @@ -132,7 +144,7 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interac ) construct_error_message += f"\n`{message}`" - await interaction.respond(construct_error_message, ephemeral=True) + await responder.respond(content=construct_error_message, view=None) if logging_message: logger.error( From 1115948e1e45f4d2a9c3ebdccfbff37f2e0db134 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 15 Jun 2024 12:40:11 +0100 Subject: [PATCH 047/128] Improve boolean handling --- cogs/change_config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index 628acda0f..c7162f5ed 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -478,7 +478,11 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ) return - if config.CONFIG_SETTINGS_HELPS[config_setting_name].default is None: + SELECTED_SETTING_HAS_DEFAULT: Final[bool] = ( + config.CONFIG_SETTINGS_HELPS[config_setting_name].default is not None + ) + + if not SELECTED_SETTING_HAS_DEFAULT: response: discord.Message | discord.Interaction = await ctx.respond( content=( f"Setting {config_setting_name.replace("`", "\\`")} " @@ -538,7 +542,7 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n responder: GenericResponderComponent = ( EditorResponseComponent(ctx.interaction) - if config.CONFIG_SETTINGS_HELPS[config_setting_name].default is None + if not SELECTED_SETTING_HAS_DEFAULT else SenderResponseComponent(ctx.interaction, ephemeral=True) ) From ed0d9a69c1bf574f6173ef3d7a4ae3728013cde6 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 17 Jun 2024 21:59:34 +0100 Subject: [PATCH 048/128] Improve error message handling --- cogs/archive.py | 180 ++++++++++-------- cogs/change_config.py | 6 +- cogs/command_error.py | 91 +++++---- cogs/get_token_authorisation.py | 1 + cogs/induct.py | 7 +- cogs/kill.py | 37 ++-- cogs/ping.py | 1 + cogs/remind_me.py | 19 +- cogs/startup.py | 1 - .../_yaml/custom_scalar_validators.py | 4 +- exceptions/__init__.py | 4 + exceptions/custom_django.py | 22 +++ exceptions/does_not_exist.py | 1 + exceptions/error_message_generation.py | 29 +++ utils/error_capture_decorators.py | 1 - utils/tex_bot.py | 12 +- utils/tex_bot_base_cog.py | 86 ++++++--- 17 files changed, 318 insertions(+), 184 deletions(-) create mode 100644 exceptions/custom_django.py create mode 100644 exceptions/error_message_generation.py diff --git a/cogs/archive.py b/cogs/archive.py index 2794023a1..0df7e0a64 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -5,11 +5,12 @@ __all__: Sequence[str] = ("ArchiveCommandCog",) +import functools import logging import re from collections.abc import Set from logging import Logger -from typing import Final +from typing import Final, Protocol, TypeAlias import discord @@ -24,6 +25,18 @@ logger: Final[Logger] = logging.getLogger("TeX-Bot") +AllowedChannelTypes: TypeAlias = ( + discord.VoiceChannel + | discord.StageChannel + | discord.TextChannel + | discord.ForumChannel + | discord.CategoryChannel +) + + +class _SetPermissionsFunc(Protocol): + async def __call__(self, channel: AllowedChannelTypes) -> None: ... + class ArchiveCommandCog(TeXBotBaseCog): """Cog class that defines the "/archive" command and its call-back method.""" @@ -59,6 +72,79 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[dis ) } + async def _set_permissions(self, channel: AllowedChannelTypes, ctx: TeXBotApplicationContext, interaction_member: discord.Member, *, committee_role: discord.Role, guest_role: discord.Role, member_role: discord.Role, archivist_role: discord.Role, everyone_role: discord.Role) -> None: # noqa: PLR0913,E501 + CHANNEL_NEEDS_COMMITTEE_ARCHIVING: Final[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), + ) + ) + if CHANNEL_NEEDS_COMMITTEE_ARCHIVING: + await channel.set_permissions( + everyone_role, + reason=f"{interaction_member.display_name} used \"/archive\".", + view_channel=False, + ) + await channel.set_permissions( + guest_role, + overwrite=None, + reason=f"{interaction_member.display_name} used \"/archive\".", + ) + await channel.set_permissions( + member_role, + overwrite=None, + reason=f"{interaction_member.display_name} used \"/archive\".", + ) + await channel.set_permissions( + committee_role, + overwrite=None, + reason=f"{interaction_member.display_name} used \"/archive\".", + ) + return + + CHANNEL_NEEDS_NORMAL_ARCHIVING: Final[bool] = ( + channel.permissions_for(guest_role).is_superset( + discord.Permissions(view_channel=True), + ) + ) + if CHANNEL_NEEDS_NORMAL_ARCHIVING: + await channel.set_permissions( + everyone_role, + reason=f"{interaction_member.display_name} used \"/archive\".", + view_channel=False, + ) + await channel.set_permissions( + guest_role, + overwrite=None, + reason=f"{interaction_member.display_name} used \"/archive\".", + ) + await channel.set_permissions( + member_role, + overwrite=None, + reason=f"{interaction_member.display_name} used \"/archive\".", + ) + await channel.set_permissions( + committee_role, + reason=f"{interaction_member.display_name} used \"/archive\".", + view_channel=False, + ) + await channel.set_permissions( + archivist_role, + reason=f"{interaction_member.display_name} used \"/archive\".", + view_channel=True, + ) + return + + await self.command_send_error( + ctx, + message=f"Channel {channel.mention} had invalid permissions", + ) + logger.error( + "Channel %s had invalid permissions, so could not be archived.", + channel.name, + ) + @discord.slash_command( # type: ignore[no-untyped-call, misc] name="archive", description="Archives the selected category.", @@ -119,88 +205,22 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> ) return - # noinspection PyUnreachableCode - channel: ( - discord.VoiceChannel - | discord.StageChannel - | discord.TextChannel - | discord.ForumChannel - | discord.CategoryChannel + set_permissions_func: _SetPermissionsFunc = functools.partial( + self._set_permissions, + ctx=ctx, + interaction_member=interaction_member, + committee_role=committee_role, + guest_role=guest_role, + member_role=member_role, + archivist_role=archivist_role, + everyone_role=everyone_role, ) + + # noinspection PyUnreachableCode + channel: AllowedChannelTypes for channel in category.channels: try: - 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), - ) - if channel_needs_committee_archiving: - await channel.set_permissions( - everyone_role, - reason=f"{interaction_member.display_name} used \"/archive\".", - view_channel=False, - ) - await channel.set_permissions( - guest_role, - overwrite=None, - reason=f"{interaction_member.display_name} used \"/archive\".", - ) - await channel.set_permissions( - member_role, - overwrite=None, - reason=f"{interaction_member.display_name} used \"/archive\".", - ) - await channel.set_permissions( - committee_role, - overwrite=None, - reason=f"{interaction_member.display_name} used \"/archive\".", - ) - - elif channel_needs_normal_archiving: - await channel.set_permissions( - everyone_role, - reason=f"{interaction_member.display_name} used \"/archive\".", - view_channel=False, - ) - await channel.set_permissions( - guest_role, - overwrite=None, - reason=f"{interaction_member.display_name} used \"/archive\".", - ) - await channel.set_permissions( - member_role, - overwrite=None, - reason=f"{interaction_member.display_name} used \"/archive\".", - ) - await channel.set_permissions( - committee_role, - reason=f"{interaction_member.display_name} used \"/archive\".", - view_channel=False, - ) - await channel.set_permissions( - archivist_role, - reason=f"{interaction_member.display_name} used \"/archive\".", - view_channel=True, - ) - - else: - await self.command_send_error( - ctx, - message=f"Channel {channel.mention} had invalid permissions", - ) - logger.error( - "Channel %s had invalid permissions, so could not be archived.", - channel.name, - ) - return - + await set_permissions_func(channel=channel) except discord.Forbidden: await self.command_send_error( ctx, diff --git a/cogs/change_config.py b/cogs/change_config.py index c7162f5ed..da759a635 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -344,7 +344,7 @@ async def get_config_value(self, ctx: TeXBotApplicationContext, config_setting_n if config_setting_name not in config.CONFIG_SETTINGS_HELPS: await self.command_send_error( ctx, - f"Invalid setting: {config_setting_name!r}", + message=f"Invalid setting: {config_setting_name!r}", ) return @@ -403,7 +403,7 @@ async def help_config_setting(self, ctx: TeXBotApplicationContext, config_settin if config_setting_name not in config.CONFIG_SETTINGS_HELPS: await self.command_send_error( ctx, - f"Invalid setting: {config_setting_name!r}", + message=f"Invalid setting: {config_setting_name!r}", ) return @@ -474,7 +474,7 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n if config_setting_name not in config.CONFIG_SETTINGS_HELPS: await self.command_send_error( ctx, - f"Invalid setting: {config_setting_name!r}", + message=f"Invalid setting: {config_setting_name!r}", ) return diff --git a/cogs/command_error.py b/cogs/command_error.py index cade4d0fc..a065b35b1 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -16,7 +16,9 @@ from exceptions import ( CommitteeRoleDoesNotExistError, + ErrorCodeCouldNotBeIdentifiedError, GuildDoesNotExistError, + UnknownDjangoError, ) from exceptions.base import BaseErrorWithErrorCode from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog @@ -27,46 +29,54 @@ class CommandErrorCog(TeXBotBaseCog): """Cog class that defines additional code to execute upon a command error.""" + @classmethod + def _get_logging_message_from_error(cls, error: discord.ApplicationCommandInvokeError) -> str | None: # noqa: E501 + if isinstance(error.original, GuildDoesNotExistError): + return None + + if not str(error.original).strip(". -\"'"): + return f"{error.original.__class__.__name__} was raised." + + if str(error.original).startswith("\"") or str(error.original).startswith("'"): + return f"{error.original.__class__.__name__}: {error.original}" + + if isinstance(error.original, UnknownDjangoError): + return str(error.original) + + if isinstance(error.original, RuntimeError | NotImplementedError): + return f"{error.original.__class__.__name__}: {error.original}" + + return str(error.original) + + @classmethod + def _get_error_code_from_error(cls, error: discord.ApplicationCommandInvokeError) -> str: + if isinstance(error.original, Forbidden): + return "E1044" + + if isinstance(error.original, BaseErrorWithErrorCode): + return error.original.ERROR_CODE + + raise ErrorCodeCouldNotBeIdentifiedError(other_error=error.original) + @TeXBotBaseCog.listener() async def on_application_command_error(self, ctx: TeXBotApplicationContext, error: discord.ApplicationCommandError) -> None: # noqa: E501 """Log any major command errors in the logging channel & stderr.""" + IS_FATAL: Final[bool] = ( + isinstance(error, discord.ApplicationCommandInvokeError) + and ( + isinstance(error.original, RuntimeError | NotImplementedError) + or type(error.original) is Exception + ) + ) + error_code: str | None = None - message: str | None = "Please contact a committee member." + message: str | None = "Please contact a committee member." if not IS_FATAL else "" logging_message: str | BaseException | None = None if isinstance(error, discord.ApplicationCommandInvokeError): - message = None - logging_message = ( - None - if isinstance(error.original, GuildDoesNotExistError) - else ( - f"{error.original.__class__.__name__} was raised." - if not str(error.original).strip(". -\"'") - else ( - f"{error.original.__class__.__name__}: {error.original}" - if ( - str(error.original).startswith("\"") - or str(error.original).startswith("'") - ) - else ( - f"{ - f"{error.original.__class__.__name__}: " - if isinstance( - error.original, - RuntimeError | NotImplementedError, - ) - else "" - }{error.original}" - ) - ) - ) - ) - - if isinstance(error.original, Forbidden): - error_code = "E1044" - - elif isinstance(error.original, BaseErrorWithErrorCode): - error_code = error.original.ERROR_CODE + logging_message = self._get_logging_message_from_error(error) + with contextlib.suppress(ErrorCodeCouldNotBeIdentifiedError): + error_code = self._get_error_code_from_error(error) elif isinstance(error, CheckAnyFailure): if CommandChecks.is_interaction_user_in_main_guild_failure(error.checks[0]): @@ -87,13 +97,11 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro error_code=error_code, message=message, logging_message=logging_message, + is_fatal=IS_FATAL, ) if isinstance(error, discord.ApplicationCommandInvokeError): - if isinstance(error.original, RuntimeError | NotImplementedError): - await self.bot.close() - - elif isinstance(error.original, GuildDoesNotExistError): + if isinstance(error.original, GuildDoesNotExistError): command_name: str = ( ctx.command.callback.__name__ if (hasattr(ctx.command, "callback") @@ -116,4 +124,13 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro if message_part ), ) + + BOT_NEEDS_CLOSING: Final[bool] = ( + isinstance( + error.original, + RuntimeError | NotImplementedError | GuildDoesNotExistError, + ) + or type(error.original) is Exception + ) + if BOT_NEEDS_CLOSING: await self.bot.close() diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 122c79268..8ca8ab8cd 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -108,6 +108,7 @@ 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 diff --git a/cogs/induct.py b/cogs/induct.py index 5350a1509..c62c8295f 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -74,14 +74,14 @@ 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.", ) @@ -248,6 +248,7 @@ 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 @@ -323,7 +324,6 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor in members } - @discord.slash_command( # type: ignore[no-untyped-call, misc] name="induct", description=( @@ -363,7 +363,6 @@ async def induct(self, ctx: TeXBotApplicationContext, str_induct_member_id: str, 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) diff --git a/cogs/kill.py b/cogs/kill.py index 1c41899d3..0b9f5d449 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -89,20 +89,23 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: ), ) - if button_interaction.data["custom_id"] == "shutdown_confirm": # type: ignore[index, typeddict-item] - await confirmation_message.edit( - 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 - - if button_interaction.data["custom_id"] == "shutdown_cancel": # type: ignore[index, typeddict-item] - await confirmation_message.edit( - content="Shutdown has been cancelled.", - view=None, - ) - logger.info("Manual shutdown cancelled by %s.", ctx.interaction.user) - return - - raise ValueError + match button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] + case "shutdown_confirm": + await confirmation_message.edit( + 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) + + case "shutdown_cancel": + await confirmation_message.edit( + content="Shutdown has been cancelled.", + view=None, + ) + logger.info( + "Manual shutdown cancelled by %s.", + ctx.interaction.user, + ) + + case _: + raise ValueError diff --git a/cogs/ping.py b/cogs/ping.py index dc3fbd787..000b8ec87 100644 --- a/cogs/ping.py +++ b/cogs/ping.py @@ -19,6 +19,7 @@ class PingCommandCog(TeXBotBaseCog): @discord.slash_command(description="Replies with Pong!") # type: ignore[no-untyped-call, misc] async def ping(self, ctx: TeXBotApplicationContext) -> None: """Definition & callback response of the "ping" command.""" + raise Exception await ctx.respond( random.choices( [ diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 2ddddf13e..6f8c3e904 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -21,6 +21,7 @@ from django.utils import timezone from db.core.models import DiscordMember, DiscordReminder +from exceptions import UnknownDjangoError from utils import TeXBot, TeXBotApplicationContext, TeXBotAutocompleteContext, TeXBotBaseCog if TYPE_CHECKING: @@ -223,7 +224,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 @@ -231,14 +232,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: - 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 + if not ERROR_IS_ALREADY_EXISTS: + raise UnknownDjangoError( + message=( + f"Error when creating DiscordReminder object: " + f"{create_discord_reminder_error}" + ), + ) from create_discord_reminder_error await self.command_send_error( ctx, @@ -334,7 +334,6 @@ async def clear_reminders_backlog(self) -> None: ), ) await self.bot.close() - return await channel.send( "**Sorry it's a bit late! " diff --git a/cogs/startup.py b/cogs/startup.py index c737375f2..5c005b4ed 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -107,7 +107,6 @@ async def _initialise_main_guild(self) -> None: guild_id=settings["_DISCORD_MAIN_GUILD_ID"]), ) await self.bot.close() - raise RuntimeError async def _check_strike_performed_manually_warning_location_exists(self) -> None: if settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] == "DM": diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index 784626192..6752886b5 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -261,7 +261,7 @@ def validate_scalar(self, chunk: YAMLChunk) -> timedelta: # type: ignore[misc] found="found non-matching string", ) - match: Match[str] | None = self.regex_matcher.match(chunk.contents) + match: Match[str] | None = self.regex_matcher.fullmatch(chunk.contents) if match is None: chunk_error_func() @@ -280,7 +280,7 @@ def validate_scalar(self, chunk: YAMLChunk) -> timedelta: # type: ignore[misc] @override def to_yaml(self, data: object) -> str: # type: ignore[misc] if strictyaml_utils.is_string(data): - match: Match[str] | None = self.regex_matcher.match(str(data)) + match: Match[str] | None = self.regex_matcher.fullmatch(str(data)) if match is None: INVALID_STRING_DATA_MESSAGE: Final[str] = ( f"when expecting a delay/interval string found {str(data)!r}." diff --git a/exceptions/__init__.py b/exceptions/__init__.py index 900071eb5..6ebd8133b 100644 --- a/exceptions/__init__.py +++ b/exceptions/__init__.py @@ -20,6 +20,8 @@ "NoAuditLogsStrikeTrackingError", "BotRequiresRestartAfterConfigChange", "ChangingSettingWithRequiredSiblingError", + "ErrorCodeCouldNotBeIdentifiedError", + "UnknownDjangoError", ) @@ -27,6 +29,7 @@ BotRequiresRestartAfterConfigChange, ChangingSettingWithRequiredSiblingError, ) +from .custom_django import UnknownDjangoError from .does_not_exist import ( ApplicantRoleDoesNotExistError, ArchivistRoleDoesNotExistError, @@ -40,6 +43,7 @@ RolesChannelDoesNotExistError, RulesChannelDoesNotExistError, ) +from .error_message_generation import ErrorCodeCouldNotBeIdentifiedError from .guild import ( DiscordMemberNotInMainGuildError, EveryoneRoleCouldNotBeRetrievedError, diff --git a/exceptions/custom_django.py b/exceptions/custom_django.py new file mode 100644 index 000000000..da3b90c9e --- /dev/null +++ b/exceptions/custom_django.py @@ -0,0 +1,22 @@ +"""Custom exception classes related to Django processes.""" + +from collections.abc import Sequence + +__all__: Sequence[str] = ("UnknownDjangoError",) + + +from typing import override + +from classproperties import classproperty + +from .base import BaseTeXBotError + + +class UnknownDjangoError(BaseTeXBotError, RuntimeError): + """Exception class to raise when an unknown Django error occurs.""" + + # noinspection PyMethodParameters,PyPep8Naming + @classproperty + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 + return "An unknown Django error occurred." diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index aac102a69..e7e044edc 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -10,6 +10,7 @@ "GuestRoleDoesNotExistError", "MemberRoleDoesNotExistError", "ArchivistRoleDoesNotExistError", + "ApplicantRoleDoesNotExistError", "ChannelDoesNotExistError", "RolesChannelDoesNotExistError", "GeneralChannelDoesNotExistError", diff --git a/exceptions/error_message_generation.py b/exceptions/error_message_generation.py new file mode 100644 index 000000000..64812e030 --- /dev/null +++ b/exceptions/error_message_generation.py @@ -0,0 +1,29 @@ +"""Custom exception classes related to generating error messages to send to the user.""" + +from collections.abc import Sequence + +__all__: Sequence[str] = ("ErrorCodeCouldNotBeIdentifiedError",) + + +from typing import override + +from classproperties import classproperty + +from .base import BaseTeXBotError + + +class ErrorCodeCouldNotBeIdentifiedError(BaseTeXBotError, Exception): + """Exception class to raise when the error code could not be identified from an error.""" + + # noinspection PyMethodParameters,PyPep8Naming + @classproperty + @override + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 + return "The error code could not be retrieved from the given error." + + @override + def __init__(self, message: str | None = None, other_error: Exception | type[Exception] | None = None) -> None: # noqa: E501 + """Initialize an exception for a non-existent error code.""" + self.other_error: Exception | type[Exception] | None = other_error + + super().__init__(message) diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index 99636289a..2d9de62fd 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -70,7 +70,6 @@ async def wrapper(self: TeXBotBaseCog, /, *args: P.args, **kwargs: P.kwargs) -> except error_type as error: close_func(error) await self.bot.close() - return None return wrapper # type: ignore[return-value] @staticmethod diff --git a/utils/tex_bot.py b/utils/tex_bot.py index ac6439211..fb7f3791f 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -10,7 +10,7 @@ from collections.abc import Collection from enum import IntEnum from logging import Logger -from typing import Final, TypeAlias +from typing import Final, NoReturn, TypeAlias, override import discord @@ -59,6 +59,7 @@ 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.""" self._main_guild: discord.Guild | None = None @@ -76,6 +77,11 @@ 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() + raise RuntimeError + # noinspection PyPep8Naming @property def EXIT_REASON(self) -> TeXBotExitReason: # noqa: N802 @@ -382,7 +388,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. @@ -400,7 +406,7 @@ async def perform_kill_and_close(self, initiated_by_user: discord.User | discord self._exit_reason = TeXBotExitReason.KILL_COMMAND_USED await self.close() - async def perform_restart_after_config_changes(self) -> None: + async def perform_restart_after_config_changes(self) -> NoReturn: """Restart TeX-Bot after the config changes.""" if self.EXIT_REASON is not TeXBotExitReason.UNKNOWN_ERROR: EXIT_REASON_ALREADY_SET_MESSAGE: Final[str] = ( diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index d4eb4a34c..650ce769f 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -67,12 +67,14 @@ def __init__(self, bot: TeXBot) -> None: """Initialize a new cog instance, storing a reference to the bot object.""" self.bot: TeXBot = bot - async def command_send_error(self, ctx: TeXBotApplicationContext, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, responder_component: GenericResponderComponent | None = None) -> None: # noqa: E501 + async def command_send_error(self, ctx: TeXBotApplicationContext, *, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False, responder_component: GenericResponderComponent | None = None) -> None: # noqa: E501 """ Construct & format an error message from the given details. The constructed error message is then sent as the response to the given application command context. + If `is_fatal` is set to True, this suggests that the reason for the error is unknown + and the bot will shortly close. """ await self._respond_with_error( self.bot, @@ -89,14 +91,17 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, error_code: st error_code=error_code, message=message, logging_message=logging_message, + is_fatal=is_fatal, ) @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, bot: TeXBot, interaction: discord.Interaction, *, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 """ Construct & format an error message from the given details. The constructed error message is then sent as the response to the given interaction. + If `is_fatal` is set to True, this suggests that the reason for the error is unknown + and the bot will shortly close. """ await cls._respond_with_error( bot=bot, @@ -105,44 +110,66 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interac error_code=error_code, message=message, logging_message=logging_message, + is_fatal=is_fatal, ) @classmethod - async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderComponent, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None) -> None: # noqa: E501 - construct_error_message: str = ":warning:There was an error" + async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderComponent, *, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False) -> None: # noqa: PLR0913E501 + construct_error_message: str = ":warning:" - if error_code: + if is_fatal: # noinspection PyUnusedLocal - committee_mention: str = "committee" + fatal_committee_mention: str = "committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_mention = (await bot.committee_role).mention + fatal_committee_mention = (await bot.committee_role).mention - construct_error_message = ( - f"**Contact a {committee_mention} member, referencing error code: " - f"{error_code}**\n" - + construct_error_message - ) - - if interaction_name in cls.ERROR_ACTIVITIES: construct_error_message += ( - f" when trying to {cls.ERROR_ACTIVITIES[interaction_name]}" + "A fatal error occurred, " + f"please **contact a {fatal_committee_mention} member**.:warning:" ) - if message: - construct_error_message += ":" + if message: + construct_error_message += message.strip() + else: - construct_error_message += "." + construct_error_message += "There was an error" - construct_error_message += ":warning:" + if error_code: + # noinspection PyUnusedLocal + non_fatal_committee_mention: str = "committee" - if message: - message = re.sub( - r"<([@&#]?|(@[&#])?)\d+>", - lambda match: f"`{match.group(0)}`", - message.strip(), - ) - construct_error_message += f"\n`{message}`" + with contextlib.suppress(CommitteeRoleDoesNotExistError): + non_fatal_committee_mention = (await bot.committee_role).mention + + construct_error_message = ( + f"**Contact a {non_fatal_committee_mention} member, " + f"referencing error code: {error_code}**\n" + + construct_error_message + ) + + if interaction_name in cls.ERROR_ACTIVITIES: + construct_error_message += ( + f" when trying to {cls.ERROR_ACTIVITIES[interaction_name]}" + ) + + if message: + construct_error_message += ":" + else: + construct_error_message += "." + + construct_error_message += ":warning:" + + if message: + construct_error_message += ( + f"\n`{ + re.sub( + r"<([@&#]?|(@[&#])?)\d+>", + lambda match: f"`{match.group(0)!s}`", + message.strip(), + ) + }`" + ) await responder.respond(content=construct_error_message, view=None) @@ -160,6 +187,13 @@ async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderCompo ).rstrip(": ;"), ) + if is_fatal and error_code: + FATAL_AND_ERROR_CODE_MESSAGE: Final[str] = ( + "Error message was requested to be sent with an error code, " + "despite being marked as a fatal error." + ) + raise ValueError(FATAL_AND_ERROR_CODE_MESSAGE) + @staticmethod async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """ From cea1bd60b3ca71257bb7a1aee6a25026f686b8fd Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Tue, 18 Jun 2024 01:10:34 +0100 Subject: [PATCH 049/128] Add "/config unset" command --- .gitignore | 60 +++++ cogs/change_config.py | 103 ++++++-- cogs/get_token_authorisation.py | 2 +- cogs/induct.py | 2 +- cogs/make_applicant.py | 13 +- cogs/make_member.py | 1 + cogs/ping.py | 1 - cogs/send_introduction_reminders.py | 81 +++---- cogs/startup.py | 2 +- cogs/strike.py | 219 +++++++++--------- config/__init__.py | 47 +++- config/_messages/__init__.py | 4 +- config/_settings/__init__.py | 56 ++++- .../_yaml/custom_scalar_validators.py | 26 +-- exceptions/does_not_exist.py | 4 +- 15 files changed, 413 insertions(+), 208 deletions(-) diff --git a/.gitignore b/.gitignore index 6b3e3b37e..3541cde8e 100644 --- a/.gitignore +++ b/.gitignore @@ -147,14 +147,74 @@ local/ *.db.bak local_stubs/ TeX-Bot-deployment.yaml +TeX-Bot-deployment.yaml.original +TeX-Bot-deployment.yaml.backup +TeX-Bot-deployment.yaml.bkp +TeX-Bot-deployment.yaml.bckp +TeX-Bot-deployment.yaml.bak TeX-Bot-deployment.yml +TeX-Bot-deployment.yml.original +TeX-Bot-deployment.yml.backup +TeX-Bot-deployment.yml.bkp +TeX-Bot-deployment.yml.bckp +TeX-Bot-deployment.yml.bak tex-bot-deployment.yaml +tex-bot-deployment.yaml.original +tex-bot-deployment.yaml.backup +tex-bot-deployment.yaml.bkp +tex-bot-deployment.yaml.bckp +tex-bot-deployment.yaml.bak tex-bot-deployment.yml +tex-bot-deployment.yml.original +tex-bot-deployment.yml.backup +tex-bot-deployment.yml.bkp +tex-bot-deployment.yml.bckp +tex-bot-deployment.yml.bak TeX-Bot-settings.yaml +TeX-Bot-settings.yaml.original +TeX-Bot-settings.yaml.backup +TeX-Bot-settings.yaml.bkp +TeX-Bot-settings.yaml.bckp +TeX-Bot-settings.yaml.bak TeX-Bot-settings.yml +TeX-Bot-settings.yml.original +TeX-Bot-settings.yml.backup +TeX-Bot-settings.yml.bkp +TeX-Bot-settings.yml.bckp +TeX-Bot-settings.yml.bak tex-bot-settings.yaml +tex-bot-settings.yaml.original +tex-bot-settings.yaml.backup +tex-bot-settings.yaml.bkp +tex-bot-settings.yaml.bckp +tex-bot-settings.yaml.bak tex-bot-settings.yml +tex-bot-settings.yml.original +tex-bot-settings.yml.backup +tex-bot-settings.yml.bkp +tex-bot-settings.yml.bckp +tex-bot-settings.yml.bak TeX-Bot-config.yaml +TeX-Bot-config.yaml.original +TeX-Bot-config.yaml.backup +TeX-Bot-config.yaml.bkp +TeX-Bot-config.yaml.bckp +TeX-Bot-config.yaml.bak TeX-Bot-config.yml +TeX-Bot-config.yml.original +TeX-Bot-config.yml.backup +TeX-Bot-config.yml.bkp +TeX-Bot-config.yml.bckp +TeX-Bot-config.yml.bak tex-bot-config.yaml +tex-bot-config.yaml.original +tex-bot-config.yaml.backup +tex-bot-config.yaml.bkp +tex-bot-config.yaml.bckp +tex-bot-config.yaml.bak tex-bot-config.yml +tex-bot-config.yml.original +tex-bot-config.yml.backup +tex-bot-config.yml.bkp +tex-bot-config.yml.bckp +tex-bot-config.yml.bak diff --git a/cogs/change_config.py b/cogs/change_config.py index da759a635..ebfd4a17a 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -85,11 +85,30 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> Set return set(config.CONFIG_SETTINGS_HELPS) + @staticmethod + async def autocomplete_get_unsetable_settings_names(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 + """Autocomplete callable that generates the set of unsetable settings names.""" + if not ctx.interaction.user: + return set() + + try: + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + return set() + except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): + return set() + + return { + setting_name + for setting_name, setting_help + in config.CONFIG_SETTINGS_HELPS.items() + if setting_help.default is not None + } + @staticmethod async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: C901,PLR0911,PLR0912,E501 """Autocomplete callable that generates example values for a configuration setting.""" HAS_CONTEXT: Final[bool] = bool( - ctx.interaction.user and "setting" in ctx.options and ctx.options["setting"], + ctx.interaction.user and "setting" in ctx.options and ctx.options["setting"] # noqa: COM812 ) if not HAS_CONTEXT: return set() @@ -523,18 +542,22 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ), ) - if button_interaction.data["custom_id"] == "set_config_cancel": # type: ignore[index, typeddict-item] - await confirmation_message.edit( - content=( - "Aborting editing config setting: " - f"{config_setting_name.replace("`", "\\`")}" - ), - view=None, - ) - return + match button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] + case "set_config_cancel": + await confirmation_message.edit( + content=( + "Aborting editing config setting: " + f"{config_setting_name.replace("`", "\\`")}" + ), + view=None, + ) + return + + case "set_config_confirm": + pass - if button_interaction.data["custom_id"] != "set_config_confirm": # type: ignore[index, typeddict-item] - raise ValueError + case _: + raise ValueError previous_config_setting_value: str | None = config.view_single_config_setting_value( config_setting_name, @@ -624,4 +647,58 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n view=None, ) - # TODO: Command to unset value (if it is optional) + @change_config.command( + name="unset", + description=( + "Unset the specified configuration setting, " + "so that it returns to its default value." + ), + ) + @discord.option( # type: ignore[no-untyped-call, misc] + name="setting", + description="The name of the configuration setting to unset.", + input_type=str, + autocomplete=discord.utils.basic_autocomplete( + autocomplete_get_unsetable_settings_names, # type: ignore[arg-type] + ), + required=True, + parameter_name="config_setting_name", + ) + @CommandChecks.check_interaction_user_has_committee_role + @CommandChecks.check_interaction_user_in_main_guild + async def unset_config_value(self, ctx: TeXBotApplicationContext, config_setting_name: str) -> None: # noqa: E501 + """Definition & callback response of the "unset_config_value" command.""" + if config_setting_name not in config.CONFIG_SETTINGS_HELPS: + await self.command_send_error( + ctx, + message=f"Invalid setting: {config_setting_name!r}", + ) + return + + if config.CONFIG_SETTINGS_HELPS[config_setting_name].default is None: + await self.command_send_error( + ctx, + message=( + f"Setting {config_setting_name!r} cannot be unset, " + "because it has no default value" + ), + ) + return + + try: + await config.remove_single_config_setting_value(config_setting_name) + except KeyError: + await self.command_send_error( + ctx, + message=f"Setting {config_setting_name!r} already has the default value", + ) + return + + await ctx.respond( + content=( + f"Successfully unset setting `{ + config_setting_name.replace("`", "\\`") + }`" + ), + ephemeral=True, + ) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 8ca8ab8cd..7a217b02c 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -121,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 c62c8295f..bf98eba94 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -231,7 +231,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb message_already_sent: bool = False message: discord.Message - async for message in general_channel.history(limit=7): + async for message in general_channel.history(limit=10): if message.author == self.bot.user and "grab your roles" in message.content: message_already_sent = True break diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index adcdcf067..da8ae618d 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -25,7 +25,7 @@ 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. """ @@ -59,7 +59,6 @@ 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) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") @@ -91,7 +90,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. @@ -124,7 +123,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=( @@ -163,10 +161,10 @@ 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. @@ -176,7 +174,7 @@ async def user_make_applicant(self, ctx: TeXBotApplicationContext, member: disco """ 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 @@ -198,5 +196,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 d60588e62..2fec7698a 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -259,6 +259,7 @@ 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 diff --git a/cogs/ping.py b/cogs/ping.py index 000b8ec87..dc3fbd787 100644 --- a/cogs/ping.py +++ b/cogs/ping.py @@ -19,7 +19,6 @@ class PingCommandCog(TeXBotBaseCog): @discord.slash_command(description="Replies with Pong!") # type: ignore[no-untyped-call, misc] async def ping(self, ctx: TeXBotApplicationContext) -> None: """Definition & callback response of the "ping" command.""" - raise Exception await ctx.respond( random.choices( [ diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 7b5e7a998..044f90ba4 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -5,6 +5,7 @@ __all__: Sequence[str] = ("SendIntroductionRemindersTaskCog",) +import datetime import functools import logging from logging import Logger @@ -61,6 +62,34 @@ async def on_ready(self) -> None: self.OptOutIntroductionRemindersView(self.bot), ) + @classmethod + async def _check_if_member_needs_reminder(cls, member_id: int, member_joined_at: datetime.datetime) -> bool: # noqa: E501 + MEMBER_NEEDS_ONE_OFF_REMINDER: Final[bool] = ( + settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "once" + and not await ( + await SentOneOffIntroductionReminderMember.objects.afilter( + discord_id=member_id, + ) + ).aexists() + ) + MEMBER_NEEDS_RECURRING_REMINDER: Final[bool] = ( + settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "interval" + ) + MEMBER_RECENTLY_JOINED: Final[bool] = ( + (discord.utils.utcnow() - member_joined_at) + <= settings["SEND_INTRODUCTION_REMINDERS_DELAY"] + ) + MEMBER_OPTED_OUT_FROM_REMINDERS: Final[bool] = await ( + await IntroductionReminderOptOutMember.objects.afilter( + discord_id=member_id, + ) + ).aexists() + return ( + (MEMBER_NEEDS_ONE_OFF_REMINDER or MEMBER_NEEDS_RECURRING_REMINDER) + and not MEMBER_RECENTLY_JOINED + and not MEMBER_OPTED_OUT_FROM_REMINDERS + ) + @tasks.loop(seconds=settings["SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] @functools.partial( ErrorCaptureDecorators.capture_error_and_close, @@ -98,44 +127,18 @@ async def send_introduction_reminders(self) -> None: ) continue - member_needs_one_off_reminder: bool = ( - settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "once" - and not await ( - await SentOneOffIntroductionReminderMember.objects.afilter( - discord_id=member.id, - ) - ).aexists() - ) - member_needs_recurring_reminder: bool = ( - settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "interval" - ) - member_recently_joined: bool = ( - (discord.utils.utcnow() - member.joined_at) - <= settings["SEND_INTRODUCTION_REMINDERS_DELAY"] - ) - member_opted_out_from_reminders: bool = await ( - await IntroductionReminderOptOutMember.objects.afilter( - discord_id=member.id, - ) - ).aexists() - member_needs_reminder: bool = ( - (member_needs_one_off_reminder or member_needs_recurring_reminder) - and not member_recently_joined - and not member_opted_out_from_reminders - ) - - if not member_needs_reminder: + if not await self._check_if_member_needs_reminder(member.id, member.joined_at): continue 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 @@ -225,24 +228,24 @@ 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 diff --git a/cogs/startup.py b/cogs/startup.py index 5c005b4ed..ea7731f39 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -116,7 +116,7 @@ async def _check_strike_performed_manually_warning_location_exists(self) -> None discord.utils.get( self.bot.main_guild.text_channels, name=settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"], - ), + ) # noqa: COM812 ) if STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_EXISTS: return diff --git a/cogs/strike.py b/cogs/strike.py index dd03ff2cd..89188e3f7 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -271,34 +271,35 @@ async def _confirm_perform_moderation_action(self, message_sender_component: Mes ), ) - if button_interaction.data["custom_id"] == "no_strike_member": # type: ignore[index, typeddict-item] - await button_interaction.edit_original_response( - content=( - "Aborted performing " - f"{self.SUGGESTED_ACTIONS[actual_strike_amount]} action " - f"on {strike_user.mention}." - ), - view=None, - ) - return + match button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] + case "no_strike_member": + await button_interaction.edit_original_response( + content=( + "Aborted performing " + f"{self.SUGGESTED_ACTIONS[actual_strike_amount]} action " + f"on {strike_user.mention}." + ), + view=None, + ) - if button_interaction.data["custom_id"] == "yes_strike_member": # type: ignore[index, typeddict-item] - await perform_moderation_action( - strike_user, - actual_strike_amount, - committee_member=interaction_user, - ) + case "yes_strike_member": + await perform_moderation_action( + strike_user, + actual_strike_amount, + committee_member=interaction_user, + ) - await button_interaction.edit_original_response( - content=( - f"Successfully performed {self.SUGGESTED_ACTIONS[actual_strike_amount]} " - f"action on {strike_user.mention}." - ), - view=None, - ) - return + await button_interaction.edit_original_response( + content=( + "Successfully performed " + f"{self.SUGGESTED_ACTIONS[actual_strike_amount]} " + f"action on {strike_user.mention}." + ), + view=None, + ) - raise ValueError + case _: + raise ValueError 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): @@ -511,11 +512,11 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M ) )[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 { @@ -557,53 +558,53 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M ), ) - if out_of_sync_ban_button_interaction.data["custom_id"] == "no_out_of_sync_ban_member": # type: ignore[index, typeddict-item] # noqa: E501 - await out_of_sync_ban_confirmation_message.edit( - content=( - f"Aborted performing ban action upon {strike_user.mention}. " - "(This manual moderation action has not been tracked.)\n" - "ᴛʜɪs ᴍᴇssᴀɢᴇ ᴡɪʟʟ ʙᴇ ᴅᴇʟᴇᴛᴇᴅ" - f"""{ - discord.utils.format_dt( - discord.utils.utcnow() + datetime.timedelta(minutes=2), - "R" - ) - }""" - ), - view=None, - ) - await asyncio.sleep(118) - await out_of_sync_ban_confirmation_message.delete() - return + match out_of_sync_ban_button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] + case "no_out_of_sync_ban_member": + await out_of_sync_ban_confirmation_message.edit( + content=( + f"Aborted performing ban action upon {strike_user.mention}. " + "(This manual moderation action has not been tracked.)\n" + "ᴛʜɪs ᴍᴇssᴀɢᴇ ᴡɪʟʟ ʙᴇ ᴅᴇʟᴇᴛᴇᴅ" + f"""{ + discord.utils.format_dt( + discord.utils.utcnow() + datetime.timedelta(minutes=2), + "R" + ) + }""" + ), + view=None, + ) - if out_of_sync_ban_button_interaction.data["custom_id"] == "yes_out_of_sync_ban_member": # type: ignore[index, typeddict-item] # noqa: E501 - await self._send_strike_user_message(strike_user, member_strikes) - await main_guild.ban( - strike_user, - reason=( - f"**{applied_action_user.display_name} synced moderation action " - "with number of strikes**" - ), - ) - await out_of_sync_ban_confirmation_message.edit( - content=( - f"Successfully banned {strike_user.mention}.\n" - "**Please ensure you use the `/strike` command in future!**" - "\nᴛʜɪs ᴍᴇssᴀɢᴇ ᴡɪʟʟ ʙᴇ ᴅᴇʟᴇᴛᴇᴅ" - f"""{ - discord.utils.format_dt( - discord.utils.utcnow() + datetime.timedelta(minutes=2), - "R" - ) - }""" - ), - view=None, - ) - await asyncio.sleep(118) - await out_of_sync_ban_confirmation_message.delete() - return + case "yes_out_of_sync_ban_member": + await self._send_strike_user_message(strike_user, member_strikes) + await main_guild.ban( + strike_user, + reason=( + f"**{applied_action_user.display_name} synced moderation action " + "with number of strikes**" + ), + ) + await out_of_sync_ban_confirmation_message.edit( + content=( + f"Successfully banned {strike_user.mention}.\n" + "**Please ensure you use the `/strike` command in future!**" + "\nᴛʜɪs ᴍᴇssᴀɢᴇ ᴡɪʟʟ ʙᴇ ᴅᴇʟᴇᴛᴇᴅ" + f"""{ + discord.utils.format_dt( + discord.utils.utcnow() + datetime.timedelta(minutes=2), + "R" + ) + }""" + ), + view=None, + ) - raise ValueError + case _: + raise ValueError + + await asyncio.sleep(118) + await out_of_sync_ban_confirmation_message.delete() + return confirmation_message: discord.Message = await confirmation_message_channel.send( content=( @@ -645,41 +646,45 @@ 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] - await confirmation_message.edit( - content=( - f"Aborted increasing {strike_user.mention}'s strikes " - "& sending moderation alert message. " - "(This manual moderation action has not been tracked.)\n" - "ᴛʜɪs ᴍᴇssᴀɢᴇ ᴡɪʟʟ ʙᴇ ᴅᴇʟᴇᴛᴇᴅ" - f"""{ - discord.utils.format_dt( - discord.utils.utcnow() + datetime.timedelta(minutes=2), - "R" - ) - }""" - ), - view=None, - ) - await asyncio.sleep(118) - await confirmation_message.delete() - 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) - if not interaction_user: - raise StrikeTrackingError + match button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] + case "no_manual_moderation_action": + await confirmation_message.edit( + content=( + f"Aborted increasing {strike_user.mention}'s strikes " + "& sending moderation alert message. " + "(This manual moderation action has not been tracked.)\n" + "ᴛʜɪs ᴍᴇssᴀɢᴇ ᴡɪʟʟ ʙᴇ ᴅᴇʟᴇᴛᴇᴅ" + f"""{ + discord.utils.format_dt( + discord.utils.utcnow() + datetime.timedelta(minutes=2), + "R" + ) + }""" + ), + view=None, + ) + await asyncio.sleep(118) + await confirmation_message.delete() - await self._confirm_increase_strike( - message_sender_component=ChannelMessageSender(confirmation_message_channel), - interaction_user=interaction_user, - strike_user=strike_user, - member_strikes=member_strikes, - button_callback_channel=confirmation_message_channel, - perform_action=False, - ) + case "yes_manual_moderation_action": + interaction_user: discord.User | None = self.bot.get_user( + applied_action_user.id, + ) + if not interaction_user: + raise StrikeTrackingError + + await self._confirm_increase_strike( + message_sender_component=ChannelMessageSender(confirmation_message_channel), + interaction_user=interaction_user, + strike_user=strike_user, + member_strikes=member_strikes, + button_callback_channel=confirmation_message_channel, + perform_action=False, + ) + # NOTE: Message deletion is performed within self._confirm_increase_strike() - raise ValueError + case _: + raise ValueError @TeXBotBaseCog.listener() @capture_guild_does_not_exist_error @@ -712,7 +717,7 @@ async def on_member_remove(self, member: discord.Member) -> None: 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 diff --git a/config/__init__.py b/config/__init__.py index f980f3d4b..b94a1e314 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -12,6 +12,8 @@ "MESSAGES_LOCALE_CODES", "LogLevels", "run_setup", + "reload_settings", + "load_messages", "settings", "check_for_deprecated_environment_variables", "messages", @@ -20,6 +22,7 @@ "get_settings_file_path", "view_single_config_setting_value", "assign_single_config_setting_value", + "remove_single_config_setting_value", ) @@ -53,8 +56,8 @@ def run_setup() -> None: """Execute the setup functions required, before other modules can be run.""" check_for_deprecated_environment_variables() - async_to_sync(settings.reload)() - async_to_sync(messages.load)(settings["MESSAGES_LOCALE_CODE"]) + async_to_sync(reload_settings)() + async_to_sync(load_messages)(settings["MESSAGES_LOCALE_CODE"]) logger.debug("Begin database setup") @@ -152,26 +155,46 @@ def check_for_deprecated_environment_variables() -> None: ) deprecated_environment_variable_name: str for deprecated_environment_variable_name in DEPRECATED_ENVIRONMENT_VARIABLE_NAMES: - deprecated_environment_variable_found: bool = bool( - ( - deprecated_environment_variable_name.upper() in os.environ - or deprecated_environment_variable_name.lower() in os.environ - or f"TEX_BOT_{deprecated_environment_variable_name}".upper() in os.environ - or f"TEX_BOT_{deprecated_environment_variable_name}".lower() in os.environ - ), + DEPRECATED_ENVIRONMENT_VARIABLE_FOUND: bool = bool( + deprecated_environment_variable_name.upper() in os.environ + or deprecated_environment_variable_name.lower() in os.environ + or f"TEX_BOT_{deprecated_environment_variable_name}".upper() in os.environ + or f"TEX_BOT_{deprecated_environment_variable_name}".lower() in os.environ # noqa: COM812 ) - if deprecated_environment_variable_found: + if DEPRECATED_ENVIRONMENT_VARIABLE_FOUND: raise CONFIGURATION_VIA_ENVIRONMENT_VARIABLES_IS_DEPRECATED_ERROR +async def reload_settings() -> None: + """Reload any configuration settings into the settings tree.""" + # noinspection PyProtectedMember + await settings._public_reload() # noqa: SLF001 + + def view_single_config_setting_value(config_setting_name: str) -> str | None: """Return the value of a single configuration setting from settings tree hierarchy.""" - return settings.view_single_raw_value(config_setting_name=config_setting_name) + # noinspection PyProtectedMember + return settings._public_view_single_raw_value(config_setting_name=config_setting_name) # noqa: SLF001 async def assign_single_config_setting_value(config_setting_name: str, new_config_setting_value: str) -> None: # noqa: E501 """Set the value of a single configuration setting within settings tree hierarchy.""" - return await settings.assign_single_raw_value( + # noinspection PyProtectedMember + return await settings._public_assign_single_raw_value( # noqa: SLF001 config_setting_name=config_setting_name, new_config_setting_value=new_config_setting_value, ) + + +async def remove_single_config_setting_value(config_setting_name: str) -> None: + """Unset the value of a single configuration setting within settings tree hierarchy.""" + # noinspection PyProtectedMember + return await settings._public_remove_single_raw_value( # noqa: SLF001 + config_setting_name=config_setting_name, + ) + + +async def load_messages(messages_locale_code: str) -> None: + """Load the messages defined in the language file.""" + # noinspection PyProtectedMember + await messages._public_load(messages_locale_code=messages_locale_code) # noqa: SLF001 diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index 671a432ec..32f13c16f 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -17,7 +17,7 @@ class MessagesAccessor: _messages_already_loaded: ClassVar[bool] = False @classmethod - def format_invalid_message_id_message(cls, item: str) -> str: + def _format_invalid_message_id_message(cls, item: str) -> str: """Return the message to state that the given message ID is invalid.""" return f"{item!r} is not a valid message ID." @@ -63,7 +63,7 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 raise KeyError(key_error_message) from None @classmethod - async def load(cls, messages_locale_code: str) -> None: + async def _public_load(cls, messages_locale_code: str) -> None: if messages_locale_code not in MESSAGES_LOCALE_CODES: INVALID_MESSAGES_LOCALE_CODE_MESSAGE: Final[str] = ( f"{"messages_locale_code"!r} must be one of " diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 48523cd59..4e3634c8c 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -10,7 +10,6 @@ __all__: Sequence[str] = ("get_settings_file_path", "SettingsAccessor") - import logging import re from collections.abc import Iterable, Mapping @@ -46,7 +45,7 @@ class SettingsAccessor: _most_recent_yaml: ClassVar[YAML | None] = None @classmethod - def format_invalid_settings_key_message(cls, item: str) -> str: + def _get_invalid_settings_key_message(cls, item: str) -> str: """Return the message to state that the given settings key is invalid.""" return f"{item!r} is not a valid settings key." @@ -73,14 +72,14 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if item not in self._settings: INVALID_SETTINGS_KEY_MESSAGE: Final[str] = ( - self.format_invalid_settings_key_message(item) + self._get_invalid_settings_key_message(item) ) raise AttributeError(INVALID_SETTINGS_KEY_MESSAGE) ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: Final[bool] = bool( "bot" in item.lower() and "token" in item.lower() - and utils.is_running_in_async(), + and utils.is_running_in_async() # noqa: COM812 ) if ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: TEX_BOT_ALREADY_RUNNING_MESSAGE: Final[str] = ( @@ -99,7 +98,7 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 key_error_message: str = item ERROR_WAS_FROM_INVALID_KEY_NAME: Final[bool] = ( - self.format_invalid_settings_key_message(item) in str( + self._get_invalid_settings_key_message(item) in str( attribute_not_exist_error, ) ) @@ -109,7 +108,7 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 raise KeyError(key_error_message) from None @classmethod - async def reload(cls) -> None: + async def _public_reload(cls) -> None: settings_file_path: AsyncPath = await utils.get_settings_file_path() current_yaml: YAML = load_yaml( await settings_file_path.read_text(), @@ -920,7 +919,7 @@ def _get_mapping_value(cls, partial_config_setting_name: str, partial_yaml_setti YAML_CHILD_IS_MAPPING: Final[bool] = bool( single_yaml_mapping_setting is not None - and single_yaml_mapping_setting.is_mapping(), + and single_yaml_mapping_setting.is_mapping() # noqa: COM812 ) if YAML_CHILD_IS_MAPPING: return cls._get_mapping_value(remainder, single_yaml_mapping_setting) @@ -928,7 +927,7 @@ def _get_mapping_value(cls, partial_config_setting_name: str, partial_yaml_setti return cls._get_scalar_value(partial_config_setting_name, partial_yaml_settings_tree) @classmethod - def view_single_raw_value(cls, config_setting_name: str) -> str | None: + def _public_view_single_raw_value(cls, config_setting_name: str) -> str | None: """Return the value of a single configuration setting from settings tree hierarchy.""" current_yaml: YAML | None = cls._most_recent_yaml if current_yaml is None: @@ -1052,7 +1051,7 @@ def _set_required_value_from_validator(cls, partial_config_setting_name: str | N raise RuntimeError(UNKNOWN_CONFIG_TYPE_MESSAGE) @classmethod - async def assign_single_raw_value(cls, config_setting_name: str, new_config_setting_value: str) -> None: # noqa: E501 + async def _public_assign_single_raw_value(cls, config_setting_name: str, new_config_setting_value: str) -> None: # noqa: E501 """Set the value of a single configuration setting within settings tree hierarchy.""" current_yaml: YAML | None = cls._most_recent_yaml if current_yaml is None: @@ -1074,3 +1073,42 @@ async def assign_single_raw_value(cls, config_setting_name: str, new_config_sett ) from config_setting_error await (await utils.get_settings_file_path()).write_text(current_yaml.as_yaml()) + + @classmethod + def _remove_value(cls, partial_config_setting_name: str, partial_yaml_settings_tree: YAML) -> YAML: # type: ignore[misc] # noqa: E501 + if ":" not in partial_config_setting_name: + del partial_yaml_settings_tree[partial_config_setting_name] + return partial_yaml_settings_tree + + key: str + remainder: str + key, _, remainder = partial_config_setting_name.partition(":") + + if not partial_yaml_settings_tree[key].is_mapping(): + EXPECTED_MAPPING_IN_YAML_MESSAGE: Final[str] = "Found non-mapping." + raise RuntimeError(EXPECTED_MAPPING_IN_YAML_MESSAGE) + + removed_value: YAML = cls._remove_value( + remainder, + partial_yaml_settings_tree[key], + ) + + if not removed_value.data: + del partial_yaml_settings_tree[key] + else: + partial_yaml_settings_tree[key] = removed_value.data + return partial_yaml_settings_tree + + @classmethod + async def _public_remove_single_raw_value(cls, config_setting_name: str) -> None: + """Unset the value of a single configuration setting within settings tree hierarchy.""" + current_yaml: YAML | None = cls._most_recent_yaml + if current_yaml is None: + YAML_NOT_LOADED_MESSAGE: Final[str] = ( + "Invalid state: Config YAML has not yet been loaded." + ) + raise RuntimeError(YAML_NOT_LOADED_MESSAGE) + + current_yaml = cls._remove_value(config_setting_name, current_yaml) + + await (await utils.get_settings_file_path()).write_text(current_yaml.as_yaml()) diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index 6752886b5..a7219a1cf 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -69,10 +69,8 @@ class DiscordWebhookURLValidator(strictyaml.Url): # type: ignore[misc] def validate_scalar(self, chunk: YAMLChunk) -> str: # type: ignore[misc] # noinspection PyUnresolvedReferences CHUNK_IS_VALID: Final[bool] = bool( - ( - self._Url__is_absolute_url(chunk.contents) - and chunk.contents.startswith("https://discord.com/api/webhooks/") - ), + self._Url__is_absolute_url(chunk.contents) + and chunk.contents.startswith("https://discord.com/api/webhooks/") # noqa: COM812 ) if not CHUNK_IS_VALID: chunk.expecting_but_found("when expecting a Discord webhook URL") @@ -86,10 +84,8 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] # noinspection PyUnresolvedReferences DATA_IS_VALID: Final[bool] = bool( - ( - self._Url__is_absolute_url(data) - and data.startswith("https://discord.com/api/webhooks/") # type: ignore[attr-defined] - ), + self._Url__is_absolute_url(data) + and data.startswith("https://discord.com/api/webhooks/") # type: ignore[attr-defined] # noqa: COM812 ) if not DATA_IS_VALID: INVALID_DATA_MESSAGE: Final[str] = f"'{data}' is not a Discord webhook URL." @@ -112,11 +108,9 @@ def validate_scalar(self, chunk: YAMLChunk) -> int: # type: ignore[misc] @override def to_yaml(self, data: object) -> str: # type: ignore[misc] DATA_IS_VALID: Final[bool] = bool( - ( - (strictyaml_utils.is_string(data) or isinstance(data, int)) - and strictyaml_utils.is_integer(str(data)) - and re.fullmatch(r"\A\d{17,20}\Z", str(data)) - ), + (strictyaml_utils.is_string(data) or isinstance(data, int)) + and strictyaml_utils.is_integer(str(data)) + and re.fullmatch(r"\A\d{17,20}\Z", str(data)) # noqa: COM812 ) if not DATA_IS_VALID: INVALID_DATA_MESSAGE: Final[str] = f"'{data}' is not a Discord snowflake ID." @@ -298,6 +292,12 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] if not isinstance(total_seconds, float): raise TypeError + if (total_seconds / 3600) % 1 == 0: + return f"{int(total_seconds / 3600)}h" + + if total_seconds % 1 == 0: + return f"{int(total_seconds)}s" + return f"{total_seconds}s" diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index e7e044edc..eb020b757 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -92,7 +92,7 @@ def ROLE_NAME(cls) -> str: # noqa: N802,N805 def __init__(self, message: str | None = None) -> None: """Initialize 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: @@ -259,7 +259,7 @@ def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 def __init__(self, message: str | None = None) -> None: """Initialize 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: From 8e29da68ace62b1322e8ecddbfb5900649b2434c Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Tue, 18 Jun 2024 16:04:02 +0100 Subject: [PATCH 050/128] Implement config file changes watcher --- cogs/__init__.py | 9 +- cogs/change_config.py | 210 +++++++++++++++++- cogs/get_token_authorisation.py | 8 +- cogs/remind_me.py | 4 +- cogs/send_get_roles_reminders.py | 4 +- cogs/send_introduction_reminders.py | 5 +- config/_settings/__init__.py | 39 +++- config/_settings/_yaml/__init__.py | 4 + .../_yaml/custom_scalar_validators.py | 10 +- config/constants.py | 24 +- utils/suppress_traceback.py | 2 + utils/tex_bot.py | 3 +- utils/tex_bot_base_cog.py | 3 +- 13 files changed, 281 insertions(+), 44 deletions(-) diff --git a/cogs/__init__.py b/cogs/__init__.py index 45576ca9c..5078ad61a 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -10,7 +10,7 @@ __all__: Sequence[str] = ( "ArchiveCommandCog", "ConfigChangeCommandsCog", - "GetTokenAuthorisationCommand", + "GetTokenAuthorisationCommandCog", "CommandErrorCog", "DeleteAllCommandsCog", "EditMessageCommandCog", @@ -41,11 +41,11 @@ from typing import TYPE_CHECKING from cogs.archive import ArchiveCommandCog -from cogs.change_config import ConfigChangeCommandsCog +from cogs.change_config import CheckConfigFileChangedTaskCog, ConfigChangeCommandsCog 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, @@ -76,7 +76,8 @@ def setup(bot: TeXBot) -> None: """Add all the cogs to the bot, at bot startup.""" cogs: Iterable[type[TeXBotBaseCog]] = ( ArchiveCommandCog, - GetTokenAuthorisationCommand, + GetTokenAuthorisationCommandCog, + CheckConfigFileChangedTaskCog, ConfigChangeCommandsCog, CommandErrorCog, DeleteAllCommandsCog, diff --git a/cogs/change_config.py b/cogs/change_config.py index ebfd4a17a..a2914d618 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -2,25 +2,31 @@ from collections.abc import Sequence -__all__: Sequence[str] = ("ConfigChangeCommandsCog",) +__all__: Sequence[str] = ("CheckConfigFileChangedTaskCog", "ConfigChangeCommandsCog") import contextlib import itertools import logging +import os import random import re +import stat import urllib.parse from collections.abc import MutableSequence, Set +from io import BytesIO from logging import Logger -from typing import Final +from typing import Final, NamedTuple, Self, override import discord +from aiopath import AsyncPath +from anyio import AsyncFile +from discord.ext import tasks from discord.ui import View from strictyaml import StrictYAMLError import config -from config import CONFIG_SETTINGS_HELPS, ConfigSettingHelp, LogLevels +from config import CONFIG_SETTINGS_HELPS, ConfigSettingHelp, LogLevels, settings from config.constants import MESSAGES_LOCALE_CODES, SendIntroductionRemindersFlagType from exceptions import ( ChangingSettingWithRequiredSiblingError, @@ -33,6 +39,7 @@ EditorResponseComponent, GenericResponderComponent, SenderResponseComponent, + TeXBot, TeXBotApplicationContext, TeXBotAutocompleteContext, TeXBotBaseCog, @@ -41,6 +48,46 @@ logger: Final[Logger] = logging.getLogger("TeX-Bot") +class FileStats(NamedTuple): + """Container to hold stats information about a single file.""" + + type: int + size: int + modified_time: float + + @classmethod + async def _public_from_file_path(cls, file_path: AsyncPath) -> Self: # type: ignore[misc] + return cls._public_from_full_stats(await file_path.stat()) + + @classmethod + def _public_from_full_stats(cls, full_stats: os.stat_result) -> Self: + file_type: int = stat.S_IFMT(full_stats.st_mode) + if file_type != stat.S_IFREG: + INVALID_FILE_TYPE_MESSAGE: Final[str] = "File type must be 'S_IFREG'." + raise ValueError(INVALID_FILE_TYPE_MESSAGE) + + return cls( + type=file_type, + size=full_stats.st_size, + modified_time=full_stats.st_mtime, + ) + + +class FileComparer(NamedTuple): + """Container to hold all the information to compare one file to another.""" + + stats: FileStats + raw_content: bytes + + @classmethod + async def _public_from_file_path(cls, file_path: AsyncPath) -> Self: # type: ignore[misc] + # noinspection PyProtectedMember + return cls( + stats=await FileStats._public_from_file_path(file_path), # noqa: SLF001 + raw_content=await file_path.read_bytes(), + ) + + class ConfirmSetConfigSettingValueView(View): """A discord.View containing two buttons to confirm setting a given config setting.""" @@ -63,6 +110,109 @@ async def cancel_set_config_button_callback(self, _: discord.Button, interaction logger.debug("\"No\" button pressed. %s", interaction) +class CheckConfigFileChangedTaskCog(TeXBotBaseCog): + """Cog class that defines the check_config_file_changed task.""" + + _STATS_CACHE: Final[dict[tuple[FileStats, FileStats], bool]] = {} + + @override + def __init__(self, bot: TeXBot) -> None: + """Start all task managers when this cog is initialised.""" + self._previous_file_comparer: FileComparer | None = None + + self.check_config_file_changed.start() + + super().__init__(bot) + + @override + def cog_unload(self) -> None: + """ + Unload hook that ends all running tasks whenever the tasks cog is unloaded. + + This may be run dynamically or when the bot closes. + """ + self.check_config_file_changed.cancel() + + @classmethod + async def _file_raw_contents_is_same(cls, current_file: AsyncFile[bytes], previous_raw_contents: bytes) -> bool: # noqa: E501 + BUFFER_SIZE: Final[int] = 8*1024 + + previous_file: BytesIO = BytesIO(previous_raw_contents) + + while True: + partial_current_contents: bytes = await current_file.read(BUFFER_SIZE) + partial_previous_contents: bytes = previous_file.read(BUFFER_SIZE) + + if partial_current_contents != partial_previous_contents: + return False + if not partial_current_contents: + return True + + @classmethod + async def _check_config_actually_is_same(cls, previous_file_comparer: FileComparer) -> bool: # noqa: E501 + SETTINGS_FILE_PATH: Final[AsyncPath] = ( + await config._settings.utils.get_settings_file_path() + ) + # noinspection PyProtectedMember + current_file_stats: FileStats = await FileStats._public_from_file_path( # noqa: SLF001 + SETTINGS_FILE_PATH, + ) + + if current_file_stats.size != previous_file_comparer.stats.size: + return False + + outcome: bool | None = cls._STATS_CACHE.get( + (current_file_stats, previous_file_comparer.stats), + None, + ) + if outcome is not None: + return outcome + + async with SETTINGS_FILE_PATH.open("rb") as current_file: + outcome = await cls._file_raw_contents_is_same( + current_file, + previous_file_comparer.raw_content, + ) + + if len(cls._STATS_CACHE) > 100: + cls._STATS_CACHE.clear() + + cls._STATS_CACHE[(current_file_stats, previous_file_comparer.stats)] = outcome + return outcome + + @tasks.loop(seconds=settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"]) + async def check_config_file_changed(self) -> None: + """Recurring task to check whether the config settings file has changed.""" + if self._previous_file_comparer is None: + # noinspection PyProtectedMember + self._previous_file_comparer = await FileComparer._public_from_file_path( # noqa: SLF001 + await config._settings.utils.get_settings_file_path(), + ) + return + + if await self._check_config_actually_is_same(self._previous_file_comparer): + return + + # noinspection PyProtectedMember + self._previous_file_comparer = await FileComparer._public_from_file_path( # noqa: SLF001 + await config._settings.utils.get_settings_file_path(), + ) + + raise NotImplementedError # TODO: reload/update changes + + # { + # config_setting_name + # for config_setting_name, config_setting_help + # in CONFIG_SETTINGS_HELPS.items() + # if config_setting_help.requires_restart_after_changed + # } + + @check_config_file_changed.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() + + class ConfigChangeCommandsCog(TeXBotBaseCog): """Cog class that defines the "/config" command group and command call-back methods.""" @@ -71,6 +221,18 @@ class ConfigChangeCommandsCog(TeXBotBaseCog): description="Display, edit and get help about TeX-Bot's configuration.", ) + @classmethod + def get_formatted_change_delay_message(cls) -> str: + return ( + f"Changes could take up to { + ( + str(int(settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1)) + if (settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1) % 1 == 0 + else f"{settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1:.2f}" + ) + } seconds to take effect." + ) + @staticmethod async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """Autocomplete callable that generates the set of available settings names.""" @@ -213,15 +375,40 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext return {"true", "false"} SETTING_NAME_IS_TIMEDELTA: Final[bool] = ( - ":timeout-duration" in setting_name - or ":delay" in setting_name - or ":interval" in setting_name + any( + part in setting_name + for part + in ( + ":timeout-duration:", + ":delay:", + ":interval:", + ":timeout-duration-", + ":delay-", + ":interval-", + "-timeout-duration:", + "-delay:", + "-interval:", + ) + ) + or setting_name.endswith( + ( + ":timeout-duration", + ":delay", + ":interval", + "-timeout-duration", + "-delay", + "-interval", + ), + ) ) if SETTING_NAME_IS_TIMEDELTA: - timedelta_scales: MutableSequence[str] = ["s", "m", "h"] + timedelta_scales: MutableSequence[str] = ["s", "m"] - if ":timeout-duration" in setting_name or ":delay" in setting_name: - timedelta_scales.extend(["d", "w"]) + if setting_name != "check-if-config-changed-interval": + timedelta_scales.extend(["h"]) + + if any(part in setting_name for part in ("timeout-duration", "delay")): + timedelta_scales.extend(["d", "w"]) return { "".join( @@ -641,8 +828,7 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n if changed_config_setting_value else "**to be not set**." ) - }\n\n" - "Changes could take up to ??? to take effect." # TODO: Retrieve update time from task + }\n\n{self.get_formatted_change_delay_message()}" ), view=None, ) @@ -698,7 +884,7 @@ async def unset_config_value(self, ctx: TeXBotApplicationContext, config_setting content=( f"Successfully unset setting `{ config_setting_name.replace("`", "\\`") - }`" + }`\n\n{self.get_formatted_change_delay_message()}" ), ephemeral=True, ) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 7a217b02c..75ead0f7f 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. """ diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 6f8c3e904..b4b99072d 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -12,7 +12,7 @@ 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 @@ -262,12 +262,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. diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index d80204188..3bcadd700 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 @@ -34,6 +34,7 @@ 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_ENABLED"]: @@ -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. diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 044f90ba4..8744d932a 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -9,7 +9,7 @@ import functools import logging from logging import Logger -from typing import Final +from typing import Final, override import discord import emoji @@ -37,6 +37,7 @@ 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_ENABLED"]: @@ -47,6 +48,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. @@ -188,6 +190,7 @@ class OptOutIntroductionRemindersView(View): joining your group's Discord guild. """ + @override def __init__(self, bot: TeXBot) -> None: """Initialize a new discord.View, to opt-in/out of introduction reminders.""" self.bot: TeXBot = bot diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 4e3634c8c..a6fb46160 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -185,6 +185,9 @@ async def _public_reload(cls) -> None: cls._reload_send_get_roles_reminders_interval( current_yaml["reminders"]["send-get-roles-reminders"]["interval"], ), + cls._reload_check_if_config_changed_interval( + current_yaml["check-if-config-changed-interval"], + ), ) cls._most_recent_yaml = current_yaml @@ -203,11 +206,18 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # if not CONSOLE_LOGGING_SETTINGS_CHANGED: return set() + ALL_HANDLERS: Iterable[logging.Handler] = logger.handlers + + console_logging_handler: logging.StreamHandler[TextIO] = logging.StreamHandler() + stream_handlers: set[logging.StreamHandler[TextIO]] = { handler for handler - in logger.handlers - if isinstance(handler, logging.StreamHandler) + in ALL_HANDLERS + if ( + isinstance(handler, type(console_logging_handler)) + and handler.stream == console_logging_handler.stream + ) } if len(stream_handlers) > 1: CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE: Final[str] = ( @@ -215,8 +225,6 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # ) raise ValueError(CANNOT_DETERMINE_LOGGING_HANDLER_MESSAGE) - console_logging_handler: logging.StreamHandler[TextIO] = logging.StreamHandler() - if len(stream_handlers) == 0: # noinspection SpellCheckingInspection console_logging_handler.setFormatter( @@ -851,6 +859,29 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte return {"reminders:send-get-roles-reminders:interval"} + @classmethod + def _reload_check_if_config_changed_interval(cls, check_if_config_changed_interval: YAML) -> set[str]: # type: ignore[misc] # noqa: E501 + """ + Reload the interval of time between executing the task to send check if config changed. + + Returns the set of settings keys that have been changed. + """ + CHECK_IF_CONFIG_CHANGED_INTERVAL_CHANGED: Final[bool] = bool( + cls._most_recent_yaml is None + or "CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS" not in cls._settings + or check_if_config_changed_interval != cls._most_recent_yaml[ + "check-if-config-changed-interval" + ] # noqa: COM812 + ) + if not CHECK_IF_CONFIG_CHANGED_INTERVAL_CHANGED: + return set() + + cls._settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] = ( + check_if_config_changed_interval.data.total_seconds() + ) + + return {"check-if-config-changed-interval"} + @classmethod def _get_scalar_value(cls, config_setting_name: str, yaml_settings_tree: YAML) -> str | None: # type: ignore[misc] # noqa: E501 single_yaml_scalar_setting: YAML | None = yaml_settings_tree.get( diff --git a/config/_settings/_yaml/__init__.py b/config/_settings/_yaml/__init__.py index 38cc9cc26..f04575e60 100644 --- a/config/_settings/_yaml/__init__.py +++ b/config/_settings/_yaml/__init__.py @@ -18,6 +18,7 @@ from strictyaml import YAML from config.constants import ( + DEFAULT_CHECK_IF_CONFIG_CHANGED_INTERVAL, DEFAULT_CONSOLE_LOG_LEVEL, DEFAULT_DISCORD_LOGGING_LOG_LEVEL, DEFAULT_MEMBERS_LIST_ID_FORMAT, @@ -216,6 +217,9 @@ ), }, ), + strictyaml.Optional("check-if-config-changed-interval", default=DEFAULT_CHECK_IF_CONFIG_CHANGED_INTERVAL): ( # noqa: E501 + TimeDeltaValidator(minutes=True) + ), }, ) diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index a7219a1cf..01b639480 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -18,7 +18,7 @@ from collections.abc import Callable from datetime import timedelta from re import Match -from typing import Final, NoReturn, override +from typing import Final, Literal, NoReturn, override import strictyaml from strictyaml import constants as strictyaml_constants @@ -202,17 +202,13 @@ def to_yaml(self, data: object) -> str: # type: ignore[misc] class TimeDeltaValidator(strictyaml.ScalarValidator): # type: ignore[misc] @override - def __init__(self, *, minutes: bool = True, hours: bool = True, days: bool = False, weeks: bool = False) -> None: # noqa: E501 + def __init__(self, *, seconds: Literal[True] = True, minutes: bool = True, hours: bool = True, days: bool = False, weeks: bool = False) -> None: # noqa: E501 regex_matcher: str = r"\A" time_resolution_name: str for time_resolution_name in ("seconds", "minutes", "hours", "days", "weeks"): formatted_time_resolution_name: str = time_resolution_name.lower().strip() - time_resolution: object = ( - True - if formatted_time_resolution_name == "seconds" - else locals()[formatted_time_resolution_name] - ) + time_resolution: object = locals()[formatted_time_resolution_name] if not isinstance(time_resolution, bool): raise TypeError diff --git a/config/constants.py b/config/constants.py index 6e606785b..cb8cc80d4 100644 --- a/config/constants.py +++ b/config/constants.py @@ -25,6 +25,7 @@ "DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED", "DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY", "DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL", + "DEFAULT_CHECK_IF_CONFIG_CHANGED_INTERVAL", "CONFIG_SETTINGS_HELPS", ) @@ -128,6 +129,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non DEFAULT_SEND_GET_ROLES_REMINDERS_ENABLED: Final[bool] = True DEFAULT_SEND_GET_ROLES_REMINDERS_DELAY: Final[str] = "40h" DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL: Final[str] = "6h" +DEFAULT_CHECK_IF_CONFIG_CHANGED_INTERVAL: Final[str] = "30s" CONFIG_SETTINGS_HELPS: Mapping[str, ConfigSettingHelp] = { "logging:console:log-level": ConfigSettingHelp( @@ -467,11 +469,19 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non required=False, default=DEFAULT_SEND_GET_ROLES_REMINDERS_INTERVAL, ), + "check-if-config-changed-interval": ConfigSettingHelp( + description=( + "The interval of time between checking whether the config values, " + "defined in the settings file, have changed." + ), + value_type_message=_custom_required_format_message( + ( + "string of the seconds or minutes between checks " + "(format: `sm`)" + ), + ), + requires_restart_after_changed=True, + required=False, + default=DEFAULT_CHECK_IF_CONFIG_CHANGED_INTERVAL, + ), } - -# { # TODO: Use in config reloader -# config_setting_name -# for config_setting_name, config_setting_help -# in CONFIG_SETTINGS_HELPS.items() -# if config_setting_help.requires_restart_after_changed -# } 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 fb7f3791f..a037a6df3 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -78,9 +78,8 @@ def __init__(self, *args: object, **options: object) -> None: super().__init__(*args, **options) # type: ignore[no-untyped-call] @override - async def close(self) -> NoReturn: + async def close(self) -> NoReturn: # type: ignore[misc] await super().close() - raise RuntimeError # noinspection PyPep8Naming @property diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 650ce769f..c5cb59d85 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -10,7 +10,7 @@ import re 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 @@ -63,6 +63,7 @@ class TeXBotBaseCog(Cog): "write_roles": "send messages", } + @override def __init__(self, bot: TeXBot) -> None: """Initialize a new cog instance, storing a reference to the bot object.""" self.bot: TeXBot = bot From 30e621763a9019221bd18e643bf473561e5b1b77 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Tue, 18 Jun 2024 17:12:59 +0100 Subject: [PATCH 051/128] Improve already unset message --- cogs/change_config.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index a2914d618..c99dcc948 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -872,11 +872,15 @@ async def unset_config_value(self, ctx: TeXBotApplicationContext, config_setting return try: - await config.remove_single_config_setting_value(config_setting_name) + await config.remove_single_config_setting_value(config_setting_name) # TODO: Fix sibling not removed correctly (E.g. reminders enables/disabled) except KeyError: - await self.command_send_error( - ctx, - message=f"Setting {config_setting_name!r} already has the default value", + await ctx.respond( + content=( + ":information_source: " + f"Setting `{config_setting_name}` already has the default value" + " :information_source:" + ), + ephemeral=True, ) return From 38cb3572b242fca64804193a8038369371d1fae6 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 19 Jun 2024 10:34:20 +0100 Subject: [PATCH 052/128] Add TODO --- cogs/change_config.py | 6 ++++++ config/_settings/__init__.py | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cogs/change_config.py b/cogs/change_config.py index c99dcc948..9f241a290 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -198,6 +198,12 @@ async def check_config_file_changed(self) -> None: await config._settings.utils.get_settings_file_path(), ) + # 1. retrieve new yaml + # 2. recurse yaml and get changed values + # 3. if needs restart do total restart + # 4. if not needs restart do reload func + # 5. if is messages-code changed, reload messages + raise NotImplementedError # TODO: reload/update changes # { diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index a6fb46160..af2365d0c 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -109,10 +109,10 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 @classmethod async def _public_reload(cls) -> None: - settings_file_path: AsyncPath = await utils.get_settings_file_path() + SETTINGS_FILE_PATH: Final[AsyncPath] = await utils.get_settings_file_path() current_yaml: YAML = load_yaml( - await settings_file_path.read_text(), - file_name=settings_file_path.name, + await SETTINGS_FILE_PATH.read_text(), + file_name=SETTINGS_FILE_PATH.name, ) if current_yaml == cls._most_recent_yaml and cls._settings: From c5764c4998bffa40d0bcf59fa3322bbc93c1f2fc Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 20 Jun 2024 23:09:43 +0100 Subject: [PATCH 053/128] Update dependencies --- poetry.lock | 144 ++++++++++++++++++++++++++----------------------- pyproject.toml | 2 +- 2 files changed, 78 insertions(+), 68 deletions(-) diff --git a/poetry.lock b/poetry.lock index 411d4929d..70b4703fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -633,18 +633,18 @@ dev = ["coverage", "pytest (>=7.4.4)"] [[package]] name = "filelock" -version = "3.15.1" +version = "3.15.3" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac"}, - {file = "filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8"}, + {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, + {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, ] [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-asyncio (>=0.21)", "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]] @@ -1214,47 +1214,56 @@ files = [ [[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]] @@ -1556,6 +1565,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"}, @@ -1613,44 +1623,44 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.8" +version = "0.4.10" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"}, - {file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"}, - {file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"}, - {file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"}, - {file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"}, - {file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"}, - {file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"}, - {file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"}, - {file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"}, - {file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"}, - {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"}, + {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, + {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, + {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, + {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, + {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, + {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, ] [[package]] name = "setuptools" -version = "70.0.0" +version = "70.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, - {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, + {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, + {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, ] [package.extras] docs = ["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"] -testing = ["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)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.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-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["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.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" @@ -1818,13 +1828,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] @@ -1970,4 +1980,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "e99e50d35eeb083936eaaf70a8509f57ec7bc7f7a6998e50bd773ba1b50fe816" +content-hash = "bef6edeeafaf62c2d081bc162bb2bb78726d28cb4499a10bfc365e0740a2061c" diff --git a/pyproject.toml b/pyproject.toml index 9c6814465..f73b0c041 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ mplcyberpunk = "^0.7" python-logging-discord-handler = "^0.1" classproperties = {git = "https://github.com/hottwaj/classproperties.git"} asyncstdlib = "~3.12" -setuptools = "^70.0" +setuptools = "^70.1" strictyaml = "^1.7.3" python-slugify = "^8.0" aiopath = "^0.7" From 816538889ebb3ed149e8492894cd88c945297c4d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 26 Jun 2024 18:09:20 +0100 Subject: [PATCH 054/128] Fix incorrectly bolded `@` symbol within `README.md` --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8869aba26..3d4906c46 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) * `E1031` - Your [Discord guild](https://discord.com/developers/docs/resources/guild) does not contain a [text channel](https://docs.pycord.dev/en/stable/api/models.html#discord.TextChannel) with the name "#**roles**". (This [text channel](https://docs.pycord.dev/en/stable/api/models.html#discord.TextChannel) is required for the `/writeroles` [command](https://discord.com/developers/docs/interactions/application-commands)) From 6a6e9d5977c1563c758d9d07c8794174d852c09a Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 26 Jun 2024 21:39:29 +0100 Subject: [PATCH 055/128] Improve shortcut accessors & autocomplete set iterators --- cogs/induct.py | 37 ++++++++++++----------------- cogs/make_applicant.py | 28 +++++++++------------- cogs/send_get_roles_reminders.py | 8 +++---- cogs/send_introduction_reminders.py | 6 ++--- cogs/stats.py | 20 ++++++++-------- cogs/strike.py | 23 +++++++++--------- utils/tex_bot_base_cog.py | 19 +++++++-------- 7 files changed, 62 insertions(+), 79 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index bf98eba94..a0f0fe98c 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -55,9 +55,9 @@ 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.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: @@ -192,8 +192,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 + guest_role: discord.Role = await self.bot.guest_role intro_channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, @@ -298,30 +298,23 @@ 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: - return set() - - members: set[discord.Member] = {member for member in guild.members if not member.bot} - - try: + main_guild: discord.Guild = ctx.bot.main_guild guest_role: discord.Role = await ctx.bot.guest_role - except GuestRoleDoesNotExistError: + except (GuildDoesNotExistError, GuestRoleDoesNotExistError): return set() - else: - members = {member for member in members if guest_role not in member.roles} - - if not ctx.value or ctx.value.startswith("@"): - return { - discord.OptionChoice(name=f"@{member.name}", value=str(member.id)) - for member - in members - } return { - discord.OptionChoice(name=member.name, value=str(member.id)) + discord.OptionChoice( + name=( + f"@{member.name}" + if not ctx.value or ctx.value.startswith("@") + else member.name + ), + value=str(member.id), + ) for member - in members + in main_guild.members + if not member.bot and guest_role not in member.roles } @discord.slash_command( # type: ignore[no-untyped-call, misc] diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index da8ae618d..fea9ab91f 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -98,29 +98,23 @@ async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord options that have a member input-type. """ try: - guild: discord.Guild = ctx.bot.main_guild + main_guild: discord.Guild = ctx.bot.main_guild applicant_role: discord.Role = await ctx.bot.applicant_role except (GuildDoesNotExistError, ApplicantRoleDoesNotExistError): return set() - members: set[discord.Member] = { - member - for member - in guild.members - if not member.bot and applicant_role not in member.roles - } - - if not ctx.value or ctx.value.startswith("@"): - return { - discord.OptionChoice(name=f"@{member.name}", value=str(member.id)) - for member - in members - } - return { - discord.OptionChoice(name=member.name, value=str(member.id)) + discord.OptionChoice( + name=( + f"@{member.name}" + if not ctx.value or ctx.value.startswith("@") + else member.name + ), + value=str(member.id), + ) for member - in members + in main_guild.members + if not member.bot and applicant_role not in member.roles } @discord.slash_command( # type: ignore[no-untyped-call, misc] diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 3bcadd700..d0379521f 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -69,7 +69,7 @@ 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 + main_guild: discord.Guild = self.bot.main_guild guest_role: discord.Role = await self.bot.guest_role # noinspection PyUnusedLocal @@ -112,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) @@ -140,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 @@ -155,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 " diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 8744d932a..f94e69082 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -110,10 +110,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.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 @@ -143,7 +143,7 @@ async def send_introduction_reminders(self) -> None: 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 " diff --git a/cogs/stats.py b/cogs/stats.py index 72dd9a54a..97b27391e 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -200,9 +200,9 @@ 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 + main_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: @@ -218,7 +218,7 @@ 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: AsyncIterable[discord.Message] = channel.history( @@ -300,7 +300,7 @@ 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 + main_guild: discord.Guild = self.bot.main_guild guest_role: discord.Role = await self.bot.guest_role await ctx.defer(ephemeral=True) @@ -312,11 +312,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( @@ -434,7 +434,7 @@ 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 + main_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 @@ -453,7 +453,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( @@ -520,7 +520,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.bot.main_guild await ctx.defer(ephemeral=True) @@ -530,7 +530,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 diff --git a/cogs/strike.py b/cogs/strike.py index 89188e3f7..e7ac6e7fe 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -755,23 +755,22 @@ 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.bot.main_guild except GuildDoesNotExistError: return set() - members: set[discord.Member] = {member for member in guild.members if not member.bot} - - 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 - in members - } - return { - discord.OptionChoice(name=member.name, value=str(member.id)) + discord.OptionChoice( + name=( + f"@{member.name}" + if not ctx.value or re.fullmatch(r"\A@.*\Z", ctx.value) + else member.name + ), + value=str(member.id), + ) for member - in members + in main_guild.members + if not member.bot } @discord.slash_command( # type: ignore[no-untyped-call, misc] diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index c5cb59d85..6334c3f66 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -218,18 +218,15 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ ctx.interaction.user, ) - 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 - in main_guild.text_channels - if channel.permissions_for(channel_permissions_limiter).is_superset( - discord.Permissions(send_messages=True, view_channel=True), - ) - } - return { - discord.OptionChoice(name=channel.name, value=str(channel.id)) + discord.OptionChoice( + name=( + f"#{channel.name}" + if not ctx.value or re.fullmatch(r"\A#.*\Z", ctx.value) + else channel.name + ), + value=str(channel.id), + ) for channel in main_guild.text_channels if channel.permissions_for(channel_permissions_limiter).is_superset( From 1f98a83641a41e0f2bc2963d82f0bcb15ed43170 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 26 Jun 2024 21:43:02 +0100 Subject: [PATCH 056/128] Fix main_guild shortcut accessor priorities --- cogs/make_applicant.py | 1 + cogs/stats.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index fea9ab91f..a2a3ad531 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -31,6 +31,7 @@ class BaseMakeApplicantCog(TeXBotBaseCog): 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.""" + # 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.bot.main_guild applicant_role: discord.Role = await ctx.bot.applicant_role guest_role: discord.Role = await ctx.bot.guest_role diff --git a/cogs/stats.py b/cogs/stats.py index 97b27391e..0be0e5e0e 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -187,6 +187,9 @@ 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.bot.main_guild + channel_id: int = ctx.channel_id if str_channel_id: @@ -199,8 +202,6 @@ 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 - main_guild: discord.Guild = self.bot.main_guild channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, id=channel_id, From d698140a851e4b05bcc914c8108f81f9c1810501 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 26 Jun 2024 22:01:30 +0100 Subject: [PATCH 057/128] Update dependencies --- poetry.lock | 81 ++++++++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/poetry.lock b/poetry.lock index 70b4703fb..f8f5cc65f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -193,13 +193,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] @@ -633,13 +633,13 @@ dev = ["coverage", "pytest (>=7.4.4)"] [[package]] name = "filelock" -version = "3.15.3" +version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, - {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] @@ -1146,38 +1146,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] @@ -1565,7 +1565,6 @@ 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"}, @@ -1649,13 +1648,13 @@ files = [ [[package]] name = "setuptools" -version = "70.1.0" +version = "70.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, - {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, + {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"}, + {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"}, ] [package.extras] @@ -1845,13 +1844,13 @@ zstd = ["zstandard (>=0.18.0)"] [[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] From 72b260b3d8166eeb6f211a8be3126f5fd9a274e9 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 27 Jun 2024 00:49:37 +0100 Subject: [PATCH 058/128] Fix SPAG & formatting --- cogs/archive.py | 6 +++--- cogs/change_config.py | 7 ++++--- cogs/edit_message.py | 2 +- cogs/make_applicant.py | 4 ++-- cogs/send_introduction_reminders.py | 4 ++-- cogs/strike.py | 16 ++++++++++------ config/__init__.py | 5 ++++- config/_settings/__init__.py | 3 +++ db/core/app_config.py | 2 +- db/core/models/__init__.py | 2 +- db/core/models/managers.py | 4 ++-- exceptions/base.py | 2 +- exceptions/config_changes.py | 2 +- exceptions/does_not_exist.py | 18 +++++++++++------- exceptions/guild.py | 2 +- utils/__init__.py | 2 +- utils/command_checks.py | 2 ++ utils/message_sender_components.py | 2 +- utils/tex_bot.py | 8 ++++---- utils/tex_bot_base_cog.py | 4 ++-- utils/tex_bot_contexts.py | 2 +- 21 files changed, 58 insertions(+), 41 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index e9025ca16..6bc1d3231 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -39,7 +39,7 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[dis """ 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: @@ -156,8 +156,8 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> """ Definition & callback response of the "archive" command. - The "archive" command hides a given category from view of casual members unless they - have the "Archivist" role. + The "archive" command hides a given category from the view of casual members + unless they 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 diff --git a/cogs/change_config.py b/cogs/change_config.py index 9f241a290..db0cd2e3f 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -1,4 +1,4 @@ -"""Contains cog classes for any config changing interactions.""" +"""Contains cog classes for any config-changing interactions.""" from collections.abc import Sequence @@ -200,8 +200,8 @@ async def check_config_file_changed(self) -> None: # 1. retrieve new yaml # 2. recurse yaml and get changed values - # 3. if needs restart do total restart - # 4. if not needs restart do reload func + # 3. if it needs restart, do total restart + # 4. if not needs restart, do reload func # 5. if is messages-code changed, reload messages raise NotImplementedError # TODO: reload/update changes @@ -466,6 +466,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext } if "document" in setting_name: + # noinspection SpellCheckingInspection return { "https://", "https://drive.google.com/file/d/", diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 3cf2a20c2..f6139efb0 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -29,7 +29,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ """ 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: diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index a2a3ad531..59c69c35b 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -164,7 +164,7 @@ async def user_make_applicant(self, ctx: TeXBotApplicationContext, member: disco 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) @@ -177,7 +177,7 @@ 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: diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index f94e69082..1de48385c 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -186,13 +186,13 @@ 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. """ @override def __init__(self, bot: TeXBot) -> None: - """Initialize a new discord.View, to opt-in/out of introduction reminders.""" + """Initialise a new discord.View, to opt-in/out of introduction reminders.""" self.bot: TeXBot = bot super().__init__(timeout=None) diff --git a/cogs/strike.py b/cogs/strike.py index e7ac6e7fe..583bbaee0 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -99,7 +99,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. @@ -116,7 +116,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. @@ -137,7 +137,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. @@ -155,7 +155,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. @@ -177,7 +177,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. @@ -195,7 +195,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. @@ -340,6 +340,7 @@ async def _confirm_increase_strike(self, message_sender_component: MessageSaving ) if not perform_action: + # noinspection SpellCheckingInspection await message_sender_component.send( content=( f"{confirm_strike_message}\n" @@ -560,6 +561,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M match out_of_sync_ban_button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] case "no_out_of_sync_ban_member": + # noinspection SpellCheckingInspection await out_of_sync_ban_confirmation_message.edit( content=( f"Aborted performing ban action upon {strike_user.mention}. " @@ -584,6 +586,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" @@ -648,6 +651,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M match button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] case "no_manual_moderation_action": + # noinspection SpellCheckingInspection await confirmation_message.edit( content=( f"Aborted increasing {strike_user.mention}'s strikes " diff --git a/config/__init__.py b/config/__init__.py index b94a1e314..29e2619b7 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -53,7 +53,7 @@ def run_setup() -> None: - """Execute the setup functions required, before other modules can be run.""" + """Execute the setup functions required before other modules can be run.""" check_for_deprecated_environment_variables() async_to_sync(reload_settings)() @@ -172,12 +172,14 @@ async def reload_settings() -> None: def view_single_config_setting_value(config_setting_name: str) -> str | None: + # noinspection GrazieInspection """Return the value of a single configuration setting from settings tree hierarchy.""" # noinspection PyProtectedMember return settings._public_view_single_raw_value(config_setting_name=config_setting_name) # noqa: SLF001 async def assign_single_config_setting_value(config_setting_name: str, new_config_setting_value: str) -> None: # noqa: E501 + # noinspection GrazieInspection """Set the value of a single configuration setting within settings tree hierarchy.""" # noinspection PyProtectedMember return await settings._public_assign_single_raw_value( # noqa: SLF001 @@ -187,6 +189,7 @@ async def assign_single_config_setting_value(config_setting_name: str, new_confi async def remove_single_config_setting_value(config_setting_name: str) -> None: + # noinspection GrazieInspection """Unset the value of a single configuration setting within settings tree hierarchy.""" # noinspection PyProtectedMember return await settings._public_remove_single_raw_value( # noqa: SLF001 diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index af2365d0c..feef5f054 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -959,6 +959,7 @@ def _get_mapping_value(cls, partial_config_setting_name: str, partial_yaml_setti @classmethod def _public_view_single_raw_value(cls, config_setting_name: str) -> str | None: + # noinspection GrazieInspection """Return the value of a single configuration setting from settings tree hierarchy.""" current_yaml: YAML | None = cls._most_recent_yaml if current_yaml is None: @@ -1083,6 +1084,7 @@ def _set_required_value_from_validator(cls, partial_config_setting_name: str | N @classmethod async def _public_assign_single_raw_value(cls, config_setting_name: str, new_config_setting_value: str) -> None: # noqa: E501 + # noinspection GrazieInspection """Set the value of a single configuration setting within settings tree hierarchy.""" current_yaml: YAML | None = cls._most_recent_yaml if current_yaml is None: @@ -1132,6 +1134,7 @@ def _remove_value(cls, partial_config_setting_name: str, partial_yaml_settings_t @classmethod async def _public_remove_single_raw_value(cls, config_setting_name: str) -> None: + # noinspection GrazieInspection """Unset the value of a single configuration setting within settings tree hierarchy.""" current_yaml: YAML | None = cls._most_recent_yaml if current_yaml is None: 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 33bed40a6..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. diff --git a/db/core/models/managers.py b/db/core/models/managers.py index b9d6025f1..bcd5e8900 100644 --- a/db/core/models/managers.py +++ b/db/core/models/managers.py @@ -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/exceptions/base.py b/exceptions/base.py index baec0d29d..af922ed85 100644 --- a/exceptions/base.py +++ b/exceptions/base.py @@ -26,7 +26,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 @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) diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index 7d1de98fa..c05c7fee2 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -26,7 +26,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 @override def __init__(self, message: str | None = None, changed_settings: set[str] | None = None) -> None: # noqa: E501 - """Initialize a ValueError exception for a non-existent user ID.""" + """Initialise a ValueError exception for a non-existent user ID.""" self.changed_settings: set[str] | None = changed_settings super().__init__(message) diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index eb020b757..59cd7052d 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -58,7 +58,7 @@ def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 @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: @@ -90,7 +90,7 @@ def ROLE_NAME(cls) -> str: # noqa: N802,N805 @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 # noqa: COM812 ) @@ -210,26 +210,30 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 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 + """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1025" + # noinspection PyMethodParameters @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. - This set being empty could mean thta all bot commands require this entity, + This set being empty could mean that all bot commands require this entity, or that none of them do. """ # noqa: D401 return frozenset({"make_applicant"}) + # noinspection PyMethodParameters @classproperty - 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 return "Applicant" @@ -257,7 +261,7 @@ def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 @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 # noqa: COM812 ) diff --git a/exceptions/guild.py b/exceptions/guild.py index 3e252698c..70336aa1b 100644 --- a/exceptions/guild.py +++ b/exceptions/guild.py @@ -26,7 +26,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "Given user ID does not represent any member of your group's Discord guild." 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) diff --git a/utils/__init__.py b/utils/__init__.py index daa0bc843..07fcb9291 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -83,7 +83,7 @@ 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("@ \n\t") not in ("news",) for role in member.roles diff --git a/utils/command_checks.py b/utils/command_checks.py index 330d1f9c4..12f08e05f 100644 --- a/utils/command_checks.py +++ b/utils/command_checks.py @@ -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/message_sender_components.py b/utils/message_sender_components.py index a7599dfaa..b61d97833 100644 --- a/utils/message_sender_components.py +++ b/utils/message_sender_components.py @@ -41,7 +41,7 @@ async def respond(self, content: str, *, view: View | None | type[_VIEW_NOT_PROV class SenderResponseComponent(GenericResponderComponent): """ - Concrete definition of a message sending response component. + Concrete definition of a message-sending response component. Defines the way to send a provided message content & optional view. """ diff --git a/utils/tex_bot.py b/utils/tex_bot.py index b85d200ff..8b025d888 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -10,7 +10,7 @@ from collections.abc import Collection from enum import IntEnum from logging import Logger -from typing import Final, NoReturn, TYPE_CHECKING, override +from typing import TYPE_CHECKING, Final, NoReturn, override import discord @@ -55,7 +55,7 @@ class TeXBot(discord.Bot): @override def __init__(self, *args: object, **options: object) -> None: - """Initialize a new discord.Bot subclass with empty shortcut accessors.""" + """Initialise a new discord.Bot subclass with empty shortcut accessors.""" self._main_guild: discord.Guild | None = None self._committee_role: discord.Role | None = None self._guest_role: discord.Role | None = None @@ -172,7 +172,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. """ @@ -265,7 +265,7 @@ def group_full_name(self) -> str: 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, + 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] diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 6334c3f66..48c256de7 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -65,7 +65,7 @@ class TeXBotBaseCog(Cog): @override def __init__(self, bot: TeXBot) -> None: - """Initialize a new cog instance, storing a reference to the bot object.""" + """Initialise a new cog instance, storing a reference to the bot object.""" self.bot: TeXBot = bot async def command_send_error(self, ctx: TeXBotApplicationContext, *, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False, responder_component: GenericResponderComponent | None = None) -> None: # noqa: E501 @@ -200,7 +200,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ """ 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: diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 28c265862..cac61ea80 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 overridden 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. """ From 5a3ed706747e112ddc2f40fdc56e7779715347c2 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 30 Jun 2024 19:10:33 +0100 Subject: [PATCH 059/128] Update dependencies --- poetry.lock | 40 +++++++++++++++++++++------------------- pyproject.toml | 3 ++- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index f8f5cc65f..e699073df 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1565,6 +1565,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"}, @@ -1622,28 +1623,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.10" +version = "0.5.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, - {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, - {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, - {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, - {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, - {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, + {file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, + {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, + {file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"}, + {file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"}, + {file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"}, + {file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"}, + {file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"}, ] [[package]] @@ -1979,4 +1981,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "bef6edeeafaf62c2d081bc162bb2bb78726d28cb4499a10bfc365e0740a2061c" +content-hash = "a0c4ff73c6695f75430da4a25765ab57b66571f3033140090ae047c4b090b5c6" diff --git a/pyproject.toml b/pyproject.toml index f73b0c041..739b323eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ mypy = "~1.10" django-stubs = {extras = ["compatible-mypy"], version = "~5.0"} types-beautifulsoup4 = "^4.12.0" pytest = "^8.2" -ruff = "^0.4" +ruff = "^0.5" ccft-pymarkdown = "^1.1" @@ -99,6 +99,7 @@ django_settings_module = "db._settings" [tool.ruff] +output-format = "concise" line-length = 95 target-version ="py312" extend-exclude = [ From 6d251315b0add527a6fd3351ae100ad274e7c711 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 30 Jun 2024 19:11:01 +0100 Subject: [PATCH 060/128] Use improved member access functions --- cogs/induct.py | 6 +++--- cogs/make_applicant.py | 4 ++-- cogs/strike.py | 2 +- db/core/models/utils.py | 4 ++-- utils/tex_bot.py | 47 ++++++++++++++++++++++++----------------- 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index a0f0fe98c..f0af09b94 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -349,7 +349,7 @@ 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.bot.get_main_guild_member( str_induct_member_id, ) except ValueError as member_id_not_integer_error: @@ -403,7 +403,7 @@ async def non_silent_message_induct(self, ctx: TeXBotApplicationContext, message by giving them the "Guest" role. """ try: - member: discord.Member = await self.bot.get_member_from_str_id( + member: discord.Member = await self.bot.get_main_guild_member( str(message.author.id), ) except ValueError: @@ -432,7 +432,7 @@ async def silent_message_induct(self, ctx: TeXBotApplicationContext, message: di 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.bot.get_main_guild_member( str(message.author.id), ) except ValueError: diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 59c69c35b..e76dd13be 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -143,7 +143,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.bot.get_main_guild_member( str_applicant_member_id, ) except ValueError as member_id_not_integer_error: @@ -181,7 +181,7 @@ async def message_make_applicant(self, ctx: TeXBotApplicationContext, message: d "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.bot.get_main_guild_member( str(message.author.id), ) except ValueError: diff --git a/cogs/strike.py b/cogs/strike.py index 583bbaee0..b21c9ab9d 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -803,7 +803,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.bot.get_main_guild_member( str_strike_member_id, ) except ValueError as member_id_not_integer_error: diff --git a/db/core/models/utils.py b/db/core/models/utils.py index 8a0229c9d..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: diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 8b025d888..fcec00f95 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -466,7 +466,7 @@ def set_main_guild(self, main_guild: discord.Guild) -> None: self._main_guild = main_guild self._main_guild_set = True - async def get_main_guild_member(self, user: discord.Member | discord.User) -> discord.Member: # noqa: E501 + async def _get_main_guild_member_from_user(self, user: discord.Member | discord.User) -> discord.Member: # noqa: E501 """ Util method to retrieve a member of your group's Discord guild from their User object. @@ -477,14 +477,35 @@ async def get_main_guild_member(self, user: discord.Member | discord.User) -> di raise DiscordMemberNotInMainGuildError(user_id=user.id) return main_guild_member - async def get_member_from_str_id(self, str_member_id: str) -> discord.Member: + async def _get_main_guild_member_from_id(self, member_id: int) -> discord.Member: """ - Retrieve a member of your group's Discord guild by their ID. + Util method to retrieve a member of your group's Discord guild from their User ID. - Raises `ValueError` if the provided ID does not represent any member - of your group's Discord guild. + Raises `DiscordMemberNotInMainGuild` if the user is not in your group's Discord guild. + Raises `ValueError` if the provided ID is not a valid user ID. """ - str_member_id = str_member_id.replace("<@", "").replace(">", "") + user: discord.User | None = self.get_user(member_id) + if not user: + raise ValueError( + DiscordMemberNotInMainGuildError(user_id=member_id).message, + ) + + return await self.get_main_guild_member(user) + + async def get_main_guild_member(self, user: discord.Member | discord.User | str | int) -> discord.Member: # noqa: E501 + """ + Util method to retrieve a member of your group's Discord guild from their ID or User. + + Raises `DiscordMemberNotInMainGuild` if the user is not in your group's Discord guild. + Raises `ValueError` if the provided ID is not a valid user ID. + """ + if isinstance(user, discord.Member | discord.User): + return await self._get_main_guild_member_from_user(user) + + if isinstance(user, int): + return await self._get_main_guild_member_from_id(user) + + str_member_id = user.replace("<@", "").replace(">", "") if not re.fullmatch(r"\A\d{17,20}\Z", str_member_id): INVALID_USER_ID_MESSAGE: Final[str] = ( @@ -492,16 +513,4 @@ async def get_member_from_str_id(self, str_member_id: str) -> discord.Member: ) raise ValueError(INVALID_USER_ID_MESSAGE) - user: discord.User | None = self.get_user(int(str_member_id)) - if not user: - raise ValueError( - DiscordMemberNotInMainGuildError(user_id=int(str_member_id)).message, - ) - - user_not_in_main_guild_error: DiscordMemberNotInMainGuildError - try: - member: discord.Member = await self.get_main_guild_member(user) - except DiscordMemberNotInMainGuildError as user_not_in_main_guild_error: - raise ValueError from user_not_in_main_guild_error - - return member + return await self._get_main_guild_member_from_id(int(user)) From 16717f75b51b918bfa39327fca42b8c6479cd6cb Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Tue, 2 Jul 2024 14:46:39 +0100 Subject: [PATCH 061/128] Use more specific strike action types --- cogs/strike.py | 4 +- poetry.lock | 172 ++++++++++++++++++++++++++----------------------- pyproject.toml | 6 +- 3 files changed, 96 insertions(+), 86 deletions(-) diff --git a/cogs/strike.py b/cogs/strike.py index b21c9ab9d..2fc53b82d 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -21,7 +21,7 @@ import re from collections.abc import Mapping from logging import Logger -from typing import Final +from typing import Final, Literal import aiohttp import discord @@ -466,7 +466,7 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me return guild_confirmation_message_channel @capture_strike_tracking_error - async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.Member, action: discord.AuditLogAction) -> None: # noqa: E501 + async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.Member, action: Literal[discord.AuditLogAction.member_update | discord.AuditLogAction.kick | discord.AuditLogAction.ban]) -> 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 diff --git a/poetry.lock b/poetry.lock index e699073df..4e7e075a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1290,84 +1290,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"] @@ -1444,13 +1455,13 @@ voice = ["PyNaCl (>=1.3.0,<1.6)"] [[package]] name = "pymarkdownlnt" -version = "0.9.20" +version = "0.9.21" description = "A GitHub Flavored Markdown compliant Markdown linter." optional = false python-versions = ">=3.8.0" files = [ - {file = "pymarkdownlnt-0.9.20-py3-none-any.whl", hash = "sha256:d9cd2db8e79dce67396236b7cdbaebe788bc8a07f95af9731433d353a91fe3d9"}, - {file = "pymarkdownlnt-0.9.20.tar.gz", hash = "sha256:75e3792e066c3ba53d71c2ef9b411cb3e429ad5d612ad02607f3c1ad120c5abc"}, + {file = "pymarkdownlnt-0.9.21-py3-none-any.whl", hash = "sha256:5efc4092898469c92cdc33686c356f3d94afb99aca09addf79119ba58959f350"}, + {file = "pymarkdownlnt-0.9.21.tar.gz", hash = "sha256:f30f231f1fa36d98d646c7d2bedf2e8f3696a78b5e420d7a40a0d2f1c9159c0a"}, ] [package.dependencies] @@ -1565,7 +1576,6 @@ 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"}, @@ -1650,18 +1660,18 @@ files = [ [[package]] name = "setuptools" -version = "70.1.1" +version = "70.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"}, - {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"}, + {file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"}, + {file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"}, ] [package.extras] -docs = ["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"] -testing = ["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.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"] +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" @@ -1981,4 +1991,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "a0c4ff73c6695f75430da4a25765ab57b66571f3033140090ae047c4b090b5c6" +content-hash = "65a89b848b64b4255262ae887ec768e71a8f603647c48ae5fe000a454986bef9" diff --git a/pyproject.toml b/pyproject.toml index 739b323eb..47c7fea65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ mplcyberpunk = "^0.7" python-logging-discord-handler = "^0.1" classproperties = {git = "https://github.com/hottwaj/classproperties.git"} asyncstdlib = "~3.12" -setuptools = "^70.1" -strictyaml = "^1.7.3" +setuptools = "^70.2" +strictyaml = "^1.7" python-slugify = "^8.0" aiopath = "^0.7" @@ -48,7 +48,7 @@ aiopath = "^0.7" 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.5" ccft-pymarkdown = "^1.1" From 51dd2915bab22b7ac3bb72993c51e603878e6b9f Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Tue, 2 Jul 2024 14:49:25 +0100 Subject: [PATCH 062/128] Improve code ordering in `cogs/strike.py` --- cogs/strike.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cogs/strike.py b/cogs/strike.py index d42b9d699..844586200 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -483,14 +483,15 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M if _audit_log_entry.target == strike_user ) 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: @@ -747,7 +748,7 @@ async def on_member_ban(self, guild: discord.Guild, user: discord.User | discord # noinspection PyArgumentList await self._confirm_manual_add_strike( strike_user=user, - action=discord.AuditLogAction.ban, + action=discord.AuditLogAction.channel_create, ) From 1f52335dc6311a62a32305c1de451b6cc881df57 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Tue, 2 Jul 2024 14:52:42 +0100 Subject: [PATCH 063/128] Fix type-annotations in `cogs/strike.py` --- cogs/strike.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/strike.py b/cogs/strike.py index 844586200..59b82d3d7 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -465,6 +465,7 @@ 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: Literal[discord.AuditLogAction.member_update | discord.AuditLogAction.kick | discord.AuditLogAction.ban]) -> 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 @@ -748,7 +749,7 @@ async def on_member_ban(self, guild: discord.Guild, user: discord.User | discord # noinspection PyArgumentList await self._confirm_manual_add_strike( strike_user=user, - action=discord.AuditLogAction.channel_create, + action=discord.AuditLogAction.ban, ) From b25908af88d29d873415ed8b226955862e72e92d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Tue, 2 Jul 2024 20:35:30 +0100 Subject: [PATCH 064/128] Update __init__.py --- config/_settings/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index feef5f054..036f378d6 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -206,7 +206,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # if not CONSOLE_LOGGING_SETTINGS_CHANGED: return set() - ALL_HANDLERS: Iterable[logging.Handler] = logger.handlers + ALL_HANDLERS: Final[Iterable[logging.Handler]] = logger.handlers # NOTE: The collection of handlers needs to be retrieved before the new StreamHandler is created console_logging_handler: logging.StreamHandler[TextIO] = logging.StreamHandler() From 377920628dd1f81b9f664489c8ed64eec32c2002 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Wed, 10 Jul 2024 11:39:48 +0100 Subject: [PATCH 065/128] Update dependencies --- poetry.lock | 217 +++++++++++++++++++++++++------------------------ pyproject.toml | 4 +- 2 files changed, 111 insertions(+), 110 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4e7e075a5..47b513593 100644 --- a/poetry.lock +++ b/poetry.lock @@ -289,13 +289,13 @@ setuptools = ">=70.0,<71.0" [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -557,13 +557,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] @@ -649,53 +649,53 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "fonttools" -version = "4.53.0" +version = "4.53.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.53.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20"}, - {file = "fonttools-4.53.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d"}, - {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6"}, - {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5"}, - {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9"}, - {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca"}, - {file = "fonttools-4.53.0-cp310-cp310-win32.whl", hash = "sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068"}, - {file = "fonttools-4.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68"}, - {file = "fonttools-4.53.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec"}, - {file = "fonttools-4.53.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749"}, - {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161"}, - {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee"}, - {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6"}, - {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e"}, - {file = "fonttools-4.53.0-cp311-cp311-win32.whl", hash = "sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005"}, - {file = "fonttools-4.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796"}, - {file = "fonttools-4.53.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a"}, - {file = "fonttools-4.53.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4"}, - {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06"}, - {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d"}, - {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109"}, - {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2"}, - {file = "fonttools-4.53.0-cp312-cp312-win32.whl", hash = "sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea"}, - {file = "fonttools-4.53.0-cp312-cp312-win_amd64.whl", hash = "sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380"}, - {file = "fonttools-4.53.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12"}, - {file = "fonttools-4.53.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4"}, - {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce"}, - {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f"}, - {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206"}, - {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd"}, - {file = "fonttools-4.53.0-cp38-cp38-win32.whl", hash = "sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af"}, - {file = "fonttools-4.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b"}, - {file = "fonttools-4.53.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac"}, - {file = "fonttools-4.53.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d"}, - {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1"}, - {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f"}, - {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64"}, - {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0"}, - {file = "fonttools-4.53.0-cp39-cp39-win32.whl", hash = "sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9"}, - {file = "fonttools-4.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2"}, - {file = "fonttools-4.53.0-py3-none-any.whl", hash = "sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4"}, - {file = "fonttools-4.53.0.tar.gz", hash = "sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002"}, + {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] @@ -832,13 +832,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] @@ -981,40 +981,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] @@ -1436,13 +1436,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] @@ -1576,6 +1576,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"}, @@ -1633,40 +1634,40 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.5.0" +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.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, - {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, - {file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"}, - {file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"}, - {file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"}, - {file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"}, - {file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"}, + {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 = "70.2.0" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"}, - {file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] @@ -1991,4 +1992,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "65a89b848b64b4255262ae887ec768e71a8f603647c48ae5fe000a454986bef9" +content-hash = "c5f7a0556374fa14750a160563c5677919b3ac3e9793b7404f0e250533d316fa" diff --git a/pyproject.toml b/pyproject.toml index 47c7fea65..85cd60b4a 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" beautifulsoup4 = "^4.12" emoji = "^2.12" parsedatetime = "^2.6" @@ -39,7 +39,7 @@ mplcyberpunk = "^0.7" python-logging-discord-handler = "^0.1" classproperties = {git = "https://github.com/hottwaj/classproperties.git"} asyncstdlib = "~3.12" -setuptools = "^70.2" +setuptools = "^70.3" strictyaml = "^1.7" python-slugify = "^8.0" aiopath = "^0.7" From 8a633dbaf6f024deb8987d4ff73a29fef183815a Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 11 Jul 2024 15:22:25 +0100 Subject: [PATCH 066/128] Rename error_capture_decorators to make it clear that they shutdown TeX-Bot --- cogs/induct.py | 2 +- cogs/send_get_roles_reminders.py | 8 ++++---- cogs/send_introduction_reminders.py | 8 ++++---- cogs/stats.py | 2 +- cogs/strike.py | 2 +- ...rs.py => closing_error_capture_decorators.py} | 16 ++++++++-------- 6 files changed, 19 insertions(+), 19 deletions(-) rename utils/{error_capture_decorators.py => closing_error_capture_decorators.py} (87%) diff --git a/cogs/induct.py b/cogs/induct.py index f0af09b94..e75404aee 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -37,7 +37,7 @@ TeXBotAutocompleteContext, TeXBotBaseCog, ) -from utils.error_capture_decorators import capture_guild_does_not_exist_error +from utils.closing_error_capture_decorators import capture_guild_does_not_exist_error logger: Logger = logging.getLogger("TeX-Bot") diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index d0379521f..a01ad1c8a 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -20,8 +20,8 @@ from db.core.models import SentGetRolesReminderMember from exceptions import GuestRoleDoesNotExistError, RolesChannelDoesNotExistError from utils import TeXBot, TeXBotBaseCog -from utils.error_capture_decorators import ( - ErrorCaptureDecorators, +from utils.closing_error_capture_decorators import ( + ClosingErrorCaptureDecorators, capture_guild_does_not_exist_error, ) @@ -53,9 +53,9 @@ def cog_unload(self) -> None: @tasks.loop(seconds=settings["SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] @functools.partial( - ErrorCaptureDecorators.capture_error_and_close, + ClosingErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError, - close_func=ErrorCaptureDecorators.critical_error_close_func, + close_func=ClosingErrorCaptureDecorators.critical_error_close_func, ) @capture_guild_does_not_exist_error async def send_get_roles_reminders(self) -> None: diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 1de48385c..a2935e748 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -26,8 +26,8 @@ ) from exceptions import DiscordMemberNotInMainGuildError, GuestRoleDoesNotExistError from utils import TeXBot, TeXBotBaseCog -from utils.error_capture_decorators import ( - ErrorCaptureDecorators, +from utils.closing_error_capture_decorators import ( + ClosingErrorCaptureDecorators, capture_guild_does_not_exist_error, ) @@ -94,9 +94,9 @@ async def _check_if_member_needs_reminder(cls, member_id: int, member_joined_at: @tasks.loop(seconds=settings["SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] @functools.partial( - ErrorCaptureDecorators.capture_error_and_close, + ClosingErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError, - close_func=ErrorCaptureDecorators.critical_error_close_func, + close_func=ClosingErrorCaptureDecorators.critical_error_close_func, ) @capture_guild_does_not_exist_error async def send_introduction_reminders(self) -> None: diff --git a/cogs/stats.py b/cogs/stats.py index 0be0e5e0e..1fad8c0ad 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -18,7 +18,7 @@ from config import settings from db.core.models import LeftDiscordMember from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog -from utils.error_capture_decorators import capture_guild_does_not_exist_error +from utils.closing_error_capture_decorators import capture_guild_does_not_exist_error if TYPE_CHECKING: from collections.abc import Collection diff --git a/cogs/strike.py b/cogs/strike.py index 59b82d3d7..465c8cf6d 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -45,7 +45,7 @@ TeXBotAutocompleteContext, TeXBotBaseCog, ) -from utils.error_capture_decorators import ( +from utils.closing_error_capture_decorators import ( capture_guild_does_not_exist_error, capture_strike_tracking_error, ) diff --git a/utils/error_capture_decorators.py b/utils/closing_error_capture_decorators.py similarity index 87% rename from utils/error_capture_decorators.py rename to utils/closing_error_capture_decorators.py index 2d9de62fd..a63bbbfc5 100644 --- a/utils/error_capture_decorators.py +++ b/utils/closing_error_capture_decorators.py @@ -1,5 +1,5 @@ """ -Common decorator utilities to capture & suppress errors. +Common decorator utilities to capture & suppress errors, then shutdown TeX-Bot. Capturing errors is necessary in contexts where exceptions are not already suppressed. """ @@ -7,7 +7,7 @@ from collections.abc import Sequence __all__: Sequence[str] = ( - "ErrorCaptureDecorators", + "ClosingErrorCaptureDecorators", "capture_guild_does_not_exist_error", "capture_strike_tracking_error", ) @@ -43,9 +43,9 @@ logger: Final[Logger] = logging.getLogger("TeX-Bot") -class ErrorCaptureDecorators: +class ClosingErrorCaptureDecorators: """ - Common decorator utilities to capture & suppress errors. + Common decorator utilities to capture & suppress errors, then shutdown TeX-Bot. Capturing errors is necessary in contexts where exceptions are not already suppressed. """ @@ -90,10 +90,10 @@ def capture_guild_does_not_exist_error(func: "WrapperInputFunc[P, T]") -> "Wrapp The raised exception is then suppressed. """ # noqa: D401 - return ErrorCaptureDecorators.capture_error_and_close( + return ClosingErrorCaptureDecorators.capture_error_and_close( func, # type: ignore[arg-type] error_type=GuildDoesNotExistError, - close_func=ErrorCaptureDecorators.critical_error_close_func, + close_func=ClosingErrorCaptureDecorators.critical_error_close_func, ) @@ -103,8 +103,8 @@ def capture_strike_tracking_error(func: "WrapperInputFunc[P, T]") -> "WrapperOut The raised exception is then suppressed. """ # noqa: D401 - return ErrorCaptureDecorators.capture_error_and_close( + return ClosingErrorCaptureDecorators.capture_error_and_close( func, # type: ignore[arg-type] error_type=StrikeTrackingError, - close_func=ErrorCaptureDecorators.strike_tracking_error_close_func, + close_func=ClosingErrorCaptureDecorators.strike_tracking_error_close_func, ) From 22c76d3fbdd2ff537d8bcb083c581bd09255291d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 11 Jul 2024 15:29:54 +0100 Subject: [PATCH 067/128] Revert "Rename error_capture_decorators to make it clear that they shutdown TeX-Bot" This reverts commit 8a633dbaf6f024deb8987d4ff73a29fef183815a. --- cogs/induct.py | 2 +- cogs/send_get_roles_reminders.py | 8 ++++---- cogs/send_introduction_reminders.py | 8 ++++---- cogs/stats.py | 2 +- cogs/strike.py | 2 +- ...decorators.py => error_capture_decorators.py} | 16 ++++++++-------- 6 files changed, 19 insertions(+), 19 deletions(-) rename utils/{closing_error_capture_decorators.py => error_capture_decorators.py} (87%) diff --git a/cogs/induct.py b/cogs/induct.py index e75404aee..f0af09b94 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -37,7 +37,7 @@ TeXBotAutocompleteContext, TeXBotBaseCog, ) -from utils.closing_error_capture_decorators import capture_guild_does_not_exist_error +from utils.error_capture_decorators import capture_guild_does_not_exist_error logger: Logger = logging.getLogger("TeX-Bot") diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index a01ad1c8a..d0379521f 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -20,8 +20,8 @@ from db.core.models import SentGetRolesReminderMember from exceptions import GuestRoleDoesNotExistError, RolesChannelDoesNotExistError from utils import TeXBot, TeXBotBaseCog -from utils.closing_error_capture_decorators import ( - ClosingErrorCaptureDecorators, +from utils.error_capture_decorators import ( + ErrorCaptureDecorators, capture_guild_does_not_exist_error, ) @@ -53,9 +53,9 @@ def cog_unload(self) -> None: @tasks.loop(seconds=settings["SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] @functools.partial( - ClosingErrorCaptureDecorators.capture_error_and_close, + ErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError, - close_func=ClosingErrorCaptureDecorators.critical_error_close_func, + close_func=ErrorCaptureDecorators.critical_error_close_func, ) @capture_guild_does_not_exist_error async def send_get_roles_reminders(self) -> None: diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index a2935e748..1de48385c 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -26,8 +26,8 @@ ) from exceptions import DiscordMemberNotInMainGuildError, GuestRoleDoesNotExistError from utils import TeXBot, TeXBotBaseCog -from utils.closing_error_capture_decorators import ( - ClosingErrorCaptureDecorators, +from utils.error_capture_decorators import ( + ErrorCaptureDecorators, capture_guild_does_not_exist_error, ) @@ -94,9 +94,9 @@ async def _check_if_member_needs_reminder(cls, member_id: int, member_joined_at: @tasks.loop(seconds=settings["SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] @functools.partial( - ClosingErrorCaptureDecorators.capture_error_and_close, + ErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError, - close_func=ClosingErrorCaptureDecorators.critical_error_close_func, + close_func=ErrorCaptureDecorators.critical_error_close_func, ) @capture_guild_does_not_exist_error async def send_introduction_reminders(self) -> None: diff --git a/cogs/stats.py b/cogs/stats.py index 1fad8c0ad..0be0e5e0e 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -18,7 +18,7 @@ from config import settings from db.core.models import LeftDiscordMember from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog -from utils.closing_error_capture_decorators import capture_guild_does_not_exist_error +from utils.error_capture_decorators import capture_guild_does_not_exist_error if TYPE_CHECKING: from collections.abc import Collection diff --git a/cogs/strike.py b/cogs/strike.py index 465c8cf6d..59b82d3d7 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -45,7 +45,7 @@ TeXBotAutocompleteContext, TeXBotBaseCog, ) -from utils.closing_error_capture_decorators import ( +from utils.error_capture_decorators import ( capture_guild_does_not_exist_error, capture_strike_tracking_error, ) diff --git a/utils/closing_error_capture_decorators.py b/utils/error_capture_decorators.py similarity index 87% rename from utils/closing_error_capture_decorators.py rename to utils/error_capture_decorators.py index a63bbbfc5..2d9de62fd 100644 --- a/utils/closing_error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -1,5 +1,5 @@ """ -Common decorator utilities to capture & suppress errors, then shutdown TeX-Bot. +Common decorator utilities to capture & suppress errors. Capturing errors is necessary in contexts where exceptions are not already suppressed. """ @@ -7,7 +7,7 @@ from collections.abc import Sequence __all__: Sequence[str] = ( - "ClosingErrorCaptureDecorators", + "ErrorCaptureDecorators", "capture_guild_does_not_exist_error", "capture_strike_tracking_error", ) @@ -43,9 +43,9 @@ logger: Final[Logger] = logging.getLogger("TeX-Bot") -class ClosingErrorCaptureDecorators: +class ErrorCaptureDecorators: """ - Common decorator utilities to capture & suppress errors, then shutdown TeX-Bot. + Common decorator utilities to capture & suppress errors. Capturing errors is necessary in contexts where exceptions are not already suppressed. """ @@ -90,10 +90,10 @@ def capture_guild_does_not_exist_error(func: "WrapperInputFunc[P, T]") -> "Wrapp The raised exception is then suppressed. """ # noqa: D401 - return ClosingErrorCaptureDecorators.capture_error_and_close( + return ErrorCaptureDecorators.capture_error_and_close( func, # type: ignore[arg-type] error_type=GuildDoesNotExistError, - close_func=ClosingErrorCaptureDecorators.critical_error_close_func, + close_func=ErrorCaptureDecorators.critical_error_close_func, ) @@ -103,8 +103,8 @@ def capture_strike_tracking_error(func: "WrapperInputFunc[P, T]") -> "WrapperOut The raised exception is then suppressed. """ # noqa: D401 - return ClosingErrorCaptureDecorators.capture_error_and_close( + return ErrorCaptureDecorators.capture_error_and_close( func, # type: ignore[arg-type] error_type=StrikeTrackingError, - close_func=ClosingErrorCaptureDecorators.strike_tracking_error_close_func, + close_func=ErrorCaptureDecorators.strike_tracking_error_close_func, ) From 4c197a82fc2a8308f5cda2f16321becda5000465 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 11 Jul 2024 16:39:35 +0100 Subject: [PATCH 068/128] Improve descriptions, rename `ctx.bot` & improve strike moderation types --- CONTRIBUTING.md | 8 +- cogs/annual_handover_and_reset.py | 14 +-- cogs/archive.py | 20 ++--- cogs/change_config.py | 24 +++--- cogs/command_error.py | 7 +- cogs/edit_message.py | 4 +- cogs/get_token_authorisation.py | 2 +- cogs/induct.py | 58 +++++++------ cogs/kill.py | 8 +- cogs/make_applicant.py | 18 ++-- cogs/make_member.py | 21 ++--- cogs/remind_me.py | 10 +-- cogs/send_get_roles_reminders.py | 10 +-- cogs/send_introduction_reminders.py | 20 ++--- cogs/source.py | 4 +- cogs/startup.py | 32 +++---- cogs/stats.py | 39 +++++---- cogs/strike.py | 129 +++++++++++++++------------- cogs/write_roles.py | 4 +- main.py | 12 +-- utils/command_checks.py | 4 +- utils/error_capture_decorators.py | 2 +- utils/tex_bot_base_cog.py | 12 +-- utils/tex_bot_contexts.py | 4 +- 24 files changed, 242 insertions(+), 224 deletions(-) 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/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 6bc1d3231..97724f5df 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -46,11 +46,11 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[dis return set() try: - if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): return set() - main_guild: discord.Guild = ctx.bot.main_guild - interaction_user: discord.Member = await ctx.bot.get_main_guild_member( + 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): @@ -160,13 +160,13 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> unless they 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() + 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( diff --git a/cogs/change_config.py b/cogs/change_config.py index db0cd2e3f..9aad8bb54 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -216,7 +216,7 @@ async def check_config_file_changed(self) -> None: @check_config_file_changed.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() class ConfigChangeCommandsCog(TeXBotBaseCog): @@ -246,7 +246,7 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> Set return set() try: - if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -260,7 +260,7 @@ async def autocomplete_get_unsetable_settings_names(ctx: TeXBotAutocompleteConte return set() try: - if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -282,7 +282,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext return set() try: - if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): # type: ignore[arg-type] + if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): # type: ignore[arg-type] return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -478,7 +478,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext for domain, path in itertools.product( ("github", "raw.githubusercontent"), - (f"{urllib.parse.quote(ctx.bot.group_short_name)}/", ""), + (f"{urllib.parse.quote(ctx.tex_bot.group_short_name)}/", ""), ) } | { f"https://{subdomain}dropbox{domain_suffix}.com/{path}" @@ -493,7 +493,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext return {"https://"} try: - main_guild: discord.Guild = ctx.bot.main_guild + main_guild: discord.Guild = ctx.tex_bot.main_guild except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -517,7 +517,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext } try: - interaction_member: discord.Member = await ctx.bot.get_main_guild_member( + interaction_member: discord.Member = await ctx.tex_bot.get_main_guild_member( ctx.interaction.user, # type: ignore[arg-type] ) except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): @@ -632,13 +632,13 @@ async def help_config_setting(self, ctx: TeXBotApplicationContext, config_settin f"{ config_setting_help.description.replace( "**`@TeX-Bot`**", - self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", + self.tex_bot.user.mention if self.tex_bot.user else "**`@TeX-Bot`**", ).replace( "TeX-Bot", - self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", + self.tex_bot.user.mention if self.tex_bot.user else "**`@TeX-Bot`**", ).replace( "the bot", - self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", + self.tex_bot.user.mention if self.tex_bot.user else "**`@TeX-Bot`**", ) }\n\n" f"{ @@ -711,14 +711,14 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n 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 confirmation_message: discord.Message = ( response if isinstance(response, discord.Message) 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 diff --git a/cogs/command_error.py b/cogs/command_error.py index a065b35b1..0a746bc14 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -81,7 +81,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." ) @@ -89,7 +90,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( @@ -133,4 +134,4 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro or type(error.original) is Exception ) if BOT_NEEDS_CLOSING: - await self.bot.close() + await self.tex_bot.close() diff --git a/cogs/edit_message.py b/cogs/edit_message.py index f6139efb0..842b8270b 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -36,7 +36,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ return set() try: - if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -83,7 +83,7 @@ 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.fullmatch(r"\A\d{17,20}\Z", str_channel_id): await self.command_send_error( diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 75ead0f7f..eda8a767a 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -111,7 +111,7 @@ async def get_token_authorisation(self, ctx: TeXBotApplicationContext) -> None: # 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 " diff --git a/cogs/induct.py b/cogs/induct.py index f0af09b94..dd41893d9 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -55,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 - 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 try: - guest_role: discord.Role = await self.bot.guest_role + guest_role: discord.Role = await self.tex_bot.guest_role except GuestRoleDoesNotExistError: return @@ -89,21 +89,21 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) # noinspection PyUnusedLocal 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`**" 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" @@ -117,16 +117,15 @@ 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"to {self.tex_bot.group_full_name} for only £5! " f"{ f"Just head to {settings["PURCHASE_MEMBERSHIP_LINK"]}. " if settings["PURCHASE_MEMBERSHIP_LINK"] else "" }" "You'll get awesome perks like a free T-shirt:shirt:, " - "access to member only events:calendar_spiral: " - f"and 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"{ f"Checkout all the perks at {settings["MEMBERSHIP_PERKS_LINK"]}" if settings["MEMBERSHIP_PERKS_LINK"] @@ -163,7 +162,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: @@ -184,7 +183,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() @@ -192,8 +191,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 - main_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 intro_channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, @@ -222,18 +221,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`**" 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=10): - 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: @@ -251,7 +253,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb # 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( @@ -259,7 +261,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") @@ -298,8 +300,8 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor that have a member input-type. """ try: - main_guild: discord.Guild = ctx.bot.main_guild - guest_role: discord.Role = await ctx.bot.guest_role + main_guild: discord.Guild = ctx.tex_bot.main_guild + guest_role: discord.Role = await ctx.tex_bot.guest_role except (GuildDoesNotExistError, GuestRoleDoesNotExistError): return set() @@ -349,7 +351,7 @@ 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_main_guild_member( + induct_member: discord.Member = await self.tex_bot.get_main_guild_member( str_induct_member_id, ) except ValueError as member_id_not_integer_error: @@ -403,7 +405,7 @@ async def non_silent_message_induct(self, ctx: TeXBotApplicationContext, message by giving them the "Guest" role. """ try: - member: discord.Member = await self.bot.get_main_guild_member( + member: discord.Member = await self.tex_bot.get_main_guild_member( str(message.author.id), ) except ValueError: @@ -432,7 +434,7 @@ async def silent_message_induct(self, ctx: TeXBotApplicationContext, message: di by giving them the "Guest" role, only without broadcasting a welcome message. """ try: - member: discord.Member = await self.bot.get_main_guild_member( + member: discord.Member = await self.tex_bot.get_main_guild_member( str(message.author.id), ) except ValueError: @@ -468,9 +470,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 0b9f5d449..a6ab3b9a8 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -59,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=( @@ -78,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 @@ -95,7 +95,9 @@ 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) + await self.tex_bot.perform_kill_and_close( + initiated_by_user=ctx.interaction.user, + ) case "shutdown_cancel": await confirmation_message.edit( diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index e76dd13be..0d1d8e40a 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -32,9 +32,9 @@ class BaseMakeApplicantCog(TeXBotBaseCog): 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.""" # 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.bot.main_guild - applicant_role: discord.Role = await ctx.bot.applicant_role - guest_role: discord.Role = await ctx.bot.guest_role + 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, @@ -60,7 +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") @@ -99,8 +99,8 @@ async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord options that have a member input-type. """ try: - main_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() @@ -126,7 +126,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, @@ -143,7 +143,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_main_guild_member( + applicant_member: discord.Member = await self.tex_bot.get_main_guild_member( str_applicant_member_id, ) except ValueError as member_id_not_integer_error: @@ -181,7 +181,7 @@ async def message_make_applicant(self, ctx: TeXBotApplicationContext, message: d "Applicant" role and removes the "Guest" role if they have it. """ try: - member: discord.Member = await self.bot.get_main_guild_member( + member: discord.Member = await self.tex_bot.get_main_guild_member( str(message.author.id), ) except ValueError: diff --git a/cogs/make_member.py b/cogs/make_member.py index 2fec7698a..483ac75c7 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -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( @@ -124,7 +124,8 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) 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( ( @@ -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. " @@ -262,7 +263,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) # 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/remind_me.py b/cogs/remind_me.py index b4b99072d..e3a4f37dd 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -181,7 +181,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord @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] ) @@ -304,7 +304,7 @@ async def clear_reminders_backlog(self) -> None: ), _reminder=reminder, ), - self.bot.users, + self.tex_bot.users, ) if not user: @@ -316,7 +316,7 @@ async def clear_reminders_backlog(self) -> None: continue # noinspection PyUnresolvedReferences - channel: discord.PartialMessageable = self.bot.get_partial_messageable( + channel: discord.PartialMessageable = self.tex_bot.get_partial_messageable( reminder.channel_id, type=( discord.ChannelType(reminder.channel_type.value) @@ -335,7 +335,7 @@ async def clear_reminders_backlog(self) -> None: "Reminder's channel_id must refer to a valid text channel/DM.", ), ) - await self.bot.close() + await self.tex_bot.close() await channel.send( "**Sorry it's a bit late! " @@ -348,4 +348,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 d0379521f..abcaa336d 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -69,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 - main_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`**" 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( @@ -168,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 " @@ -185,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 1de48385c..c1f1a094b 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -60,8 +60,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), ) @classmethod @@ -110,7 +110,7 @@ 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 - main_guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild member: discord.Member for member in main_guild.members: @@ -157,13 +157,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_ENABLED"] == "interval" else None # type: ignore[arg-type] ), @@ -193,7 +193,7 @@ class OptOutIntroductionRemindersView(View): @override def __init__(self, bot: TeXBot) -> None: """Initialise a new discord.View, to opt-in/out of introduction reminders.""" - self.bot: TeXBot = bot + self.tex_bot: TeXBot = bot super().__init__(timeout=None) @@ -205,7 +205,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, @@ -254,7 +254,7 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B 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: @@ -262,7 +262,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.""" @@ -320,4 +320,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 dc1e4879e..faef0a2da 100644 --- a/cogs/source.py +++ b/cogs/source.py @@ -14,13 +14,13 @@ 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( ( - f"{self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**"} " + 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)." diff --git a/cogs/startup.py b/cogs/startup.py index ea7731f39..e892d48f2 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -59,14 +59,14 @@ def _setup_discord_log_channel(self) -> None: existing_discord_logging_handler.name if existing_discord_logging_handler.name != DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME # noqa: E501 else ( - self.bot.user.name - if self.bot.user + self.tex_bot.user.name + if self.tex_bot.user else DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME) ), 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 ), ) @@ -89,24 +89,24 @@ def _setup_discord_log_channel(self) -> None: async def _initialise_main_guild(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_MAIN_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, + self.tex_bot.application_id, settings["_DISCORD_MAIN_GUILD_ID"]), ) logger.critical(GuildDoesNotExistError( guild_id=settings["_DISCORD_MAIN_GUILD_ID"]), ) - await self.bot.close() + await self.tex_bot.close() async def _check_strike_performed_manually_warning_location_exists(self) -> None: if settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"] == "DM": @@ -114,7 +114,7 @@ async def _check_strike_performed_manually_warning_location_exists(self) -> None STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_EXISTS: Final[bool] = bool( discord.utils.get( - self.bot.main_guild.text_channels, + self.tex_bot.main_guild.text_channels, name=settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"], ) # noqa: COM812 ) @@ -144,10 +144,10 @@ async def _check_strike_performed_manually_warning_location_exists(self) -> None repr("DM"), ) - await self.bot.close() + await self.tex_bot.close() async def _check_all_shortcut_accessors(self) -> None: - main_guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild if not discord.utils.get(main_guild.roles, name="Committee"): logger.warning(CommitteeRoleDoesNotExistError()) @@ -180,14 +180,14 @@ async def on_ready(self) -> None: await self._initialise_main_guild() - if self.bot.application_id: + if self.tex_bot.application_id: logger.debug( "Invite URL: %s", utils.generate_invite_url( - self.bot.application_id, + self.tex_bot.application_id, settings["_DISCORD_MAIN_GUILD_ID"]), ) await self._check_all_shortcut_accessors() - 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 0be0e5e0e..41fa457ec 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -188,7 +188,7 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str 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.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild channel_id: int = ctx.channel_id @@ -301,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 - main_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) @@ -385,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 " @@ -411,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." ), ), ], @@ -435,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 - main_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 @@ -498,12 +500,13 @@ 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." ), ), ) @@ -521,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 - main_guild: discord.Guild = self.bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild await ctx.defer(ephemeral=True) @@ -565,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 " @@ -588,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 acdb9960a..d1d2a8c8d 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -21,7 +21,7 @@ import re from collections.abc import Mapping from logging import Logger -from typing import Final, Literal +from typing import Final, Literal, TypeAlias import discord @@ -55,6 +55,26 @@ logger: Final[Logger] = logging.getLogger("TeX-Bot") +# noinspection PyTypeHints +PossibleMuteModerationActions: TypeAlias = Literal[ + discord.AuditLogAction.member_update + | discord.AuditLogAction.auto_moderation_user_communication_disabled +] + +# noinspection PyTypeHints +KnownModerationActions: TypeAlias = Literal[ + PossibleMuteModerationActions + | discord.AuditLogAction.kick + | discord.AuditLogAction.ban +] + +FORMATTED_MODERATION_ACTIONS: Final[Mapping[KnownModerationActions, 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 """ @@ -219,13 +239,13 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me # noinspection PyUnusedLocal 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,12 +260,13 @@ 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_LINK}>)\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 " @@ -258,7 +279,7 @@ async def _confirm_perform_moderation_action(self, message_sender_component: Mes 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 @@ -420,14 +441,14 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me 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 ) @@ -443,7 +464,7 @@ 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, + self.tex_bot.main_guild.text_channels, name=settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"], ) if not guild_confirmation_message_channel: @@ -459,10 +480,10 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me # noinspection PyTypeHints @capture_strike_tracking_error - async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.Member, action: Literal[discord.AuditLogAction.member_update | discord.AuditLogAction.kick | discord.AuditLogAction.ban]) -> None: # noqa: E501 + async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.Member, action: KnownModerationActions) -> 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 @@ -492,7 +513,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 @@ -500,20 +521,13 @@ 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, @@ -536,7 +550,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. " @@ -548,22 +562,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", + } + ), + ) ) match out_of_sync_ban_button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] @@ -627,7 +643,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. " @@ -638,7 +654,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 @@ -678,7 +694,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M await confirmation_message.delete() case "yes_manual_moderation_action": - interaction_user: discord.User | None = self.bot.get_user( + interaction_user: discord.User | None = self.tex_bot.get_user( applied_action_user.id, ) if not interaction_user: @@ -702,7 +718,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 @@ -710,6 +726,8 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) if not after.timed_out or before.timed_out == after.timed_out: return + mute_action_type: KnownModerationActions = discord.AuditLogAction.member_update + audit_log_entry: discord.AuditLogEntry async for audit_log_entry in main_guild.audit_logs(limit=5): FOUND_CORRECT_AUDIT_LOG_ENTRY: bool = ( @@ -719,27 +737,20 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) ) ) if FOUND_CORRECT_AUDIT_LOG_ENTRY: - await self._confirm_manual_add_strike( - strike_user=after, - action=audit_log_entry.action, - ) - return + mute_action_type = audit_log_entry.action # type: ignore[assignment] + break - # noinspection PyArgumentList - await self._confirm_manual_add_strike( - strike_user=after, - action=discord.AuditLogAction.member_update, - ) + await self._confirm_manual_add_strike(strike_user=after, action=mute_action_type) @TeXBotBaseCog.listener() @capture_guild_does_not_exist_error 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() @@ -749,7 +760,6 @@ async def on_member_remove(self, member: discord.Member) -> None: return with contextlib.suppress(NoAuditLogsStrikeTrackingError): - # noinspection PyArgumentList await self._confirm_manual_add_strike( strike_user=member, action=discord.AuditLogAction.kick, @@ -759,10 +769,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, @@ -781,7 +790,7 @@ async def strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set that have a member input-type. """ try: - main_guild: discord.Guild = ctx.bot.main_guild + main_guild: discord.Guild = ctx.tex_bot.main_guild except GuildDoesNotExistError: return set() @@ -825,7 +834,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_main_guild_member( + strike_member: discord.Member = await self.tex_bot.get_main_guild_member( 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 700cc062a..6d7127ee6 100644 --- a/cogs/write_roles.py +++ b/cogs/write_roles.py @@ -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 messages["OPT_IN_ROLES_SELECTORS"]: 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/main.py b/main.py index 0056feb45..b43b5f9d2 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,10 @@ #!/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 @@ -27,12 +27,12 @@ # noinspection PyDunderSlots,PyUnresolvedReferences intents.members = True - bot = TeXBot(intents=intents) + bot: TeXBot = TeXBot(intents=intents) bot.load_extension("cogs") -def _run_bot() -> NoReturn: +def _run_tex_bot() -> NoReturn: bot.run(settings["DISCORD_BOT_TOKEN"]) if bot.EXIT_REASON is TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG: @@ -40,10 +40,10 @@ def _run_bot() -> NoReturn: bot.reset_exit_reason() config.run_setup() bot.reload_extension("cogs") - _run_bot() + _run_tex_bot() raise SystemExit(bot.EXIT_REASON.value) if __name__ == "__main__": - _run_bot() + _run_tex_bot() diff --git a/utils/command_checks.py b/utils/command_checks.py index 12f08e05f..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] """ diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index 2d9de62fd..397fa0962 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -69,7 +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() + await self.tex_bot.close() return wrapper # type: ignore[return-value] @staticmethod diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 48c256de7..e1bc65877 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -64,9 +64,9 @@ class TeXBotBaseCog(Cog): } @override - def __init__(self, bot: TeXBot) -> None: + def __init__(self, tex_bot: TeXBot) -> None: """Initialise a new cog instance, storing a reference to the bot object.""" - self.bot: TeXBot = bot + 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, is_fatal: bool = False, responder_component: GenericResponderComponent | None = None) -> None: # noqa: E501 """ @@ -78,7 +78,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, *, error_code: and the bot will shortly close. """ await self._respond_with_error( - self.bot, + self.tex_bot, responder=( responder_component or SenderResponseComponent(ctx.interaction, ephemeral=True) ), @@ -207,14 +207,14 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ 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, ) diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index cac61ea80..efb7c7978 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -24,7 +24,7 @@ class TeXBotAutocompleteContext(discord.AutocompleteContext): should be used in cogs instead. """ - bot: TeXBot + tex_bot: TeXBot class TeXBotApplicationContext(discord.ApplicationContext): @@ -36,4 +36,4 @@ class TeXBotApplicationContext(discord.ApplicationContext): should be used in cogs instead. """ - bot: TeXBot + tex_bot: TeXBot From 8d98bcb60b4efb1c690d13e3218707fc66c620e9 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 12 Jul 2024 11:25:16 +0100 Subject: [PATCH 069/128] Update dependencies --- poetry.lock | 613 +++++++++++++++++++++++++------------------------ pyproject.toml | 9 +- 2 files changed, 321 insertions(+), 301 deletions(-) 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..0cc36190e 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" @@ -97,6 +97,7 @@ django_settings_module = "db._settings" [tool.ruff] +output-format = "concise" line-length = 95 target-version ="py312" extend-exclude = [ From a6091574aa4543120702cbd281605ec5a006257b Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 12 Jul 2024 11:49:09 +0100 Subject: [PATCH 070/128] Improve selected linting rules --- .github/workflows/tests.yaml | 2 +- cogs/get_token_authorisation.py | 3 ++- cogs/induct.py | 2 +- cogs/make_member.py | 1 + cogs/remind_me.py | 1 + cogs/strike.py | 9 +++++---- exceptions/does_not_exist.py | 3 +++ pyproject.toml | 2 +- utils/command_checks.py | 2 ++ 9 files changed, 17 insertions(+), 8 deletions(-) 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/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 122c79268..7a217b02c 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -108,6 +108,7 @@ 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 @@ -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..546807002 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -238,6 +238,7 @@ 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 @@ -353,7 +354,6 @@ async def induct(self, ctx: TeXBotApplicationContext, str_induct_member_id: str, 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) diff --git a/cogs/make_member.py b/cogs/make_member.py index e3fe77bd7..4bcd3b8fa 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -259,6 +259,7 @@ 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 diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 3505cef13..7f7689ecd 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -312,6 +312,7 @@ async def clear_reminders_backlog(self) -> None: await reminder.adelete() continue + # noinspection PyUnresolvedReferences channel: discord.PartialMessageable = self.bot.get_partial_messageable( reminder.channel_id, type=( diff --git a/cogs/strike.py b/cogs/strike.py index 48be3c6d4..36bc7f431 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -337,6 +337,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" @@ -455,6 +456,7 @@ 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 @@ -592,6 +594,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" @@ -653,6 +656,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 " @@ -716,7 +720,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, @@ -734,13 +737,12 @@ async def on_member_remove(self, member: discord.Member) -> None: 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, @@ -753,7 +755,6 @@ async def on_member_ban(self, guild: discord.Guild, user: discord.User | discord if guild != self.bot.main_guild or user.bot: return - # noinspection PyArgumentList await self._confirm_manual_add_strike( strike_user=user, action=discord.AuditLogAction.ban, diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index 5459f74cc..61006545b 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -260,11 +260,13 @@ def ROLE_NAME(cls) -> str: # noqa: N802,N805 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 return "E1025" + # noinspection PyMethodParameters @classproperty def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802, N805 """ @@ -275,6 +277,7 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802, N805 """ # noqa: D401 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 diff --git a/pyproject.toml b/pyproject.toml index 0cc36190e..873cc33a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/utils/command_checks.py b/utils/command_checks.py index 330d1f9c4..12f08e05f 100644 --- a/utils/command_checks.py +++ b/utils/command_checks.py @@ -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] From 50d33ee2b68f4097f4295cdd16ea913494a5ede0 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 12 Jul 2024 11:57:28 +0100 Subject: [PATCH 071/128] Improve `GetTokenAuthorisationCommandCog` name --- cogs/__init__.py | 6 +++--- cogs/get_token_authorisation.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) 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/get_token_authorisation.py b/cogs/get_token_authorisation.py index 7a217b02c..db5c575e9 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,7 +23,7 @@ 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] From 1a8a41b55ebbe09b9c1e733a1c8684f2d1913a71 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 12 Jul 2024 12:01:00 +0100 Subject: [PATCH 072/128] Improve `MessageSavingSenderComponent` name --- cogs/strike.py | 6 +++--- utils/__init__.py | 4 ++-- utils/message_sender_components.py | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cogs/strike.py b/cogs/strike.py index 36bc7f431..f53c2b3be 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -49,7 +49,7 @@ ) from utils.message_sender_components import ( ChannelMessageSender, - MessageSenderComponent, + MessageSavingSenderComponent, ResponseMessageSender, ) @@ -252,7 +252,7 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me "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(), @@ -298,7 +298,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." diff --git a/utils/__init__.py b/utils/__init__.py index fac494628..f0cb8be32 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -4,7 +4,7 @@ __all__: Sequence[str] = ( "CommandChecks", - "MessageSenderComponent", + "MessageSavingSenderComponent", "SuppressTraceback", "TeXBot", "TeXBotBaseCog", @@ -23,7 +23,7 @@ import discord from utils.command_checks import CommandChecks -from utils.message_sender_components import MessageSenderComponent +from utils.message_sender_components import MessageSavingSenderComponent from utils.suppress_traceback import SuppressTraceback from utils.tex_bot import TeXBot from utils.tex_bot_base_cog import TeXBotBaseCog diff --git a/utils/message_sender_components.py b/utils/message_sender_components.py index 196bb0fe8..060c12417 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", ) @@ -18,9 +18,9 @@ from utils.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. From fb360b427eb627fc4da283c72a27458e5c6f747a Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 12 Jul 2024 12:07:24 +0100 Subject: [PATCH 073/128] Ensure command descriptions end with punctuation --- cogs/annual_handover_and_reset.py | 4 ++-- cogs/delete_all.py | 2 +- cogs/remind_me.py | 2 +- cogs/stats.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/annual_handover_and_reset.py b/cogs/annual_handover_and_reset.py index 57371a114..4dfad5161 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 @@ -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 diff --git a/cogs/delete_all.py b/cogs/delete_all.py index aafb9eccb..93909a140 100644 --- a/cogs/delete_all.py +++ b/cogs/delete_all.py @@ -17,7 +17,7 @@ class DeleteAllCommandsCog(TeXBotBaseCog): delete_all: discord.SlashCommandGroup = discord.SlashCommandGroup( "delete-all", - "Delete all instances of the selected object type from the backend database", + "Delete all instances of the selected object type from the backend database.", ) @staticmethod diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 7f7689ecd..22e1f50ce 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -179,7 +179,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> set[str]: @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] ) diff --git a/cogs/stats.py b/cogs/stats.py index e27c263f3..22adb41f1 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -157,11 +157,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}", + f"Various statistics about {_DISCORD_SERVER_NAME} Discord server.", ) # noinspection SpellCheckingInspection From 684028069030c30845938769b20d1d87e39ff85f Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 12 Jul 2024 16:52:37 +0100 Subject: [PATCH 074/128] Make all custom exceptions inherit from `BaseTeXBotError` --- exceptions/__init__.py | 4 ++-- exceptions/config_changes.py | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/exceptions/__init__.py b/exceptions/__init__.py index 0a397c406..1da3e47e4 100644 --- a/exceptions/__init__.py +++ b/exceptions/__init__.py @@ -23,11 +23,11 @@ "MessagesJSONFileValueError", "InvalidMessagesJSONFileError", "ImproperlyConfiguredError", - "BotRequiresRestartAfterConfigChange", + "RestartRequiredDueToConfigChange", ) from .config_changes import ( - BotRequiresRestartAfterConfigChange, + RestartRequiredDueToConfigChange, ImproperlyConfiguredError, ) from .does_not_exist import ( diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index 721a45872..b0eb389c5 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -4,12 +4,40 @@ __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) From 26907c926809a599a3e8195598069fe9eece9399 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 12 Jul 2024 17:35:55 +0100 Subject: [PATCH 075/128] Fix references of "the bot" into "TeX-Bot" --- cogs/annual_handover_and_reset.py | 10 ++-- cogs/archive.py | 24 ++++----- cogs/command_error.py | 7 +-- cogs/edit_message.py | 6 +-- cogs/get_token_authorisation.py | 2 +- cogs/induct.py | 60 +++++++++++----------- cogs/kill.py | 6 +-- cogs/make_applicant.py | 20 ++++---- cogs/make_member.py | 18 +++---- cogs/remind_me.py | 8 +-- cogs/send_get_roles_reminders.py | 10 ++-- cogs/send_introduction_reminders.py | 22 ++++----- cogs/source.py | 2 +- cogs/startup.py | 30 +++++------ cogs/stats.py | 39 ++++++++------- cogs/strike.py | 77 +++++++++++++++-------------- cogs/write_roles.py | 4 +- exceptions/base.py | 19 ++++--- exceptions/does_not_exist.py | 52 +++++++++---------- main.py | 14 +++--- utils/command_checks.py | 4 +- utils/error_capture_decorators.py | 2 +- utils/tex_bot.py | 26 +++++----- utils/tex_bot_base_cog.py | 24 +++++---- utils/tex_bot_contexts.py | 42 ++++++++++++++-- 25 files changed, 287 insertions(+), 241 deletions(-) diff --git a/cogs/annual_handover_and_reset.py b/cogs/annual_handover_and_reset.py index 4dfad5161..e5cd08ace 100644 --- a/cogs/annual_handover_and_reset.py +++ b/cogs/annual_handover_and_reset.py @@ -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:", @@ -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..0c833da9d 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -39,14 +39,14 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> set[dis return set() try: - main_guild: discord.Guild = ctx.bot.main_guild - interaction_user: discord.Member = await ctx.bot.get_main_guild_member( + 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): + if not await ctx.tex_bot.check_user_has_committee_role(interaction_user): return set() return { @@ -80,13 +80,13 @@ 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() + 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.match(r"\A\d{17,20}\Z", str_category_id): await self.command_send_error( @@ -197,12 +197,12 @@ 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..8d920a220 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -48,7 +48,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 +57,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 +86,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/edit_message.py b/cogs/edit_message.py index fb230b4bc..f316bf495 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -35,13 +35,13 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[ return set() try: - interaction_user: discord.Member = await ctx.bot.get_main_guild_member( + 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): + if not await ctx.tex_bot.check_user_has_committee_role(interaction_user): return set() return await TeXBotBaseCog.autocomplete_get_text_channels(ctx) @@ -86,7 +86,7 @@ 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): await self.command_send_error( diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index db5c575e9..b9df0ffdf 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -111,7 +111,7 @@ async def get_token_authorisation(self, ctx: TeXBotApplicationContext) -> None: # 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 " diff --git a/cogs/induct.py b/cogs/induct.py index 546807002..a0b28bc63 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -53,13 +53,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 + guild: discord.Guild = self.tex_bot.main_guild if before.guild != guild or after.guild != 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 @@ -87,21 +87,21 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) # noinspection PyUnusedLocal 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" 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 +115,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"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 +152,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 +173,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 +181,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 + guest_role: discord.Role = await self.tex_bot.guest_role + main_guild: discord.Guild = self.tex_bot.main_guild intro_channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, @@ -212,18 +211,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" 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: @@ -241,7 +243,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb # 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( @@ -249,7 +251,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") @@ -268,7 +270,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 @@ -288,14 +290,14 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set[discor that have a member input-type. """ try: - guild: discord.Guild = ctx.bot.main_guild + 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} try: - guest_role: discord.Role = await ctx.bot.guest_role + guest_role: discord.Role = await ctx.tex_bot.guest_role except GuestRoleDoesNotExistError: return set() else: @@ -347,7 +349,7 @@ 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: @@ -398,7 +400,7 @@ async def non_silent_message_induct(self, ctx: TeXBotApplicationContext, message induct slash command using the message-context-menu instead of the user-menu. """ 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: @@ -425,7 +427,7 @@ async def silent_message_induct(self, ctx: TeXBotApplicationContext, message: di induct slash command using the message-context-menu instead of the user-menu. """ 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 +463,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..ba90c6d08 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -58,7 +58,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 +77,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,7 +93,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) + await self.tex_bot.perform_kill_and_close(initiated_by_user=ctx.interaction.user) return if button_interaction.data["custom_id"] == "shutdown_cancel": # type: ignore[index, typeddict-item] diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index adcdcf067..70e7a90e6 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -31,9 +31,9 @@ class BaseMakeApplicantCog(TeXBotBaseCog): 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 + 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 +49,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) @@ -60,7 +60,7 @@ async def _perform_make_applicant(self, ctx: TeXBotApplicationContext, applicant 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 @@ -99,8 +99,8 @@ 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 + guild: discord.Guild = ctx.tex_bot.main_guild + applicant_role: discord.Role = await ctx.tex_bot.applicant_role except (GuildDoesNotExistError, ApplicantRoleDoesNotExistError): return set() @@ -150,7 +150,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: @@ -188,7 +188,7 @@ async def message_make_applicant(self, ctx: TeXBotApplicationContext, message: d "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: diff --git a/cogs/make_member.py b/cogs/make_member.py index 4bcd3b8fa..3abce8900 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -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( @@ -124,7 +124,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) 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 {self.tex_bot.group_member_id_type} ID." ), ) return @@ -132,14 +132,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( ( @@ -212,11 +212,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"that you purchased your {self.tex_bot.group_short_name} membership with." ), ) return @@ -245,7 +245,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. " @@ -262,7 +262,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) # 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/remind_me.py b/cogs/remind_me.py index 22e1f50ce..56bbe9c38 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -301,7 +301,7 @@ async def clear_reminders_backlog(self) -> None: ), _reminder=reminder, ), - self.bot.users, + self.tex_bot.users, ) if not user: @@ -313,7 +313,7 @@ async def clear_reminders_backlog(self) -> None: continue # noinspection PyUnresolvedReferences - channel: discord.PartialMessageable = self.bot.get_partial_messageable( + channel: discord.PartialMessageable = self.tex_bot.get_partial_messageable( reminder.channel_id, type=( discord.ChannelType(reminder.channel_type.value) @@ -332,7 +332,7 @@ async def clear_reminders_backlog(self) -> None: "Reminder's channel_id must refer to a valid text channel/DM.", ), ) - await self.bot.close() + await self.tex_bot.close() return await channel.send( @@ -346,4 +346,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..bdf7b346f 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -67,13 +67,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 + 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" 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( @@ -166,7 +166,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 +183,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..fb6ccf9c4 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -57,8 +57,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,7 +79,7 @@ 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 + guild: discord.Guild = self.tex_bot.main_guild member: discord.Member for member in guild.members: @@ -152,13 +152,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] ), @@ -185,9 +185,9 @@ class OptOutIntroductionRemindersView(View): joining your group's Discord guild. """ - def __init__(self, bot: TeXBot) -> None: + def __init__(self, tex_bot: TeXBot) -> None: """Initialize a new discord.View, to opt-in/out of introduction reminders.""" - self.bot: TeXBot = bot + self.tex_bot: TeXBot = tex_bot super().__init__(timeout=None) @@ -199,7 +199,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, @@ -248,7 +248,7 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B 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 +256,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 +314,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..52cba0e06 100644 --- a/cogs/source.py +++ b/cogs/source.py @@ -14,7 +14,7 @@ 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.""" diff --git a/cogs/startup.py b/cogs/startup.py index 53c9d8ef3..e347dac67 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -33,17 +33,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,29 +62,29 @@ 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_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, + self.tex_bot.application_id, settings["DISCORD_GUILD_ID"]), ) logger.critical(GuildDoesNotExistError(guild_id=settings["DISCORD_GUILD_ID"])) - await self.bot.close() + await self.tex_bot.close() return - if self.bot.application_id: + if self.tex_bot.application_id: logger.debug( "Invite URL: %s", utils.generate_invite_url( - self.bot.application_id, + self.tex_bot.application_id, settings["DISCORD_GUILD_ID"]), ) @@ -135,7 +135,7 @@ async def on_ready(self) -> None: ), repr("DM"), ) - await self.bot.close() + await self.tex_bot.close() return - 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 22adb41f1..ad4b6fd67 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -199,7 +199,7 @@ 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 + guild: discord.Guild = self.tex_bot.main_guild channel: discord.TextChannel | None = discord.utils.get( guild.text_channels, id=channel_id, @@ -299,8 +299,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 + guild: discord.Guild = self.tex_bot.main_guild + guest_role: discord.Role = await self.tex_bot.guest_role await ctx.defer(ephemeral=True) @@ -383,12 +383,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 +410,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 +434,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 + 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 @@ -496,12 +498,13 @@ 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." ), ), ) @@ -519,7 +522,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 + guild: discord.Guild = self.tex_bot.main_guild await ctx.defer(ephemeral=True) @@ -563,16 +566,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 +589,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 f53c2b3be..2cd04f888 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -219,13 +219,13 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me # noinspection PyUnusedLocal 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,12 +240,13 @@ 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 " @@ -258,7 +259,7 @@ async def _confirm_perform_moderation_action(self, message_sender_component: Mes 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 @@ -419,14 +420,14 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me 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 ) @@ -442,7 +443,7 @@ 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, + self.tex_bot.main_guild.text_channels, name=settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"], ) if not guild_confirmation_message_channel: @@ -460,8 +461,8 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me @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 @@ -492,7 +493,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 @@ -500,7 +501,7 @@ 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( @@ -548,22 +549,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 @@ -637,7 +640,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 @@ -677,7 +680,7 @@ 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 @@ -697,7 +700,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 @@ -730,10 +733,10 @@ 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() @@ -752,7 +755,7 @@ 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 await self._confirm_manual_add_strike( @@ -773,7 +776,7 @@ async def strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set that have a member input-type. """ try: - guild: discord.Guild = ctx.bot.main_guild + guild: discord.Guild = ctx.tex_bot.main_guild except GuildDoesNotExistError: return set() @@ -818,7 +821,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..ad12bffb4 100644 --- a/cogs/write_roles.py +++ b/cogs/write_roles.py @@ -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/exceptions/base.py b/exceptions/base.py index bc018c4f7..b5b863b95 100644 --- a/exceptions/base.py +++ b/exceptions/base.py @@ -67,10 +67,10 @@ class BaseDoesNotExistError(BaseErrorWithErrorCode, ValueError, abc.ABC): @classproperty 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() @@ -78,10 +78,10 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 @classproperty 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() @@ -89,10 +89,10 @@ def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N802,N805 @classproperty 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() @@ -191,4 +191,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/does_not_exist.py b/exceptions/does_not_exist.py index 61006545b..dac8efaa0 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -42,7 +42,7 @@ class GuildDoesNotExistError(BaseDoesNotExistError): @classproperty def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 - return "Server with given ID does not exist or is not accessible to the bot." + return "Server with given ID does not exist or is not accessible to TeX-Bot." # noinspection PyMethodParameters,PyPep8Naming @classproperty @@ -112,10 +112,10 @@ def ERROR_CODE(cls) -> str: # noqa: N802,N805 @classproperty 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 # noinspection SpellCheckingInspection return frozenset( @@ -149,9 +149,9 @@ def ERROR_CODE(cls) -> str: # noqa: N802,N805 @classproperty 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, + This set being empty could mean that all commands require this Discord entity, or none of them do. """ # noqa: D401 return frozenset({"handover"}) @@ -175,10 +175,10 @@ def ERROR_CODE(cls) -> str: # noqa: N802,N805 @classproperty 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 # noinspection SpellCheckingInspection return frozenset({"induct", "stats", "archive", "ensure-members-inducted"}) @@ -187,10 +187,10 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 @classproperty 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 # noinspection SpellCheckingInspection return frozenset({"send_get_roles_reminders"}) @@ -215,10 +215,10 @@ def ERROR_CODE(cls) -> str: # noqa: N802,N805 @classproperty 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 # noinspection SpellCheckingInspection return frozenset({"makemember", "ensure-members-inducted"}) @@ -243,10 +243,10 @@ def ERROR_CODE(cls) -> str: # noqa: N802,N805 @classproperty 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 # noinspection SpellCheckingInspection return frozenset({"archive"}) @@ -270,9 +270,9 @@ def ERROR_CODE(cls) -> str: # noqa: N802, N805 @classproperty 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 thta all bot commands require this entity, + This set being empty could mean thta all commands require this entity, or that none of them do. """ # noqa: D401 return frozenset({"make_applicant"}) @@ -332,10 +332,10 @@ def ERROR_CODE(cls) -> str: # noqa: N802,N805 @classproperty 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 # noinspection SpellCheckingInspection return frozenset({"writeroles"}) @@ -360,10 +360,10 @@ def ERROR_CODE(cls) -> str: # noqa: N802,N805 @classproperty 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 # noinspection SpellCheckingInspection return frozenset({"induct"}) diff --git a/main.py b/main.py index cb6d12c25..450883955 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,15 @@ #!/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",) import discord @@ -25,14 +25,14 @@ # 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"]) + tex_bot.run(settings["DISCORD_BOT_TOKEN"]) - if bot.EXIT_WAS_DUE_TO_KILL_COMMAND: + if tex_bot.EXIT_WAS_DUE_TO_KILL_COMMAND: raise SystemExit(0) raise SystemExit(1) diff --git a/utils/command_checks.py b/utils/command_checks.py index 12f08e05f..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] """ diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index d8dc35184..6014642a8 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -68,7 +68,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() + await self.tex_bot.close() return None return wrapper # type: ignore[return-value] diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 9e7d99928..be671f6e8 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 @@ -41,12 +41,12 @@ class TeXBot(discord.Bot): Subclass of the default Bot class provided by Pycord. This subclass allows for storing commonly accessed roles & channels - from your group's Discord guild, while also raising the correct errors + from your group's main Discord guild, while also raising the correct errors if these objects do not exist. """ def __init__(self, *args: object, **options: object) -> None: - """Initialize a new discord.Bot subclass with empty shortcut accessors.""" + """Initialize 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 @@ -72,15 +72,15 @@ def EXIT_WAS_DUE_TO_KILL_COMMAND(self) -> bool: # noqa: N802 @property def main_guild(self) -> discord.Guild: """ - Shortcut accessor to your group's Discord guild object. + Shortcut accessor to your group's main Discord guild object. This shortcut accessor provides a consistent way of accessing - your group's Discord guild object without having to repeatedly search for it, - in the bot's list of guilds, by its ID. + your group's main Discord guild object without having to repeatedly search for it, + in the set of known guilds, by its ID. 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"]): + if not self._main_guild or not self._tex_bot_has_guild(settings["DISCORD_GUILD_ID"]): raise GuildDoesNotExistError(guild_id=settings["DISCORD_GUILD_ID"]) return self._main_guild @@ -276,10 +276,10 @@ 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. + This is substituted into many error/welcome messages sent into the main Discord guild, + 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. + or automatically identified from the name of your group's main Discord guild. """ return ( # type: ignore[no-any-return] settings["_GROUP_FULL_NAME"] @@ -370,7 +370,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: @@ -433,13 +433,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) diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index b59983ce9..3eb243fa0 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -33,7 +33,7 @@ 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 +61,13 @@ 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 + def __init__(self, tex_bot: TeXBot) -> None: + """ + Initialize a new cog instance. + + During initialization, 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 +85,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 +94,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 +107,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: " @@ -159,14 +163,14 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> set[ 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, ) diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 28170ecff..b9ff57303 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -10,6 +10,8 @@ __all__: Sequence[str] = ("TeXBotAutocompleteContext", "TeXBotApplicationContext") +from typing import override + import discord from utils.tex_bot import TeXBot @@ -19,21 +21,53 @@ class TeXBotAutocompleteContext(discord.AutocompleteContext): """ 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 + @override + def __init__(self, tex_bot: TeXBot, interaction: discord.Interaction) -> None: + self._tex_bot: TeXBot = tex_bot + + super().__init__(tex_bot, interaction) + + @property + def tex_bot(self) -> TeXBot: + return self._tex_bot + + @property + def bot(self) -> discord.Bot: + raise DeprecationWarning + + @bot.setter + def bot(self, __value: discord.Bot, /) -> None: + raise DeprecationWarning class TeXBotApplicationContext(discord.ApplicationContext): """ 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 + @override + def __init__(self, tex_bot: TeXBot, interaction: discord.Interaction) -> None: + self._tex_bot: TeXBot = tex_bot + + super().__init__(tex_bot, interaction) + + @property + def tex_bot(self) -> TeXBot: + return self._tex_bot + + @property + def bot(self) -> discord.Bot: + raise DeprecationWarning + + @bot.setter + def bot(self, __value: discord.Bot, /) -> None: + raise DeprecationWarning From 0d950156a41c74922e1ebafeef3eedf492dfdbc3 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Fri, 12 Jul 2024 17:41:36 +0100 Subject: [PATCH 076/128] Ensure all loggers annotated as `Final` --- cogs/archive.py | 3 ++- cogs/command_error.py | 3 ++- cogs/kill.py | 3 ++- cogs/make_member.py | 2 +- cogs/remind_me.py | 2 +- cogs/send_get_roles_reminders.py | 2 +- cogs/send_introduction_reminders.py | 2 +- cogs/startup.py | 3 ++- db/core/models/managers.py | 2 +- utils/error_capture_decorators.py | 2 +- utils/tex_bot.py | 2 +- utils/tex_bot_base_cog.py | 2 +- 12 files changed, 16 insertions(+), 12 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index 0c833da9d..acbcdfad6 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -8,6 +8,7 @@ import logging import re from logging import Logger +from typing import Final import discord @@ -21,7 +22,7 @@ TeXBotBaseCog, ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class ArchiveCommandCog(TeXBotBaseCog): diff --git a/cogs/command_error.py b/cogs/command_error.py index 8d920a220..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): diff --git a/cogs/kill.py b/cogs/kill.py index ba90c6d08..aaad0f050 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): diff --git a/cogs/make_member.py b/cogs/make_member.py index 3abce8900..7451924d4 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"""{ diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 56bbe9c38..31a63e359 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -27,7 +27,7 @@ from collections.abc import Iterator -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class RemindMeCommandCog(TeXBotBaseCog): diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index bdf7b346f..b64218b4a 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: import datetime -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class SendGetRolesRemindersTaskCog(TeXBotBaseCog): diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index fb6ccf9c4..a835444f5 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -30,7 +30,7 @@ capture_guild_does_not_exist_error, ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class SendIntroductionRemindersTaskCog(TeXBotBaseCog): diff --git a/cogs/startup.py b/cogs/startup.py index e347dac67..1c84b7295 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): diff --git a/db/core/models/managers.py b/db/core/models/managers.py index dc95cfaac..b9d6025f1 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): diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index 6014642a8..e3aba5ace 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -39,7 +39,7 @@ Callable[Concatenate[TeXBotBaseCog, P], Coroutine[object, object, T]] ) -logger: Logger = logging.getLogger("TeX-Bot") +logger: Final[Logger] = logging.getLogger("TeX-Bot") class ErrorCaptureDecorators: diff --git a/utils/tex_bot.py b/utils/tex_bot.py index be671f6e8..84fde7b1f 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -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): diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 3eb243fa0..bee38bc09 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -29,7 +29,7 @@ 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): From 650e8ff9fda1e40bc6b0353ca27a7742ea1a2cac Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 16:33:52 +0100 Subject: [PATCH 077/128] Use improved set type annotation in autocomplete functions --- cogs/archive.py | 3 ++- cogs/edit_message.py | 3 ++- cogs/induct.py | 3 ++- cogs/remind_me.py | 3 ++- exceptions/messages.py | 1 - utils/tex_bot_base_cog.py | 4 ++-- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index acbcdfad6..07cd8498e 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -7,6 +7,7 @@ import logging import re +from collections.abc import Set from logging import Logger from typing import Final @@ -29,7 +30,7 @@ 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. diff --git a/cogs/edit_message.py b/cogs/edit_message.py index f316bf495..80b81c3d9 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -6,6 +6,7 @@ import re +from collections.abc import Set import discord @@ -24,7 +25,7 @@ class EditMessageCommandCog(TeXBotBaseCog): """Cog class that defines the "/editmessage" 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. diff --git a/cogs/induct.py b/cogs/induct.py index a0b28bc63..a4462598c 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 @@ -282,7 +283,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. diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 31a63e359..1096f706a 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -10,6 +10,7 @@ import itertools import logging import re +from collections.abc import Set from logging import Logger from typing import TYPE_CHECKING, Final @@ -34,7 +35,7 @@ class RemindMeCommandCog(TeXBotBaseCog): """Cog class that defines the "/remindme" 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. diff --git a/exceptions/messages.py b/exceptions/messages.py index 23acfa2ef..b1b5df2a6 100644 --- a/exceptions/messages.py +++ b/exceptions/messages.py @@ -68,4 +68,3 @@ def __init__(self, message: str | None = None, dict_key: str | None = None, inva self.invalid_value: object | None = invalid_value super().__init__(message, dict_key) - diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index bee38bc09..c4e9f452e 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -8,7 +8,7 @@ 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 @@ -152,7 +152,7 @@ async def send_error(cls, tex_bot: TeXBot, interaction: discord.Interaction, int ) @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. From 732bef990bf52150cd20d1800cef145ff77daed7 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 16:42:39 +0100 Subject: [PATCH 078/128] Use improved formatting of channel & role names --- README.md | 4 ++-- cogs/induct.py | 6 +++--- cogs/send_get_roles_reminders.py | 2 +- cogs/strike.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) 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/induct.py b/cogs/induct.py index a4462598c..4bc9ca50f 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -86,12 +86,12 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) ) # noinspection PyUnusedLocal - rules_channel_mention: str = "`#welcome`" + rules_channel_mention: str = "**`#welcome`**" with contextlib.suppress(RulesChannelDoesNotExistError): 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.tex_bot.roles_channel).mention @@ -215,7 +215,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb 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.tex_bot.roles_channel).mention diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index b64218b4a..c5374420d 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -71,7 +71,7 @@ async def send_get_roles_reminders(self) -> None: 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.tex_bot.roles_channel).mention diff --git a/cogs/strike.py b/cogs/strike.py index 2cd04f888..5ca38e1cd 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -217,7 +217,7 @@ 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.tex_bot.rules_channel).mention From 347106cf50b4d9725a9c2c0eceecdde0ec3bb042 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 16:46:38 +0100 Subject: [PATCH 079/128] Make command names hyphenated --- cogs/edit_message.py | 4 ++-- cogs/remind_me.py | 4 ++-- cogs/write_roles.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 80b81c3d9..ec1eedc4e 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -22,7 +22,7 @@ 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] | Set[str]: # noqa: E501 @@ -49,7 +49,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ # 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] diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 1096f706a..e23d1d843 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -32,7 +32,7 @@ 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[discord.OptionChoice] | Set[str]: # noqa: C901,PLR0912,PLR0915,E501 @@ -174,7 +174,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord 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] diff --git a/cogs/write_roles.py b/cogs/write_roles.py index ad12bffb4..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 From 77afbe6bd628283dc4bd2a8ad91e7b97cc995e02 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 16:54:54 +0100 Subject: [PATCH 080/128] Use `re.fullmatch()`, rather than ~`re.match`~ --- cogs/archive.py | 2 +- cogs/edit_message.py | 4 ++-- cogs/make_member.py | 2 +- cogs/remind_me.py | 16 ++++++++-------- cogs/stats.py | 2 +- cogs/strike.py | 2 +- config.py | 20 ++++++++++---------- db/core/models/__init__.py | 2 +- db/core/models/utils.py | 2 +- tests/test_utils.py | 10 ++++++++-- utils/tex_bot.py | 2 +- utils/tex_bot_base_cog.py | 2 +- 12 files changed, 36 insertions(+), 30 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index 07cd8498e..720ac47af 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -90,7 +90,7 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> archivist_role: discord.Role = await self.tex_bot.archivist_role everyone_role: discord.Role = await self.tex_bot.get_everyone_role() - if not re.match(r"\A\d{17,20}\Z", str_category_id): + 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.", diff --git a/cogs/edit_message.py b/cogs/edit_message.py index ec1eedc4e..677d448da 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -89,7 +89,7 @@ async def edit_message(self, ctx: TeXBotApplicationContext, str_channel_id: str, # 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 - 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.", @@ -98,7 +98,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.", diff --git a/cogs/make_member.py b/cogs/make_member.py index 7451924d4..107f14b67 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -120,7 +120,7 @@ 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=( diff --git a/cogs/remind_me.py b/cogs/remind_me.py index e23d1d843..315ea0e1b 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -70,7 +70,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord 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), {"", " "}, @@ -90,7 +90,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord 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}""" @@ -103,7 +103,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord 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}" @@ -125,7 +125,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord 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, @@ -143,7 +143,7 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord 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), @@ -155,18 +155,18 @@ async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord 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")):]) diff --git a/cogs/stats.py b/cogs/stats.py index ad4b6fd67..83d3bf7c3 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -189,7 +189,7 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str 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.", diff --git a/cogs/strike.py b/cogs/strike.py index 5ca38e1cd..0a0361689 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -782,7 +782,7 @@ async def strike_autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> set members: set[discord.Member] = {member for member in 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 diff --git a/config.py b/config.py index f1238577d..5426fe8d5 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] = ( @@ -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] = ( @@ -401,7 +401,7 @@ def _setup_members_list_url_session_cookie(cls) -> None: 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), + and re.fullmatch(r"\A[A-Fa-f\d]{128,256}\Z", raw_members_list_url_session_cookie), ) if not MEMBERS_LIST_URL_SESSION_COOKIE_IS_VALID: INVALID_MEMBERS_LIST_URL_SESSION_COOKIE_MESSAGE: Final[str] = ( @@ -441,7 +441,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 +489,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 +544,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 +592,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")), ) diff --git a/db/core/models/__init__.py b/db/core/models/__init__.py index 7127cabcb..33bed40a6 100644 --- a/db/core/models/__init__.py +++ b/db/core/models/__init__.py @@ -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/utils.py b/db/core/models/utils.py index 454bd0581..8a0229c9d 100644 --- a/db/core/models/utils.py +++ b/db/core/models/utils.py @@ -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/tests/test_utils.py b/tests/test_utils.py index b6dcfd8ea..b9dbb79d0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -95,7 +95,13 @@ def test_url_generates() -> None: DISCORD_BOT_APPLICATION_ID, DISCORD_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_GUILD_ID) + + r".*\Z" + ), invite_url, ) diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 84fde7b1f..6c60dc660 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -466,7 +466,7 @@ 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." ) diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index c4e9f452e..1cc7ccfa1 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -174,7 +174,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ 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 From 4908ba772008d57143222e49cc6da8a5ee14f39b Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:02:48 +0100 Subject: [PATCH 081/128] Fix missed references of "the bot" to "TeX-Bot" --- CONTRIBUTING.md | 4 ++-- cogs/make_member.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c1e6be53..a4cd3993d 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. diff --git a/cogs/make_member.py b/cogs/make_member.py index 107f14b67..e7b5ff1eb 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -215,7 +215,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) 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"the {self.tex_bot.group_member_id_type} ID " f"that you purchased your {self.tex_bot.group_short_name} membership with." ), ) From 53f3de8cc5efcbdbe9ff3a4386ab4aa9f61a7e37 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:15:52 +0100 Subject: [PATCH 082/128] Add subclass `@override` decorator for overridden methods --- cogs/remind_me.py | 2 + cogs/send_get_roles_reminders.py | 4 +- cogs/send_introduction_reminders.py | 5 +- exceptions/base.py | 4 +- exceptions/does_not_exist.py | 187 +++++++++++----------------- exceptions/guild.py | 16 ++- exceptions/messages.py | 17 ++- exceptions/strike.py | 11 +- utils/suppress_traceback.py | 2 + utils/tex_bot.py | 3 +- utils/tex_bot_base_cog.py | 3 +- 11 files changed, 119 insertions(+), 135 deletions(-) diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 315ea0e1b..88912b8f9 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -262,12 +262,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. diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index c5374420d..738b6a1e4 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 @@ -34,6 +34,7 @@ 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. diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index a835444f5..6a27c665b 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 @@ -36,6 +36,7 @@ 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. @@ -185,6 +187,7 @@ class OptOutIntroductionRemindersView(View): joining your group's Discord guild. """ + @override def __init__(self, tex_bot: TeXBot) -> None: """Initialize a new discord.View, to opt-in/out of introduction reminders.""" self.tex_bot: TeXBot = tex_bot diff --git a/exceptions/base.py b/exceptions/base.py index b5b863b95..483868841 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 @@ -24,12 +24,14 @@ class BaseTeXBotError(BaseException, abc.ABC): 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.""" 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 diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index dac8efaa0..6e10eaaeb 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -18,7 +18,7 @@ import abc -from typing import Final +from typing import Final, override from classproperties import classproperty @@ -30,8 +30,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,22 +40,23 @@ 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 - return "Server with given ID does not exist or is not accessible to TeX-Bot." + @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.""" self.guild_id: int | None = guild_id @@ -71,14 +72,14 @@ 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 @@ -87,6 +88,7 @@ def DOES_NOT_EXIST_TYPE(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.""" HAS_DEPENDANTS: Final[bool] = bool( @@ -104,19 +106,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 commands that 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 + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 # noinspection SpellCheckingInspection return frozenset( { @@ -133,32 +130,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 commands that require this Discord entity. - - This set being empty could mean that all 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 +162,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 commands that 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 + @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 tasks that 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 + @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 +192,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 commands that 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 + @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,26 +215,21 @@ 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 commands that 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 + @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): @@ -262,25 +237,20 @@ class ApplicantRoleDoesNotExistError(RoleDoesNotExistError): # 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 commands that require this Discord entity. - - This set being empty could mean thta all 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" @@ -289,14 +259,14 @@ 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 @@ -305,6 +275,7 @@ def DOES_NOT_EXIST_TYPE(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.""" HAS_DEPENDANTS: Final[bool] = bool( @@ -324,26 +295,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 commands that 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 + @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" @@ -352,24 +318,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 commands that 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 + @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..342069a9f 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,10 +20,11 @@ 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.""" self.user_id: int | None = user_id @@ -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 b1b5df2a6..8172175ef 100644 --- a/exceptions/messages.py +++ b/exceptions/messages.py @@ -9,6 +9,8 @@ ) +from typing import override + from classproperties import classproperty from .base import BaseTeXBotError @@ -20,10 +22,11 @@ class InvalidMessagesJSONFileError(BaseTeXBotError, ImproperlyConfiguredError): # 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.""" self.dict_key: str | None = dict_key @@ -36,10 +39,11 @@ 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.""" super().__init__(message, dict_key=missing_key) @@ -59,10 +63,11 @@ 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.""" self.invalid_value: object | None = invalid_value diff --git a/exceptions/strike.py b/exceptions/strike.py index e4c0bc226..0de4c7d38 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,7 @@ 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 + def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "An error occurred while trying to track manually applied moderation actions." @@ -37,7 +39,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/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 6c60dc660..008c1e04e 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -8,7 +8,7 @@ import logging import re from logging import Logger -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, override import aiohttp import discord @@ -45,6 +45,7 @@ class TeXBot(discord.Bot): if these objects do not exist. """ + @override def __init__(self, *args: object, **options: object) -> None: """Initialize a new Pycord Bot subclass with empty shortcut accessors.""" self._main_guild: discord.Guild | None = None diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 1cc7ccfa1..1bc78a042 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -10,7 +10,7 @@ import re 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 @@ -61,6 +61,7 @@ class TeXBotBaseCog(Cog): "write_roles": "send messages", } + @override def __init__(self, tex_bot: TeXBot) -> None: """ Initialize a new cog instance. From 53ea6e1415c6b784c416ded19108f3d9c1346cc5 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:16:33 +0100 Subject: [PATCH 083/128] Fix missed references of "the bot" to "TeX-Bot" --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4cd3993d..80f63e1a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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) From 5e8c78d1351b4d30c1511fa7e7ccfeffb0c48f85 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:20:30 +0100 Subject: [PATCH 084/128] Use relative imports inside local packages to fix cyclical import errors --- utils/__init__.py | 12 ++++++------ utils/error_capture_decorators.py | 3 ++- utils/message_sender_components.py | 2 +- utils/tex_bot_base_cog.py | 4 ++-- utils/tex_bot_contexts.py | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/utils/__init__.py b/utils/__init__.py index f0cb8be32..e45be9fa4 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -22,12 +22,12 @@ import discord -from utils.command_checks import CommandChecks -from utils.message_sender_components import MessageSavingSenderComponent -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 diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index e3aba5ace..f418a3217 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 diff --git a/utils/message_sender_components.py b/utils/message_sender_components.py index 060c12417..45f444468 100644 --- a/utils/message_sender_components.py +++ b/utils/message_sender_components.py @@ -15,7 +15,7 @@ import discord from discord.ui import View -from utils.tex_bot_contexts import TeXBotApplicationContext +from .tex_bot_contexts import TeXBotApplicationContext class MessageSavingSenderComponent(abc.ABC): diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 1bc78a042..b4f03bf0b 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -19,8 +19,8 @@ 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 diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index b9ff57303..ac6855a95 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -14,7 +14,7 @@ import discord -from utils.tex_bot import TeXBot +from .tex_bot import TeXBot class TeXBotAutocompleteContext(discord.AutocompleteContext): From a6f161167b8f5442e74d6662d726216caf35606f Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:23:21 +0100 Subject: [PATCH 085/128] Fix missing import & class export --- cogs/remind_me.py | 2 +- exceptions/does_not_exist.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 88912b8f9..30da2e9cd 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -12,7 +12,7 @@ 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 diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index 6e10eaaeb..45d9ed910 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -11,6 +11,7 @@ "GuestRoleDoesNotExistError", "MemberRoleDoesNotExistError", "ArchivistRoleDoesNotExistError", + "ApplicantRoleDoesNotExistError", "ChannelDoesNotExistError", "RolesChannelDoesNotExistError", "GeneralChannelDoesNotExistError", From 3262c7b138d10275f863b9d234a210a3cee80068 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:31:29 +0100 Subject: [PATCH 086/128] Fix spelling & grammar mistakes --- cogs/archive.py | 2 +- cogs/edit_message.py | 2 +- cogs/get_token_authorisation.py | 2 +- cogs/induct.py | 25 +++++++++++++++---------- cogs/make_applicant.py | 6 +++--- cogs/send_introduction_reminders.py | 4 ++-- cogs/strike.py | 12 ++++++------ db/core/app_config.py | 2 +- db/core/models/__init__.py | 2 +- db/core/models/managers.py | 4 ++-- exceptions/base.py | 2 +- exceptions/does_not_exist.py | 6 +++--- exceptions/guild.py | 2 +- utils/__init__.py | 2 +- utils/tex_bot.py | 4 ++-- utils/tex_bot_base_cog.py | 6 +++--- utils/tex_bot_contexts.py | 2 +- 17 files changed, 45 insertions(+), 40 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index 720ac47af..ca1dcf855 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -34,7 +34,7 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[dis """ 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: diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 677d448da..0b7a6eb80 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -29,7 +29,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ """ 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: diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index b9df0ffdf..f5cc57f57 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -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. """ diff --git a/cogs/induct.py b/cogs/induct.py index 4bc9ca50f..0da9d7126 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -370,10 +370,10 @@ async def non_silent_induct(self, ctx: TeXBotApplicationContext, member: discord """ 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) @@ -385,8 +385,9 @@ async def silent_induct(self, ctx: TeXBotApplicationContext, member: discord.Mem 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) @@ -397,8 +398,10 @@ 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.tex_bot.get_member_from_str_id( @@ -424,8 +427,10 @@ 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.tex_bot.get_member_from_str_id( diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 70e7a90e6..d505867e3 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -25,7 +25,7 @@ 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. """ @@ -171,7 +171,7 @@ async def user_make_applicant(self, ctx: TeXBotApplicationContext, member: disco 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) @@ -184,7 +184,7 @@ 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: diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 6a27c665b..a3ef21719 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -183,13 +183,13 @@ 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. """ @override def __init__(self, tex_bot: TeXBot) -> None: - """Initialize a new discord.View, to opt-in/out of introduction reminders.""" + """Initialise a new discord.View, to opt-in/out of introduction reminders.""" self.tex_bot: TeXBot = tex_bot super().__init__(timeout=None) diff --git a/cogs/strike.py b/cogs/strike.py index 0a0361689..38a4cbbc3 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -97,7 +97,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 +114,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 +135,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 +153,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 +175,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 +193,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. 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 33bed40a6..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. diff --git a/db/core/models/managers.py b/db/core/models/managers.py index b9d6025f1..bcd5e8900 100644 --- a/db/core/models/managers.py +++ b/db/core/models/managers.py @@ -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/exceptions/base.py b/exceptions/base.py index 483868841..e4d9ad82a 100644 --- a/exceptions/base.py +++ b/exceptions/base.py @@ -26,7 +26,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N802,N805 @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) diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index 45d9ed910..af983cc68 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -59,7 +59,7 @@ def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 @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: @@ -91,7 +91,7 @@ def ROLE_NAME(cls) -> str: # noqa: N802,N805 @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, ) @@ -278,7 +278,7 @@ def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 @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, ) diff --git a/exceptions/guild.py b/exceptions/guild.py index 342069a9f..770322021 100644 --- a/exceptions/guild.py +++ b/exceptions/guild.py @@ -26,7 +26,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 @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) diff --git a/utils/__init__.py b/utils/__init__.py index e45be9fa4..77bf4ee08 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -69,7 +69,7 @@ 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 diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 008c1e04e..fc95e35e6 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -47,7 +47,7 @@ class TeXBot(discord.Bot): @override def __init__(self, *args: object, **options: object) -> None: - """Initialize a new Pycord 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 @@ -186,7 +186,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. """ diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index b4f03bf0b..f6269f2a1 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -64,9 +64,9 @@ class TeXBotBaseCog(Cog): @override def __init__(self, tex_bot: TeXBot) -> None: """ - Initialize a new cog instance. + Initialise a new cog instance. - During initialization, a reference to the currently running TeXBot instance is stored. + During initialisation, a reference to the currently running TeXBot instance is stored. """ self.tex_bot: TeXBot = tex_bot @@ -157,7 +157,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ """ 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: diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index ac6855a95..5c6abc199 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. """ From 788a136c3c47b7253a41ab61af45612c7f6a7702 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:35:03 +0100 Subject: [PATCH 087/128] Change calls to functions to use keyword arguments to be more explicit --- cogs/delete_all.py | 6 ++++-- cogs/stats.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cogs/delete_all.py b/cogs/delete_all.py index 93909a140..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/stats.py b/cogs/stats.py index 83d3bf7c3..818f7421a 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -160,8 +160,8 @@ class StatsCommandsCog(TeXBotBaseCog): }""" stats: discord.SlashCommandGroup = discord.SlashCommandGroup( - "stats", - f"Various statistics about {_DISCORD_SERVER_NAME} Discord server.", + name="stats", + description=f"Various statistics about {_DISCORD_SERVER_NAME}", ) # noinspection SpellCheckingInspection From 5afc748917b3dc526e7f4efdbc9c08cf1f2447c3 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:40:33 +0100 Subject: [PATCH 088/128] Fix incorrect usage of whitespace and newlines --- cogs/get_token_authorisation.py | 2 +- cogs/induct.py | 2 +- cogs/make_applicant.py | 8 +++----- exceptions/__init__.py | 1 + exceptions/does_not_exist.py | 1 + 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index f5cc57f57..eda8a767a 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -26,7 +26,7 @@ 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.", ) diff --git a/cogs/induct.py b/cogs/induct.py index 0da9d7126..64b9ea05f 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -41,6 +41,7 @@ logger: Logger = logging.getLogger("TeX-Bot") + class InductSendMessageCog(TeXBotBaseCog): """Cog class that defines the "/induct" command and its call-back method.""" @@ -317,7 +318,6 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor in members } - @discord.slash_command( # type: ignore[no-untyped-call, misc] name="induct", description=( diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index d505867e3..35639eabb 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -59,7 +59,6 @@ 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.tex_bot.get_emoji(743218410409820213) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") @@ -124,7 +123,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=( @@ -163,10 +161,10 @@ 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. @@ -176,7 +174,7 @@ async def user_make_applicant(self, ctx: TeXBotApplicationContext, member: disco """ 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 diff --git a/exceptions/__init__.py b/exceptions/__init__.py index 1da3e47e4..ccdbaf47d 100644 --- a/exceptions/__init__.py +++ b/exceptions/__init__.py @@ -26,6 +26,7 @@ "RestartRequiredDueToConfigChange", ) + from .config_changes import ( RestartRequiredDueToConfigChange, ImproperlyConfiguredError, diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index af983cc68..55b8f2631 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -233,6 +233,7 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 def ROLE_NAME(cls) -> str: # noqa: N805 return "Archivist" + class ApplicantRoleDoesNotExistError(RoleDoesNotExistError): """Exception class to raise when the "Applicant" Discord role is missing.""" From 9a00a0652163180fefdc544ec1ca46d72e3019aa Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:41:52 +0100 Subject: [PATCH 089/128] Improve ordering of Dockerfile directory copying --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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"] From 2a445ce6f46ccfeac7f42ad44a83307a44f83da8 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 13 Jul 2024 17:42:59 +0100 Subject: [PATCH 090/128] Fix grammar error --- utils/tex_bot_contexts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 5c6abc199..62f3b729b 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 overridden 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. """ From e937704b7427c889478da177d0674458ae38cd8a Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 01:35:54 +0100 Subject: [PATCH 091/128] Use the term "main guild" rather than just "guild" & improve shortcut accessors ordering --- cogs/induct.py | 10 +++++----- cogs/make_applicant.py | 4 ++-- cogs/send_get_roles_reminders.py | 8 ++++---- cogs/send_introduction_reminders.py | 6 +++--- cogs/startup.py | 10 ++++++---- config.py | 2 +- utils/tex_bot.py | 18 +++++++++--------- 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index 64b9ea05f..536fe7893 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -55,9 +55,9 @@ 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.tex_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: @@ -183,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.tex_bot.guest_role 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, @@ -292,11 +292,11 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor that have a member input-type. """ try: - guild: discord.Guild = ctx.tex_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} try: guest_role: discord.Role = await ctx.tex_bot.guest_role diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 35639eabb..4978b6f23 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -98,7 +98,7 @@ async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord options that have a member input-type. """ try: - guild: discord.Guild = ctx.tex_bot.main_guild + main_guild: discord.Guild = ctx.tex_bot.main_guild applicant_role: discord.Role = await ctx.tex_bot.applicant_role except (GuildDoesNotExistError, ApplicantRoleDoesNotExistError): return set() @@ -106,7 +106,7 @@ async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord 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 } diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 738b6a1e4..84d4cf484 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -69,7 +69,7 @@ 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.tex_bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild guest_role: discord.Role = await self.tex_bot.guest_role # noinspection PyUnusedLocal @@ -112,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) @@ -140,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 @@ -155,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 " diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index a3ef21719..6ef0c46ad 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -81,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.tex_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 @@ -140,7 +140,7 @@ async def send_introduction_reminders(self) -> None: 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 " diff --git a/cogs/startup.py b/cogs/startup.py index 1c84b7295..3d0b4dfc5 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -65,7 +65,7 @@ async def on_ready(self) -> None: try: main_guild: discord.Guild | None = self.tex_bot.main_guild except GuildDoesNotExistError: - main_guild = self.tex_bot.get_guild(settings["DISCORD_GUILD_ID"]) + main_guild = self.tex_bot.get_guild(settings["_DISCORD_MAIN_GUILD_ID"]) if main_guild: self.tex_bot.set_main_guild(main_guild) @@ -75,9 +75,11 @@ async def on_ready(self) -> None: "Invite URL: %s", utils.generate_invite_url( self.tex_bot.application_id, - settings["DISCORD_GUILD_ID"]), + settings["_DISCORD_MAIN_GUILD_ID"]), ) - logger.critical(GuildDoesNotExistError(guild_id=settings["DISCORD_GUILD_ID"])) + logger.critical(GuildDoesNotExistError( + guild_id=settings["_DISCORD_MAIN_GUILD_ID"]), + ) await self.tex_bot.close() return @@ -86,7 +88,7 @@ async def on_ready(self) -> None: "Invite URL: %s", utils.generate_invite_url( self.tex_bot.application_id, - settings["DISCORD_GUILD_ID"]), + settings["_DISCORD_MAIN_GUILD_ID"]), ) if not discord.utils.get(main_guild.roles, name="Committee"): diff --git a/config.py b/config.py index 5426fe8d5..c12f4c902 100644 --- a/config.py +++ b/config.py @@ -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: diff --git a/utils/tex_bot.py b/utils/tex_bot.py index fc95e35e6..97f6bdf50 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -41,7 +41,7 @@ class TeXBot(discord.Bot): Subclass of the default Bot class provided by Pycord. This subclass allows for storing commonly accessed roles & channels - from your group's main Discord guild, while also raising the correct errors + from your group's Discord guild, while also raising the correct errors if these objects do not exist. """ @@ -73,16 +73,16 @@ def EXIT_WAS_DUE_TO_KILL_COMMAND(self) -> bool: # noqa: N802 @property def main_guild(self) -> discord.Guild: """ - Shortcut accessor to your group's main Discord guild object. + Shortcut accessor to your group's Discord guild object. This shortcut accessor provides a consistent way of accessing - your group's main Discord guild object without having to repeatedly search for it, - in the set of known guilds, by its ID. + your group's Discord guild object without having to repeatedly search for it, + in the bot's list of guilds, by its ID. Raises `GuildDoesNotExist` if the given ID does not link to a valid Discord guild. """ - if not self._main_guild or not self._tex_bot_has_guild(settings["DISCORD_GUILD_ID"]): - raise GuildDoesNotExistError(guild_id=settings["DISCORD_GUILD_ID"]) + if not self._main_guild or not self._tex_bot_has_guild(settings["_DISCORD_MAIN_GUILD_ID"]): + raise GuildDoesNotExistError(guild_id=settings["_DISCORD_MAIN_GUILD_ID"]) return self._main_guild @@ -277,10 +277,10 @@ def group_full_name(self) -> str: """ The full name of your community group. - This is substituted into many error/welcome messages sent into the main Discord guild, + This is substituted into many error/welcome messages sent into your Discord guild, 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 main Discord guild. + 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] settings["_GROUP_FULL_NAME"] From fbecdabe015b1dd0539286b88d87a8447fd98f39 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 12:08:16 +0100 Subject: [PATCH 092/128] Apply missed small fixes --- cogs/archive.py | 4 +-- cogs/command_error.py | 4 +-- cogs/induct.py | 2 +- cogs/make_applicant.py | 4 +-- cogs/send_introduction_reminders.py | 4 +-- cogs/startup.py | 4 +-- cogs/stats.py | 4 +-- cogs/strike.py | 6 ++--- exceptions/__init__.py | 4 +-- exceptions/base.py | 18 ++++++------- exceptions/config_changes.py | 15 ++++++----- exceptions/does_not_exist.py | 37 +++++++++++-------------- exceptions/guild.py | 4 +-- exceptions/strike.py | 3 --- main.py | 16 +++++------ utils/message_sender_components.py | 1 + utils/tex_bot.py | 14 +++++----- utils/tex_bot_base_cog.py | 18 ++++++++----- utils/tex_bot_contexts.py | 42 ++++++++++++++++++++++++++--- 19 files changed, 116 insertions(+), 88 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index 97724f5df..ff9ef0c75 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -218,12 +218,12 @@ 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 0a746bc14..11644817d 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -126,12 +126,12 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro ), ) - BOT_NEEDS_CLOSING: Final[bool] = ( + TEX_BOT_NEEDS_CLOSING: Final[bool] = ( isinstance( error.original, RuntimeError | NotImplementedError | GuildDoesNotExistError, ) or type(error.original) is Exception ) - if BOT_NEEDS_CLOSING: + if TEX_BOT_NEEDS_CLOSING: await self.tex_bot.close() diff --git a/cogs/induct.py b/cogs/induct.py index dd41893d9..8c95ee7f1 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -280,7 +280,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 diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 0d1d8e40a..633e3153c 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -50,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) @@ -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 diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index c1f1a094b..73179600c 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -191,9 +191,9 @@ class OptOutIntroductionRemindersView(View): """ @override - def __init__(self, bot: TeXBot) -> None: + def __init__(self, tex_bot: TeXBot) -> None: """Initialise a new discord.View, to opt-in/out of introduction reminders.""" - self.tex_bot: TeXBot = bot + self.tex_bot: TeXBot = tex_bot super().__init__(timeout=None) diff --git a/cogs/startup.py b/cogs/startup.py index e892d48f2..a8a9776a4 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -172,9 +172,9 @@ async def _check_all_shortcut_accessors(self) -> None: @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 once TeX-Bot is ready to make API requests. """ self._setup_discord_log_channel() diff --git a/cogs/stats.py b/cogs/stats.py index 41fa457ec..92ed58079 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -158,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( name="stats", - description=f"Various statistics about {_DISCORD_SERVER_NAME}", + description=f"Various statistics about {_DISCORD_SERVER_NAME} Discord server", ) # noinspection SpellCheckingInspection diff --git a/cogs/strike.py b/cogs/strike.py index d1d2a8c8d..9e3a20575 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, Literal, TypeAlias @@ -782,7 +782,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] | set[str]: # 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. @@ -819,7 +819,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", ) diff --git a/exceptions/__init__.py b/exceptions/__init__.py index b8eb7132c..d90214e64 100644 --- a/exceptions/__init__.py +++ b/exceptions/__init__.py @@ -19,7 +19,7 @@ "EveryoneRoleCouldNotBeRetrievedError", "StrikeTrackingError", "NoAuditLogsStrikeTrackingError", - "BotRequiresRestartAfterConfigChange", + "RestartRequiredDueToConfigChange", "ChangingSettingWithRequiredSiblingError", "ErrorCodeCouldNotBeIdentifiedError", "UnknownDjangoError", @@ -27,8 +27,8 @@ from .config_changes import ( - BotRequiresRestartAfterConfigChange, ChangingSettingWithRequiredSiblingError, + RestartRequiredDueToConfigChange, ) from .custom_django import UnknownDjangoError from .does_not_exist import ( diff --git a/exceptions/base.py b/exceptions/base.py index af922ed85..e4d9ad82a 100644 --- a/exceptions/base.py +++ b/exceptions/base.py @@ -69,10 +69,10 @@ class BaseDoesNotExistError(BaseErrorWithErrorCode, ValueError, abc.ABC): @classproperty 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() @@ -80,10 +80,10 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 @classproperty 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() @@ -91,10 +91,10 @@ def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N802,N805 @classproperty 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() diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index c05c7fee2..3bd3f950a 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -3,11 +3,12 @@ from collections.abc import Sequence __all__: Sequence[str] = ( - "BotRequiresRestartAfterConfigChange", + "RestartRequiredDueToConfigChange", "ChangingSettingWithRequiredSiblingError", ) +from collections.abc import Set from typing import override from classproperties import classproperty @@ -15,19 +16,19 @@ from .base import BaseTeXBotError -class BotRequiresRestartAfterConfigChange(BaseTeXBotError, Exception): - """Exception class to raise to enforce handling of bot restarts after config changes.""" +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 due to configuration changes." + 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 a ValueError exception for a non-existent user ID.""" - self.changed_settings: set[str] | None = changed_settings + 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 eec47f433..83a70c353 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -139,24 +139,22 @@ def ROLE_NAME(cls) -> str: # noqa: N805 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" @@ -241,25 +239,20 @@ class ApplicantRoleDoesNotExistError(RoleDoesNotExistError): # noinspection PyMethodParameters @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 "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 that 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" diff --git a/exceptions/guild.py b/exceptions/guild.py index 70336aa1b..770322021 100644 --- a/exceptions/guild.py +++ b/exceptions/guild.py @@ -22,9 +22,9 @@ class DiscordMemberNotInMainGuildError(BaseTeXBotError, ValueError): @classproperty @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 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: """Initialise a ValueError exception for a non-existent user ID.""" self.user_id: int | None = user_id @@ -39,12 +39,10 @@ class EveryoneRoleCouldNotBeRetrievedError(BaseErrorWithErrorCode, ValueError): @classproperty @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "The reference to the \"@everyone\" role could not be correctly retrieved." # noinspection PyMethodParameters,PyPep8Naming @classproperty @override def ERROR_CODE(cls) -> str: # noqa: N805 - """The unique error code for users to tell admins about an error that occurred.""" # noqa: D401 return "E1042" diff --git a/exceptions/strike.py b/exceptions/strike.py index 643332363..0de4c7d38 100644 --- a/exceptions/strike.py +++ b/exceptions/strike.py @@ -25,9 +25,7 @@ class StrikeTrackingError(BaseTeXBotError, RuntimeError): # noinspection PyMethodParameters,PyPep8Naming @classproperty - @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "An error occurred while trying to track manually applied moderation actions." @@ -43,5 +41,4 @@ class NoAuditLogsStrikeTrackingError(BaseTeXBotError, RuntimeError): @classproperty @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 - """The message to be displayed alongside this exception class if none is provided.""" # noqa: D401 return "Unable to retrieve audit log entry after possible manual moderation action." diff --git a/main.py b/main.py index b43b5f9d2..4b9013549 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ from collections.abc import Sequence -__all__: Sequence[str] = ("bot",) +__all__: Sequence[str] = ("tex_bot",) from typing import NoReturn @@ -27,22 +27,22 @@ # noinspection PyDunderSlots,PyUnresolvedReferences intents.members = True - bot: TeXBot = TeXBot(intents=intents) + tex_bot: TeXBot = TeXBot(intents=intents) - bot.load_extension("cogs") + tex_bot.load_extension("cogs") def _run_tex_bot() -> NoReturn: - bot.run(settings["DISCORD_BOT_TOKEN"]) + tex_bot.run(settings["DISCORD_BOT_TOKEN"]) - if bot.EXIT_REASON is TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG: + if tex_bot.EXIT_REASON is TeXBotExitReason.RESTART_REQUIRED_DUE_TO_CHANGED_CONFIG: with SuppressTraceback(): - bot.reset_exit_reason() + tex_bot.reset_exit_reason() config.run_setup() - bot.reload_extension("cogs") + tex_bot.reload_extension("cogs") _run_tex_bot() - raise SystemExit(bot.EXIT_REASON.value) + raise SystemExit(tex_bot.EXIT_REASON.value) if __name__ == "__main__": diff --git a/utils/message_sender_components.py b/utils/message_sender_components.py index b61d97833..771d002bd 100644 --- a/utils/message_sender_components.py +++ b/utils/message_sender_components.py @@ -21,6 +21,7 @@ from .tex_bot_contexts import TeXBotApplicationContext +# noinspection PyPep8Naming class _VIEW_NOT_PROVIDED: # noqa: N801 pass diff --git a/utils/tex_bot.py b/utils/tex_bot.py index d4c48b038..b27423826 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 @@ -58,7 +58,7 @@ class TeXBot(discord.Bot): @override def __init__(self, *args: object, **options: object) -> None: - """Initialise 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 @@ -96,7 +96,7 @@ 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_MAIN_GUILD_ID"]): + if not self._main_guild or not self._tex_bot_has_guild(settings["_DISCORD_MAIN_GUILD_ID"]): raise GuildDoesNotExistError(guild_id=settings["_DISCORD_MAIN_GUILD_ID"]) return self._main_guild @@ -293,7 +293,7 @@ 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. + 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. """ @@ -386,7 +386,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: @@ -482,13 +482,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) diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index e1bc65877..6730f4c2f 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -35,7 +35,7 @@ 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", @@ -65,7 +65,11 @@ class TeXBotBaseCog(Cog): @override def __init__(self, tex_bot: TeXBot) -> None: - """Initialise a new cog instance, storing a reference to the bot object.""" + """ + 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, is_fatal: bool = False, responder_component: GenericResponderComponent | None = None) -> None: # noqa: E501 @@ -96,7 +100,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, *, error_code: ) @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, is_fatal: bool = False) -> None: # noqa: PLR0913,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, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 """ Construct & format an error message from the given details. @@ -105,7 +109,7 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, *, inte and the bot will shortly close. """ await cls._respond_with_error( - bot=bot, + bot=tex_bot, responder=SenderResponseComponent(interaction, ephemeral=True), interaction_name=interaction_name, error_code=error_code, @@ -115,7 +119,7 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, *, inte ) @classmethod - async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderComponent, *, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False) -> None: # noqa: PLR0913E501 + async def _respond_with_error(cls, tex_bot: TeXBot, responder: GenericResponderComponent, *, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 construct_error_message: str = ":warning:" if is_fatal: @@ -123,7 +127,7 @@ async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderCompo fatal_committee_mention: str = "committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - fatal_committee_mention = (await bot.committee_role).mention + fatal_committee_mention = (await tex_bot.committee_role).mention construct_error_message += ( "A fatal error occurred, " @@ -141,7 +145,7 @@ async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderCompo non_fatal_committee_mention: str = "committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - non_fatal_committee_mention = (await bot.committee_role).mention + non_fatal_committee_mention = (await tex_bot.committee_role).mention construct_error_message = ( f"**Contact a {non_fatal_committee_mention} member, " diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index efb7c7978..62f3b729b 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -10,6 +10,8 @@ __all__: Sequence[str] = ("TeXBotAutocompleteContext", "TeXBotApplicationContext") +from typing import override + import discord from .tex_bot import TeXBot @@ -19,21 +21,53 @@ class TeXBotAutocompleteContext(discord.AutocompleteContext): """ 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. """ - tex_bot: TeXBot + @override + def __init__(self, tex_bot: TeXBot, interaction: discord.Interaction) -> None: + self._tex_bot: TeXBot = tex_bot + + super().__init__(tex_bot, interaction) + + @property + def tex_bot(self) -> TeXBot: + return self._tex_bot + + @property + def bot(self) -> discord.Bot: + raise DeprecationWarning + + @bot.setter + def bot(self, __value: discord.Bot, /) -> None: + raise DeprecationWarning class TeXBotApplicationContext(discord.ApplicationContext): """ 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. """ - tex_bot: TeXBot + @override + def __init__(self, tex_bot: TeXBot, interaction: discord.Interaction) -> None: + self._tex_bot: TeXBot = tex_bot + + super().__init__(tex_bot, interaction) + + @property + def tex_bot(self) -> TeXBot: + return self._tex_bot + + @property + def bot(self) -> discord.Bot: + raise DeprecationWarning + + @bot.setter + def bot(self, __value: discord.Bot, /) -> None: + raise DeprecationWarning From f4c3d39719dbd02d7ed2d477884b936f89f29a80 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 12:13:15 +0100 Subject: [PATCH 093/128] Improve f-string formatting --- cogs/induct.py | 2 +- cogs/make_member.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index 536fe7893..0be5ed964 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -118,7 +118,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) await after.send( f"You can also get yourself an annual membership " f"to {self.tex_bot.group_full_name} for only £5! " - f"""Just head to {settings["PURCHASE_MEMBERSHIP_URL"]}. """ + 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: and a cool green name on " f"the {self.tex_bot.group_short_name} Discord server:green_square:! " diff --git a/cogs/make_member.py b/cogs/make_member.py index e7b5ff1eb..4f6e7a64a 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -124,7 +124,8 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) await self.command_send_error( ctx, message=( - f"{group_member_id!r} is not a valid {self.tex_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 From c8a7bb8d367aa3e6f9fc4958086dfd55bfd27607 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 12:16:56 +0100 Subject: [PATCH 094/128] Improve values embedded within strings --- cogs/edit_message.py | 4 ++-- exceptions/does_not_exist.py | 2 +- utils/tex_bot.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 0b7a6eb80..1434c1674 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -114,7 +114,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 @@ -123,7 +123,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/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index 55b8f2631..da1e1fe12 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -63,7 +63,7 @@ def __init__(self, message: str | None = None, guild_id: int | None = None) -> N 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) diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 97f6bdf50..3316a39fd 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -469,7 +469,7 @@ async def get_member_from_str_id(self, str_member_id: str) -> discord.Member: 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) From 127fb20c722d97dcda7fb897951dd71ea1cf990f Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 12:24:07 +0100 Subject: [PATCH 095/128] Improve usage of `Final` constants & capitalisation --- cogs/induct.py | 4 ++-- cogs/remind_me.py | 4 ++-- cogs/send_introduction_reminders.py | 12 ++++++----- cogs/strike.py | 31 ++++++++++++++--------------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index 0be5ed964..3ea1554f3 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -74,14 +74,14 @@ 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.", ) diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 30da2e9cd..58086d94f 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -223,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 @@ -231,7 +231,7 @@ 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", diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 6ef0c46ad..bd1f0d6ad 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -131,13 +131,13 @@ 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 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 @@ -231,7 +231,7 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B or (button.label and "Opt-out" in button.label), ) - _BUTTON_WILL_MAKE_OPT_IN: Final[bool] = bool( + BUTTON_WILL_MAKE_OPT_IN: Final[bool] = bool( button.style == discord.ButtonStyle.green or str(button.emoji) == emoji.emojize( ":raised_hand:", @@ -239,13 +239,15 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B ) or button.label and "Opt back in" in button.label) 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), ) 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 diff --git a/cogs/strike.py b/cogs/strike.py index 38a4cbbc3..d4453d562 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -55,6 +55,13 @@ 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 """ @@ -476,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: @@ -508,24 +514,17 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M 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), ) - 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 { @@ -537,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. " @@ -629,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. " From 259097311dc37f00c587da880ec6b2e02b2c093f Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 12:28:21 +0100 Subject: [PATCH 096/128] Fix missed `Final` constant usage --- cogs/archive.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index ca1dcf855..2d7dfaffb 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -123,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\".", @@ -157,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\".", From 03b9e0fec14675c8fbc681b09ba2638703de6e10 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 12:29:04 +0100 Subject: [PATCH 097/128] Improve ping command response message --- cogs/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"], From ff4766905c470e023787a0f8766de9a3c327be5d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 12:33:27 +0100 Subject: [PATCH 098/128] Improve source command response message --- cogs/source.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/source.py b/cogs/source.py index 52cba0e06..faef0a2da 100644 --- a/cogs/source.py +++ b/cogs/source.py @@ -20,8 +20,9 @@ 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, From 0a5626989116bc4ebe6cee401e3a0ff59f9e4264 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 12:58:18 +0100 Subject: [PATCH 099/128] Improve autocomplete exception handling --- cogs/archive.py | 6 +++--- cogs/edit_message.py | 8 ++------ cogs/induct.py | 17 ++++++++--------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index 2d7dfaffb..96551821b 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -41,6 +41,9 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[dis return set() try: + 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, @@ -48,9 +51,6 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[dis except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() - if not await ctx.tex_bot.check_user_has_committee_role(interaction_user): - return set() - return { discord.OptionChoice(name=category.name, value=str(category.id)) for category diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 1434c1674..842b8270b 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -36,15 +36,11 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ return set() try: - interaction_user: discord.Member = await ctx.tex_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.tex_bot.check_user_has_committee_role(interaction_user): - return set() - return await TeXBotBaseCog.autocomplete_get_text_channels(ctx) # noinspection SpellCheckingInspection diff --git a/cogs/induct.py b/cogs/induct.py index 3ea1554f3..c26eeff98 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -293,17 +293,16 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor """ try: main_guild: discord.Guild = ctx.tex_bot.main_guild - except GuildDoesNotExistError: - return set() - - members: set[discord.Member] = {member for member in main_guild.members if not member.bot} - - try: guest_role: discord.Role = await ctx.tex_bot.guest_role - except GuestRoleDoesNotExistError: + except (GuildDoesNotExistError, 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 { From 475d2a5c44efc1240edeab94ae336df9f843e058 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 13:07:37 +0100 Subject: [PATCH 100/128] Improve function names --- cogs/induct.py | 4 ++-- cogs/strike.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/induct.py b/cogs/induct.py index c26eeff98..defc59d43 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -365,7 +365,7 @@ 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. @@ -379,7 +379,7 @@ async def non_silent_induct(self, ctx: TeXBotApplicationContext, member: discord @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. diff --git a/cogs/strike.py b/cogs/strike.py index d4453d562..cd437c129 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -767,7 +767,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. @@ -805,7 +805,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", ) From 49e8e3c42c3655313de5272834e37c1eb9f14d78 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 13:14:44 +0100 Subject: [PATCH 101/128] Fix missing linting error changes --- cogs/send_introduction_reminders.py | 14 ++++++-------- exceptions/does_not_exist.py | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index bd1f0d6ad..a61ba1aa7 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -228,19 +228,17 @@ 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.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), + 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" diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index da1e1fe12..83a70c353 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -93,7 +93,7 @@ def ROLE_NAME(cls) -> str: # noqa: N802,N805 def __init__(self, message: str | None = None) -> None: """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: @@ -281,7 +281,7 @@ def CHANNEL_NAME(cls) -> str: # noqa: N802,N805 def __init__(self, message: str | None = None) -> None: """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: From c0bcd5f856c05b999198536dede1a337b59a92b6 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 13:19:55 +0100 Subject: [PATCH 102/128] Fix more uses of the term "guild" rather than "main guild" --- cogs/make_applicant.py | 1 + cogs/stats.py | 23 ++++++++++++----------- cogs/strike.py | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 4978b6f23..06fdfe469 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -31,6 +31,7 @@ class BaseMakeApplicantCog(TeXBotBaseCog): 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.""" + # 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 diff --git a/cogs/stats.py b/cogs/stats.py index 818f7421a..be742e797 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -186,6 +186,9 @@ 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: @@ -198,10 +201,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.tex_bot.main_guild channel: discord.TextChannel | None = discord.utils.get( - guild.text_channels, + main_guild.text_channels, id=channel_id, ) if not channel: @@ -217,7 +218,7 @@ 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( @@ -299,7 +300,7 @@ 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.tex_bot.main_guild + 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 +312,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( @@ -434,7 +435,7 @@ 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.tex_bot.main_guild + 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 @@ -454,7 +455,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( @@ -522,7 +523,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.tex_bot.main_guild + main_guild: discord.Guild = self.tex_bot.main_guild await ctx.defer(ephemeral=True) @@ -532,7 +533,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 diff --git a/cogs/strike.py b/cogs/strike.py index cd437c129..a3842e3e9 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -775,11 +775,11 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor that have a member input-type. """ try: - guild: discord.Guild = ctx.tex_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.fullmatch(r"\A@.*\Z", ctx.value): return { From 44d2104967ea1fd541734aefe16fa4db5fe4cb78 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 13:21:14 +0100 Subject: [PATCH 103/128] Fix incorrect punctuation --- cogs/make_applicant.py | 2 +- exceptions/config_changes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 06fdfe469..911cad2c5 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -132,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, diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index b0eb389c5..f781dcede 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -37,7 +37,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 @override def __init__(self, message: str | None = None, changed_settings: Set[str] | None = None) -> None: # noqa: E501 - """Initialise an Exception to apply configuration changes..""" + """Initialise an Exception to apply configuration changes.""" self.changed_settings: Set[str] | None = changed_settings if changed_settings else set() super().__init__(message) From befbc15544e00ccf7632a539c002a93de2551e1c Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 13:29:31 +0100 Subject: [PATCH 104/128] Fix more incorrect linting ignores & whitespace --- cogs/make_applicant.py | 2 +- cogs/strike.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 911cad2c5..0b7199ad0 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -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. diff --git a/cogs/strike.py b/cogs/strike.py index a3842e3e9..15d3732bc 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -522,7 +522,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M 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: out_of_sync_ban_confirmation_message: discord.Message = await confirmation_message_channel.send( # noqa: E501 From 541661c0aa8a48fb73cd36f172a91dd1abd684ff Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 13:33:02 +0100 Subject: [PATCH 105/128] Improve guard-clause function returns --- cogs/make_applicant.py | 1 + db/core/models/utils.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 0b7199ad0..6bd37cd61 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -197,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/db/core/models/utils.py b/db/core/models/utils.py index 8a0229c9d..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: From b832389e88715e0f423e12a014fe8d4599f8c9ff Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 13:45:39 +0100 Subject: [PATCH 106/128] Improve settings names --- cogs/make_member.py | 2 +- cogs/startup.py | 8 ++++---- cogs/strike.py | 8 ++++---- config.py | 36 ++++++++++++++++++------------------ 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/cogs/make_member.py b/cogs/make_member.py index 4f6e7a64a..b66687b63 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -160,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: diff --git a/cogs/startup.py b/cogs/startup.py index 3d0b4dfc5..b3d37cf05 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -109,11 +109,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: @@ -122,10 +122,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: diff --git a/cogs/strike.py b/cogs/strike.py index 15d3732bc..8da0f71a6 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -421,9 +421,9 @@ 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: @@ -451,12 +451,12 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me guild_confirmation_message_channel: discord.TextChannel | None = discord.utils.get( self.tex_bot.main_guild.text_channels, - name=settings["MANUAL_MODERATION_WARNING_MESSAGE_LOCATION"], + 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" ) diff --git a/config.py b/config.py index c12f4c902..96b8f0d51 100644 --- a/config.py +++ b/config.py @@ -394,22 +394,22 @@ 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.fullmatch(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: @@ -667,20 +667,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 +707,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 +719,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 From d8c77b4039e0f56d767c9d5c4ad47c21eeefa540 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 15:26:14 +0100 Subject: [PATCH 107/128] Fix command description --- cogs/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/stats.py b/cogs/stats.py index be742e797..8014de7e7 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -161,7 +161,7 @@ class StatsCommandsCog(TeXBotBaseCog): stats: discord.SlashCommandGroup = discord.SlashCommandGroup( name="stats", - description=f"Various statistics about {_DISCORD_SERVER_NAME}", + description=f"Various statistics about {_DISCORD_SERVER_NAME} Discord server", ) # noinspection SpellCheckingInspection From 9bcddd410069dc6d3d106c92a520ae34a4792fcb Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 15:28:33 +0100 Subject: [PATCH 108/128] Improve type annotations for messages iterable --- cogs/stats.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cogs/stats.py b/cogs/stats.py index 8014de7e7..6466cf335 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 @@ -221,7 +222,7 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str 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 @@ -327,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 @@ -466,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 From f3296a477b3c7a9bd404fccfdbdac9753f96a3c2 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 15:29:31 +0100 Subject: [PATCH 109/128] Fix missed use of the term "guild" rather than "main guild" --- tests/test_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index b9dbb79d0..a10ed97fa 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -87,12 +87,12 @@ 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.fullmatch( @@ -100,7 +100,7 @@ def test_url_generates() -> None: r"\Ahttps://discord.com/.*=" + str(DISCORD_BOT_APPLICATION_ID) + r".*=" - + str(DISCORD_GUILD_ID) + + str(DISCORD_MAIN_GUILD_ID) + r".*\Z" ), invite_url, From 828fc0f4fa45e62cf094c7a957ace2bfb7455c73 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 15:31:20 +0100 Subject: [PATCH 110/128] Improve running in `main.py` --- main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 450883955..66d1b1509 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,8 @@ __all__: Sequence[str] = ("tex_bot",) +from typing import NoReturn + import discord import config @@ -27,12 +29,17 @@ tex_bot: TeXBot = TeXBot(intents=intents) -tex_bot.load_extension("cogs") + tex_bot.load_extension("cogs") -if __name__ == "__main__": + +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() From c4b826e9ef1aec00376d07045195ae80346e65e1 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 15:32:24 +0100 Subject: [PATCH 111/128] Fix missed unhyphenated command name --- cogs/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/stats.py b/cogs/stats.py index 6466cf335..92ed58079 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -513,7 +513,7 @@ async def user_stats(self, ctx: TeXBotApplicationContext) -> None: # 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: From 884e90a57b6e56f73e817f5d1c432ba4d6c019eb Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 15:34:33 +0100 Subject: [PATCH 112/128] Improve string handling --- utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/__init__.py b/utils/__init__.py index 77bf4ee08..4f536d2d4 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -72,7 +72,7 @@ def is_member_inducted(member: discord.Member) -> bool: 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 ) From eafa980dc8e646025eb8f7a6424dfa7a8a407033 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 15:37:33 +0100 Subject: [PATCH 113/128] Annotate that `discord.Bot.close()` function will not return --- cogs/remind_me.py | 1 - utils/error_capture_decorators.py | 1 - utils/tex_bot.py | 8 ++++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 58086d94f..bc65ec8dc 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -336,7 +336,6 @@ async def clear_reminders_backlog(self) -> None: ), ) await self.tex_bot.close() - return await channel.send( "**Sorry it's a bit late! " diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index f418a3217..397fa0962 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -70,7 +70,6 @@ async def wrapper(self: TeXBotBaseCog, /, *args: P.args, **kwargs: P.kwargs) -> except error_type as error: close_func(error) await self.tex_bot.close() - return None return wrapper # type: ignore[return-value] @staticmethod diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 3316a39fd..8091b4d0e 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -8,7 +8,7 @@ import logging import re from logging import Logger -from typing import TYPE_CHECKING, Final, override +from typing import TYPE_CHECKING, Final, NoReturn, override import aiohttp import discord @@ -64,6 +64,10 @@ def __init__(self, *args: object, **options: object) -> None: super().__init__(*args, **options) # type: ignore[no-untyped-call] + @override + async def close(self) -> NoReturn: # type: ignore[misc] + await super().close() + # noinspection PyPep8Naming @property def EXIT_WAS_DUE_TO_KILL_COMMAND(self) -> bool: # noqa: N802 @@ -395,7 +399,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. From 510317f30bdfff4c0abe040107b2d79a0ceaeb12 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 15:45:03 +0100 Subject: [PATCH 114/128] Improve error handling with `TeXBot.close()` function --- utils/tex_bot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 8091b4d0e..e770601f8 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -65,9 +65,14 @@ def __init__(self, *args: object, **options: object) -> None: super().__init__(*args, **options) # type: ignore[no-untyped-call] @override - async def close(self) -> NoReturn: # type: ignore[misc] + 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 From 2a97bd303ac6a4cbd69b20c5ea77ee10a3c51305 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 16:08:13 +0100 Subject: [PATCH 115/128] Fix typing & linting errors --- cogs/archive.py | 6 +++-- cogs/kill.py | 1 - cogs/remind_me.py | 3 +-- cogs/startup.py | 2 -- cogs/strike.py | 10 +++++--- config.py | 4 ++- exceptions/__init__.py | 2 +- exceptions/config_changes.py | 4 ++- exceptions/messages.py | 3 +-- exceptions/strike.py | 1 + utils/tex_bot.py | 8 ++++-- utils/tex_bot_base_cog.py | 1 + utils/tex_bot_contexts.py | 49 +++++++++++++----------------------- 13 files changed, 46 insertions(+), 48 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index 96551821b..30bdf12fc 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -199,12 +199,14 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> await self.command_send_error( ctx, message=( - "TeX-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 ( - "TeX-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/kill.py b/cogs/kill.py index aaad0f050..44f146d88 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -95,7 +95,6 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: view=None, ) await self.tex_bot.perform_kill_and_close(initiated_by_user=ctx.interaction.user) - return if button_interaction.data["custom_id"] == "shutdown_cancel": # type: ignore[index, typeddict-item] await confirmation_message.edit( diff --git a/cogs/remind_me.py b/cogs/remind_me.py index bc65ec8dc..8ee490a6c 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -237,8 +237,7 @@ async def remind_me(self, ctx: TeXBotApplicationContext, delay: str, message: st "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, diff --git a/cogs/startup.py b/cogs/startup.py index b3d37cf05..ef35bf2da 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -81,7 +81,6 @@ async def on_ready(self) -> None: guild_id=settings["_DISCORD_MAIN_GUILD_ID"]), ) await self.tex_bot.close() - return if self.tex_bot.application_id: logger.debug( @@ -139,6 +138,5 @@ async def on_ready(self) -> None: repr("DM"), ) await self.tex_bot.close() - return logger.info("Ready! Logged in as %s", self.tex_bot.user) diff --git a/cogs/strike.py b/cogs/strike.py index 8da0f71a6..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 @@ -679,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.tex_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 @@ -779,7 +781,9 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor except GuildDoesNotExistError: return set() - members: set[discord.Member] = {member for member in main_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.fullmatch(r"\A@.*\Z", ctx.value): return { diff --git a/config.py b/config.py index 96b8f0d51..7e4b564c0 100644 --- a/config.py +++ b/config.py @@ -409,7 +409,9 @@ def _setup_members_list_auth_session_cookie(cls) -> None: ) raise ImproperlyConfiguredError(INVALID_MEMBERS_LIST_AUTH_SESSION_COOKIE_MESSAGE) - cls._settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"] = raw_members_list_auth_session_cookie + cls._settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"] = ( + raw_members_list_auth_session_cookie + ) @classmethod def _setup_send_introduction_reminders(cls) -> None: diff --git a/exceptions/__init__.py b/exceptions/__init__.py index ccdbaf47d..8f2e2446b 100644 --- a/exceptions/__init__.py +++ b/exceptions/__init__.py @@ -28,8 +28,8 @@ from .config_changes import ( - RestartRequiredDueToConfigChange, ImproperlyConfiguredError, + RestartRequiredDueToConfigChange, ) from .does_not_exist import ( ApplicantRoleDoesNotExistError, diff --git a/exceptions/config_changes.py b/exceptions/config_changes.py index f781dcede..d1d5914b1 100644 --- a/exceptions/config_changes.py +++ b/exceptions/config_changes.py @@ -38,6 +38,8 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 @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() + self.changed_settings: Set[str] | None = ( + changed_settings if changed_settings else set() + ) super().__init__(message) diff --git a/exceptions/messages.py b/exceptions/messages.py index 8172175ef..fc0c0d55a 100644 --- a/exceptions/messages.py +++ b/exceptions/messages.py @@ -13,11 +13,10 @@ 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 diff --git a/exceptions/strike.py b/exceptions/strike.py index 0de4c7d38..5624a4c90 100644 --- a/exceptions/strike.py +++ b/exceptions/strike.py @@ -25,6 +25,7 @@ class StrikeTrackingError(BaseTeXBotError, RuntimeError): # noinspection PyMethodParameters,PyPep8Naming @classproperty + @override def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 return "An error occurred while trying to track manually applied moderation actions." diff --git a/utils/tex_bot.py b/utils/tex_bot.py index e770601f8..35032ce55 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -90,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._tex_bot_has_guild(settings["_DISCORD_MAIN_GUILD_ID"]): + MAIN_GUILD_EXISTS: Final[bool] = bool( + self._main_guild + and self._tex_bot_has_guild(settings["_DISCORD_MAIN_GUILD_ID"]) + ) + 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: diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index f6269f2a1..669df4390 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -19,6 +19,7 @@ from exceptions.base import ( BaseDoesNotExistError, ) + from .tex_bot import TeXBot from .tex_bot_contexts import TeXBotApplicationContext, TeXBotAutocompleteContext diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 62f3b729b..0a7c7c99a 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -10,64 +10,51 @@ __all__: Sequence[str] = ("TeXBotAutocompleteContext", "TeXBotApplicationContext") -from typing import override +import abc +from typing import final, override import discord from .tex_bot import TeXBot -class TeXBotAutocompleteContext(discord.AutocompleteContext): - """ - Type-hinting class overriding AutocompleteContext's reference to the 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. - """ - +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) + super().__init__(tex_bot, interaction) # type: ignore[call-arg] @property def tex_bot(self) -> TeXBot: return self._tex_bot - @property + @property # type: ignore[misc] + @final def bot(self) -> discord.Bot: raise DeprecationWarning @bot.setter + @final def bot(self, __value: discord.Bot, /) -> None: raise DeprecationWarning -class TeXBotApplicationContext(discord.ApplicationContext): +class TeXBotAutocompleteContext(_TeXBotContextMixin, discord.AutocompleteContext): # type: ignore[misc] """ - Type-hinting class overriding ApplicationContext's reference to the Bot class. + Type-hinting class overriding AutocompleteContext's reference to the 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 + 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. """ - @override - def __init__(self, tex_bot: TeXBot, interaction: discord.Interaction) -> None: - self._tex_bot: TeXBot = tex_bot - super().__init__(tex_bot, interaction) - - @property - def tex_bot(self) -> TeXBot: - return self._tex_bot - - @property - def bot(self) -> discord.Bot: - raise DeprecationWarning +class TeXBotApplicationContext(_TeXBotContextMixin, discord.ApplicationContext): # type: ignore[misc] + """ + Type-hinting class overriding ApplicationContext's reference to the Bot class. - @bot.setter - def bot(self, __value: discord.Bot, /) -> None: - raise DeprecationWarning + 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. + """ From 4e9acec9391f3798739dc0cdb17bb121768b1569 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 16:08:29 +0100 Subject: [PATCH 116/128] Fix missed spelling mistakes --- exceptions/messages.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exceptions/messages.py b/exceptions/messages.py index fc0c0d55a..049f55d9b 100644 --- a/exceptions/messages.py +++ b/exceptions/messages.py @@ -27,7 +27,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 @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) @@ -44,7 +44,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 @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 @@ -68,7 +68,7 @@ def DEFAULT_MESSAGE(cls) -> str: # noqa: N805 @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) From 04227bf4bef95e5d743eddb6dceab83c42152449 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sun, 14 Jul 2024 16:09:51 +0100 Subject: [PATCH 117/128] Fix linting error --- utils/tex_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 35032ce55..6bed18ecc 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -92,7 +92,7 @@ def main_guild(self) -> discord.Guild: """ MAIN_GUILD_EXISTS: Final[bool] = bool( self._main_guild - and self._tex_bot_has_guild(settings["_DISCORD_MAIN_GUILD_ID"]) + 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"]) From 9831bca79381a470b98d9c0f5045c1c0c7039c8e Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 15 Jul 2024 21:49:19 +0100 Subject: [PATCH 118/128] Fix missing spaces in `#noqa` comments --- cogs/remind_me.py | 2 +- exceptions/base.py | 12 ++++++------ exceptions/does_not_exist.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 8ee490a6c..0c1c47b17 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -35,7 +35,7 @@ class RemindMeCommandCog(TeXBotBaseCog): """Cog class that defines the "/remind-me" command and its call-back method.""" @staticmethod - async def autocomplete_get_delays(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: C901,PLR0912,PLR0915,E501 + 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. diff --git a/exceptions/base.py b/exceptions/base.py index e4d9ad82a..d10ed0f96 100644 --- a/exceptions/base.py +++ b/exceptions/base.py @@ -21,7 +21,7 @@ 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 @@ -58,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 @@ -67,7 +67,7 @@ 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 commands that require this Discord entity. @@ -78,7 +78,7 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N802,N805 # 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 tasks that require this Discord entity. @@ -89,7 +89,7 @@ def DEPENDENT_TASKS(cls) -> frozenset[str]: # noqa: N802,N805 # 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 event listeners that require this Discord entity. @@ -101,7 +101,7 @@ def DEPENDENT_EVENTS(cls) -> frozenset[str]: # noqa: N802,N805 # 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 diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index 83a70c353..161708c10 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -86,7 +86,7 @@ def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 # 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 @@ -274,7 +274,7 @@ def DOES_NOT_EXIST_TYPE(cls) -> str: # noqa: N805 # 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 From 0af16ced5f76c4161cfd05bed50f9712504a010d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Sat, 20 Jul 2024 13:34:19 +0100 Subject: [PATCH 119/128] Add missing space --- cogs/startup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/startup.py b/cogs/startup.py index ef35bf2da..59add033a 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -36,7 +36,7 @@ async def on_ready(self) -> None: """ Populate the shortcut accessors of TeX-Bot after initialisation. - Shortcut accessors should only be populated onceTeX-Bot is ready to make API requests. + Shortcut accessors should only be populated once TeX-Bot is ready to make API requests. """ if settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"]: discord_logging_handler: logging.Handler = DiscordHandler( From cc743af881703b0d3cdb4c54acd4b213ffbf05a6 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 25 Jul 2024 16:57:41 +0100 Subject: [PATCH 120/128] Fix too limited Discord bot token regex --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index de250a57e..460f598ce 100644 --- a/config.py +++ b/config.py @@ -161,7 +161,7 @@ def _setup_discord_bot_token(cls) -> None: DISCORD_BOT_TOKEN_IS_VALID: Final[bool] = bool( raw_discord_bot_token and re.fullmatch( - r"\A([A-Za-z0-9]{24,26})\.([A-Za-z0-9]{6})\.([A-Za-z0-9_-]{27,38})\Z", + r"\A([A-Za-z0-9_-]{24,26})\.([A-Za-z0-9_-]{6})\.([A-Za-z0-9_-]{27,38})\Z", raw_discord_bot_token, ), ) From f3f7da89f43711863545b7586ed4b1f75069a72a Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 25 Jul 2024 18:01:34 +0100 Subject: [PATCH 121/128] Fix invalid references to `tex_bot` in context classes Full solution discussed in #261 --- cogs/annual_handover_and_reset.py | 6 +++--- cogs/archive.py | 6 +++--- cogs/edit_message.py | 2 +- cogs/get_token_authorisation.py | 2 +- cogs/induct.py | 6 +++--- cogs/make_applicant.py | 10 ++++----- cogs/make_member.py | 4 ++-- cogs/strike.py | 2 +- utils/command_checks.py | 4 ++-- utils/tex_bot_base_cog.py | 6 +++--- utils/tex_bot_contexts.py | 35 +++++++------------------------ 11 files changed, 31 insertions(+), 52 deletions(-) diff --git a/cogs/annual_handover_and_reset.py b/cogs/annual_handover_and_reset.py index 1422dd1f8..2c6365246 100644 --- a/cogs/annual_handover_and_reset.py +++ b/cogs/annual_handover_and_reset.py @@ -246,8 +246,8 @@ async def increment_year_channels(self, ctx: TeXBotApplicationContext) -> None: - Creates a new "first-years" 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.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 initial_message: discord.Interaction | discord.WebhookMessage = await ctx.respond( content=":hourglass: Incrementing year channels... :hourglass:", @@ -262,7 +262,7 @@ async def increment_year_channels(self, ctx: TeXBotApplicationContext) -> None: await initial_message.edit( content=":hourglass: Archiving \"final-years\" channel... :hourglass:", ) - archivist_role: discord.Role = await self.bot.archivist_role + archivist_role: discord.Role = await self.tex_bot.archivist_role await final_year_channel.set_permissions(guest_role, overwrite=None) await final_year_channel.set_permissions(archivist_role, read_messages=True) diff --git a/cogs/archive.py b/cogs/archive.py index 30bdf12fc..06817787b 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -41,11 +41,11 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[dis return set() try: - if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.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( + main_guild: discord.Guild = ctx.bot.main_guild + interaction_user: discord.Member = await ctx.bot.get_main_guild_member( ctx.interaction.user, ) except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 842b8270b..e81946e73 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -36,7 +36,7 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ return set() try: - if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index cd3fdff42..44f0cb570 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -111,7 +111,7 @@ async def get_token_authorisation(self, ctx: TeXBotApplicationContext) -> None: # noinspection PyUnusedLocal guest_role: discord.Role | None = None with contextlib.suppress(GuestRoleDoesNotExistError): - guest_role = await ctx.tex_bot.guest_role + guest_role = await ctx.bot.guest_role await ctx.respond( f"Admin token has access to the following MSL Organisations as " diff --git a/cogs/induct.py b/cogs/induct.py index 7472538f1..397965748 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -245,7 +245,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb # noinspection PyUnusedLocal applicant_role: discord.Role | None = None with contextlib.suppress(ApplicantRoleDoesNotExistError): - applicant_role = await ctx.tex_bot.applicant_role + applicant_role = await ctx.bot.applicant_role if applicant_role and applicant_role in induction_member.roles: await induction_member.remove_roles( @@ -292,8 +292,8 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor that have a member input-type. """ try: - main_guild: discord.Guild = ctx.tex_bot.main_guild - guest_role: discord.Role = await ctx.tex_bot.guest_role + main_guild: discord.Guild = ctx.bot.main_guild + guest_role: discord.Role = await ctx.bot.guest_role except (GuildDoesNotExistError, GuestRoleDoesNotExistError): return set() diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 6dfe943c0..1a7cb8fe5 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -32,9 +32,9 @@ class BaseMakeApplicantCog(TeXBotBaseCog): 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.""" # 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 + 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 if applicant_role in applicant_member.roles: await ctx.respond("User is already an applicant! Command aborted.") @@ -102,8 +102,8 @@ async def autocomplete_get_members(ctx: TeXBotApplicationContext) -> set[discord options that have a member input-type. """ try: - main_guild: discord.Guild = ctx.tex_bot.main_guild - applicant_role: discord.Role = await ctx.tex_bot.applicant_role + main_guild: discord.Guild = ctx.bot.main_guild + applicant_role: discord.Role = await ctx.bot.applicant_role except (GuildDoesNotExistError, ApplicantRoleDoesNotExistError): return set() diff --git a/cogs/make_member.py b/cogs/make_member.py index b66687b63..d32d96ec4 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -108,7 +108,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) """ # 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.tex_bot.member_role - interaction_member: discord.Member = await ctx.tex_bot.get_main_guild_member(ctx.user) + interaction_member: discord.Member = await ctx.bot.get_main_guild_member(ctx.user) if member_role in interaction_member.roles: await ctx.respond( @@ -263,7 +263,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) # noinspection PyUnusedLocal applicant_role: discord.Role | None = None with contextlib.suppress(ApplicantRoleDoesNotExistError): - applicant_role = await ctx.tex_bot.applicant_role + applicant_role = await ctx.bot.applicant_role if applicant_role and applicant_role in interaction_member.roles: await interaction_member.remove_roles( diff --git a/cogs/strike.py b/cogs/strike.py index aa5097c3d..64161ff46 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -777,7 +777,7 @@ async def autocomplete_get_members(ctx: TeXBotAutocompleteContext) -> Set[discor that have a member input-type. """ try: - main_guild: discord.Guild = ctx.tex_bot.main_guild + main_guild: discord.Guild = ctx.bot.main_guild except GuildDoesNotExistError: return set() diff --git a/utils/command_checks.py b/utils/command_checks.py index fc5bba769..12f08e05f 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.tex_bot.get_main_guild_member(ctx.user) + await ctx.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.tex_bot.check_user_has_committee_role(ctx.user) + return await ctx.bot.check_user_has_committee_role(ctx.user) check_interaction_user_has_committee_role: Callable[[T], T] """ diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 669df4390..1b26397a5 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -165,14 +165,14 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ return set() try: - main_guild: discord.Guild = ctx.tex_bot.main_guild + main_guild: discord.Guild = ctx.bot.main_guild # noinspection PyUnusedLocal - channel_permissions_limiter: MentionableMember = await ctx.tex_bot.guest_role + channel_permissions_limiter: MentionableMember = await ctx.bot.guest_role except BaseDoesNotExistError: return set() with contextlib.suppress(DiscordMemberNotInMainGuildError): - channel_permissions_limiter = await ctx.tex_bot.get_main_guild_member( + channel_permissions_limiter = await ctx.bot.get_main_guild_member( ctx.interaction.user, ) diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 0a7c7c99a..397036643 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -10,37 +10,12 @@ __all__: Sequence[str] = ("TeXBotAutocompleteContext", "TeXBotApplicationContext") -import abc -from typing import final, override - import discord -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 +from utils.tex_bot import TeXBot - @property # type: ignore[misc] - @final - def bot(self) -> discord.Bot: - raise DeprecationWarning - @bot.setter - @final - def bot(self, __value: discord.Bot, /) -> None: - raise DeprecationWarning - - -class TeXBotAutocompleteContext(_TeXBotContextMixin, discord.AutocompleteContext): # type: ignore[misc] +class TeXBotAutocompleteContext(discord.AutocompleteContext): """ Type-hinting class overriding AutocompleteContext's reference to the Bot class. @@ -49,8 +24,10 @@ class TeXBotAutocompleteContext(_TeXBotContextMixin, discord.AutocompleteContext should be used in cogs instead. """ + bot: TeXBot -class TeXBotApplicationContext(_TeXBotContextMixin, discord.ApplicationContext): # type: ignore[misc] + +class TeXBotApplicationContext(discord.ApplicationContext): """ Type-hinting class overriding ApplicationContext's reference to the Bot class. @@ -58,3 +35,5 @@ class TeXBotApplicationContext(_TeXBotContextMixin, discord.ApplicationContext): but cogs require a reference to the TeXBot class, so this ApplicationContext subclass should be used in cogs instead. """ + + bot: TeXBot From 8787f57b0f31f86807f2e6a66fefcc39bc8784ae Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 25 Jul 2024 19:11:25 +0100 Subject: [PATCH 122/128] Rename `tex_bot` to `bot` See #261 for more details --- cogs/annual_handover_and_reset.py | 16 +++++------ cogs/archive.py | 14 ++++----- cogs/command_error.py | 6 ++-- cogs/edit_message.py | 2 +- cogs/induct.py | 44 ++++++++++++++--------------- cogs/kill.py | 6 ++-- cogs/make_applicant.py | 6 ++-- cogs/make_member.py | 16 +++++------ cogs/remind_me.py | 10 +++---- cogs/send_get_roles_reminders.py | 10 +++---- cogs/send_introduction_reminders.py | 25 ++++++++-------- cogs/source.py | 2 +- cogs/startup.py | 26 ++++++++--------- cogs/stats.py | 36 +++++++++++------------ cogs/strike.py | 42 +++++++++++++-------------- cogs/write_roles.py | 4 +-- main.py | 20 ++++++++----- utils/error_capture_decorators.py | 2 +- utils/tex_bot.py | 4 +-- utils/tex_bot_base_cog.py | 13 +++++---- 20 files changed, 158 insertions(+), 146 deletions(-) diff --git a/cogs/annual_handover_and_reset.py b/cogs/annual_handover_and_reset.py index 2c6365246..71ce01ee7 100644 --- a/cogs/annual_handover_and_reset.py +++ b/cogs/annual_handover_and_reset.py @@ -45,9 +45,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.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 + 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 initial_response: discord.Interaction | discord.WebhookMessage = await ctx.respond( content=":hourglass: Running handover procedures... :hourglass:", @@ -181,8 +181,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.tex_bot.main_guild - member_role: discord.Role = await self.tex_bot.member_role + main_guild: discord.Guild = self.bot.main_guild + member_role: discord.Role = await self.bot.member_role logger.debug("Reset roles command called.") initial_response: discord.Interaction | discord.WebhookMessage = await ctx.respond( @@ -246,8 +246,8 @@ async def increment_year_channels(self, ctx: TeXBotApplicationContext) -> None: - Creates a new "first-years" 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 - guest_role: discord.Role = await self.tex_bot.guest_role + main_guild: discord.Guild = self.bot.main_guild + guest_role: discord.Role = await self.bot.guest_role initial_message: discord.Interaction | discord.WebhookMessage = await ctx.respond( content=":hourglass: Incrementing year channels... :hourglass:", @@ -262,7 +262,7 @@ async def increment_year_channels(self, ctx: TeXBotApplicationContext) -> None: await initial_message.edit( content=":hourglass: Archiving \"final-years\" channel... :hourglass:", ) - archivist_role: discord.Role = await self.tex_bot.archivist_role + archivist_role: discord.Role = await self.bot.archivist_role await final_year_channel.set_permissions(guest_role, overwrite=None) await final_year_channel.set_permissions(archivist_role, read_messages=True) diff --git a/cogs/archive.py b/cogs/archive.py index 06817787b..7e348adea 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -82,13 +82,13 @@ 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.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() + 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.fullmatch(r"\A\d{17,20}\Z", str_category_id): await self.command_send_error( diff --git a/cogs/command_error.py b/cogs/command_error.py index 37319586b..2b027958e 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -50,7 +50,7 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro if CommandChecks.is_interaction_user_in_main_guild_failure(error.checks[0]): message = ( "You must be a member of " - f"the {self.tex_bot.group_short_name} Discord server " + f"the {self.bot.group_short_name} Discord server " "to use this command." ) @@ -58,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.tex_bot.committee_role).mention + committee_role_mention = (await self.bot.committee_role).mention message = f"Only {committee_role_mention} members can run this command." await self.command_send_error( @@ -87,4 +87,4 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro if message_part ), ) - await self.tex_bot.close() + await self.bot.close() diff --git a/cogs/edit_message.py b/cogs/edit_message.py index e81946e73..f6139efb0 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -83,7 +83,7 @@ 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.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild if not re.fullmatch(r"\A\d{17,20}\Z", str_channel_id): await self.command_send_error( diff --git a/cogs/induct.py b/cogs/induct.py index 397965748..8ef52075e 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -55,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 - main_guild: discord.Guild = self.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: return try: - guest_role: discord.Role = await self.tex_bot.guest_role + guest_role: discord.Role = await self.bot.guest_role except GuestRoleDoesNotExistError: return @@ -89,21 +89,21 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) # noinspection PyUnusedLocal rules_channel_mention: str = "**`#welcome`**" with contextlib.suppress(RulesChannelDoesNotExistError): - rules_channel_mention = (await self.tex_bot.rules_channel).mention + rules_channel_mention = (await self.bot.rules_channel).mention # noinspection PyUnusedLocal roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.tex_bot.roles_channel).mention + roles_channel_mention = (await self.bot.roles_channel).mention user_type: Literal["guest", "member"] = "guest" with contextlib.suppress(MemberRoleDoesNotExistError): - if await self.tex_bot.member_role in after.roles: + if await self.bot.member_role in after.roles: user_type = "member" try: await after.send( - f"**Congrats on joining the {self.tex_bot.group_short_name} Discord server " + f"**Congrats on joining the {self.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" @@ -117,11 +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.tex_bot.group_full_name} for only £5! " + f"to {self.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: and a cool green name on " - f"the {self.tex_bot.group_short_name} Discord server:green_square:! " + f"the {self.bot.group_short_name} Discord server:green_square:! " f"Checkout all the perks at {settings["MEMBERSHIP_PERKS_URL"]}", ) except discord.Forbidden: @@ -154,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.tex_bot.committee_role).mention + committee_role_mention: str = (await self.bot.committee_role).mention except CommitteeRoleDoesNotExistError: return await self.get_random_welcome_message(induction_member) else: @@ -175,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.tex_bot.group_short_name, + self.bot.group_short_name, ) return random_welcome_message.strip() @@ -183,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 - main_guild: discord.Guild = self.tex_bot.main_guild - guest_role: discord.Role = await self.tex_bot.guest_role + main_guild: discord.Guild = self.bot.main_guild + guest_role: discord.Role = await self.bot.guest_role intro_channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, @@ -213,18 +213,18 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb return if not silent: - general_channel: discord.TextChannel = await self.tex_bot.general_channel + general_channel: discord.TextChannel = await self.bot.general_channel # noinspection PyUnusedLocal roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.tex_bot.roles_channel).mention + roles_channel_mention = (await self.bot.roles_channel).mention message_already_sent: bool = False message: discord.Message async for message in general_channel.history(limit=7): message_already_sent = ( - message.author == self.tex_bot.user + message.author == self.bot.user and "grab your roles" in message.content ) if message_already_sent: @@ -253,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.tex_bot.get_emoji(743218410409820213) + tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") @@ -349,7 +349,7 @@ async def induct(self, ctx: TeXBotApplicationContext, str_induct_member_id: str, """ member_id_not_integer_error: ValueError try: - induct_member: discord.Member = await self.tex_bot.get_member_from_str_id( + induct_member: discord.Member = await self.bot.get_member_from_str_id( str_induct_member_id, ) except ValueError as member_id_not_integer_error: @@ -403,7 +403,7 @@ async def non_silent_message_induct(self, ctx: TeXBotApplicationContext, message by giving them the "Guest" role. """ try: - member: discord.Member = await self.tex_bot.get_member_from_str_id( + member: discord.Member = await self.bot.get_member_from_str_id( str(message.author.id), ) except ValueError: @@ -432,7 +432,7 @@ async def silent_message_induct(self, ctx: TeXBotApplicationContext, message: di by giving them the "Guest" role, only without broadcasting a welcome message. """ try: - member: discord.Member = await self.tex_bot.get_member_from_str_id( + member: discord.Member = await self.bot.get_member_from_str_id( str(message.author.id), ) except ValueError: @@ -468,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.tex_bot.main_guild - member_role: discord.Role = await self.tex_bot.member_role - guest_role: discord.Role = await self.tex_bot.guest_role + 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 await ctx.defer(ephemeral=True) diff --git a/cogs/kill.py b/cogs/kill.py index 44f146d88..3269998b9 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -59,7 +59,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: """ committee_role: discord.Role | None = None with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_role = await self.tex_bot.committee_role + committee_role = await self.bot.committee_role response: discord.Message | discord.Interaction = await ctx.respond( content=( @@ -78,7 +78,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: else await response.original_response() ) - button_interaction: discord.Interaction = await self.tex_bot.wait_for( + button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component @@ -94,7 +94,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: content="My battery is low and it's getting dark...", view=None, ) - await self.tex_bot.perform_kill_and_close(initiated_by_user=ctx.interaction.user) + await self.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 1a7cb8fe5..879a20bc8 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -58,7 +58,7 @@ async def _perform_make_applicant(self, ctx: TeXBotApplicationContext, applicant await applicant_member.add_roles(applicant_role, reason=AUDIT_MESSAGE) logger.debug("Applicant role given to user %s", applicant_member) - tex_emoji: discord.Emoji | None = self.tex_bot.get_emoji(743218410409820213) + tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") @@ -152,7 +152,7 @@ async def make_applicant(self, ctx: TeXBotApplicationContext, str_applicant_memb """ member_id_not_integer_error: ValueError try: - applicant_member: discord.Member = await self.tex_bot.get_member_from_str_id( + applicant_member: discord.Member = await self.bot.get_member_from_str_id( str_applicant_member_id, ) except ValueError as member_id_not_integer_error: @@ -190,7 +190,7 @@ async def message_make_applicant(self, ctx: TeXBotApplicationContext, message: d "Applicant" role and removes the "Guest" role if they have it. """ try: - member: discord.Member = await self.tex_bot.get_member_from_str_id( + member: discord.Member = await self.bot.get_member_from_str_id( str(message.author.id), ) except ValueError: diff --git a/cogs/make_member.py b/cogs/make_member.py index d32d96ec4..0697c99f6 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -107,7 +107,7 @@ 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.tex_bot.member_role + member_role: discord.Role = await self.bot.member_role interaction_member: discord.Member = await ctx.bot.get_main_guild_member(ctx.user) if member_role in interaction_member.roles: @@ -125,7 +125,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) ctx, message=( f"{group_member_id!r} is not a valid " - f"{self.tex_bot.group_member_id_type} ID." + f"{self.bot.group_member_id_type} ID." ), ) return @@ -133,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.tex_bot.group_member_id_type, + self.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.tex_bot.committee_role).mention + committee_mention = (await self.bot.committee_role).mention await ctx.respond( ( @@ -213,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.tex_bot.group_full_name} " + f"You must be a member of {self.bot.group_full_name} " "to use this command.\n" f"The provided {_GROUP_MEMBER_ID_ARGUMENT_NAME} must match " - f"the {self.tex_bot.group_member_id_type} ID " - f"that you purchased your {self.tex_bot.group_short_name} membership with." + f"the {self.bot.group_member_id_type} ID " + f"that you purchased your {self.bot.group_short_name} membership with." ), ) return @@ -246,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.tex_bot.guest_role + guest_role: discord.Role = await self.bot.guest_role except GuestRoleDoesNotExistError: logger.warning( "\"/makemember\" command used but the \"Guest\" role does not exist. " diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 0c1c47b17..9c5249c9b 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -237,7 +237,7 @@ async def remind_me(self, ctx: TeXBotApplicationContext, delay: str, message: st "Error when creating DiscordReminder object: %s", create_discord_reminder_error, ) - await self.tex_bot.close() + await self.bot.close() await self.command_send_error( ctx, @@ -303,7 +303,7 @@ async def clear_reminders_backlog(self) -> None: ), _reminder=reminder, ), - self.tex_bot.users, + self.bot.users, ) if not user: @@ -315,7 +315,7 @@ async def clear_reminders_backlog(self) -> None: continue # noinspection PyUnresolvedReferences - channel: discord.PartialMessageable = self.tex_bot.get_partial_messageable( + channel: discord.PartialMessageable = self.bot.get_partial_messageable( reminder.channel_id, type=( discord.ChannelType(reminder.channel_type.value) @@ -334,7 +334,7 @@ async def clear_reminders_backlog(self) -> None: "Reminder's channel_id must refer to a valid text channel/DM.", ), ) - await self.tex_bot.close() + await self.bot.close() await channel.send( "**Sorry it's a bit late! " @@ -347,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.tex_bot.wait_until_ready() + await self.bot.wait_until_ready() diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 84d4cf484..1505d502c 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -69,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 - main_guild: discord.Guild = self.tex_bot.main_guild - guest_role: discord.Role = await self.tex_bot.guest_role + main_guild: discord.Guild = self.bot.main_guild + guest_role: discord.Role = await self.bot.guest_role # noinspection PyUnusedLocal roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.tex_bot.roles_channel).mention + roles_channel_mention = (await self.bot.roles_channel).mention # noinspection SpellCheckingInspection OPT_IN_ROLE_NAMES: Final[frozenset[str]] = frozenset( @@ -168,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.tex_bot.group_short_name} Discord server " + f"on the {self.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 " @@ -185,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.tex_bot.wait_until_ready() + await self.bot.wait_until_ready() diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index a61ba1aa7..8b56ec38e 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -59,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.tex_bot.add_view( - self.OptOutIntroductionRemindersView(self.tex_bot), + self.bot.add_view( + self.OptOutIntroductionRemindersView(self.bot), ) @tasks.loop(**settings["SEND_INTRODUCTION_REMINDERS_INTERVAL"]) # type: ignore[misc] @@ -81,7 +81,7 @@ 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 - main_guild: discord.Guild = self.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild member: discord.Member for member in main_guild.members: @@ -154,13 +154,13 @@ async def send_introduction_reminders(self) -> None: await member.send( content=( "Hey! It seems like you joined " - f"the {self.tex_bot.group_short_name} Discord server " + f"the {self.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.tex_bot) + self.OptOutIntroductionRemindersView(self.bot) if settings["SEND_INTRODUCTION_REMINDERS"] == "interval" else None # type: ignore[arg-type] ), @@ -188,9 +188,12 @@ class OptOutIntroductionRemindersView(View): """ @override - def __init__(self, tex_bot: TeXBot) -> None: + def __init__(self, bot: TeXBot) -> None: """Initialise a new discord.View, to opt-in/out of introduction reminders.""" - self.tex_bot: TeXBot = tex_bot + # NOTE: The attribute/variable name `bot` is used here for consistency. + # NOTE: `tex_bot` would be preferred but would be inconsitent with the required attribute name of Pycord's context classes + # NOTE: See https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/261 + self.bot: TeXBot = bot super().__init__(timeout=None) @@ -202,7 +205,7 @@ async def send_error(self, interaction: discord.Interaction, error_code: str | N to the given interaction. """ await TeXBotBaseCog.send_error( - self.tex_bot, + self.bot, interaction, interaction_name="opt_out_introduction_reminders", error_code=error_code, @@ -251,7 +254,7 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B return try: - interaction_member: discord.Member = await self.tex_bot.get_main_guild_member( + interaction_member: discord.Member = await self.bot.get_main_guild_member( interaction.user, ) except DiscordMemberNotInMainGuildError: @@ -259,7 +262,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.tex_bot.group_short_name} Discord server " + f"of the {self.bot.group_short_name} Discord server " f"""to opt{ "-out of" if BUTTON_WILL_MAKE_OPT_OUT else " back in to" } introduction reminders.""" @@ -317,4 +320,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.tex_bot.wait_until_ready() + await self.bot.wait_until_ready() diff --git a/cogs/source.py b/cogs/source.py index faef0a2da..7ec7bb2cf 100644 --- a/cogs/source.py +++ b/cogs/source.py @@ -20,7 +20,7 @@ async def source(self, ctx: TeXBotApplicationContext) -> None: """Definition & callback response of the "source" command.""" await ctx.respond( ( - f"{self.tex_bot.user.mention if self.tex_bot.user else "**`@TeX-Bot`**"} " + f"{self.bot.user.mention if self.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)." diff --git a/cogs/startup.py b/cogs/startup.py index 59add033a..428c6a7b3 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -40,11 +40,11 @@ async def on_ready(self) -> None: """ if settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"]: discord_logging_handler: logging.Handler = DiscordHandler( - self.tex_bot.user.name if self.tex_bot.user else "TeXBot", + self.bot.user.name if self.bot.user else "TeXBot", settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"], avatar_url=( - self.tex_bot.user.avatar.url - if self.tex_bot.user and self.tex_bot.user.avatar + self.bot.user.avatar.url + if self.bot.user and self.bot.user.avatar else None ), ) @@ -63,30 +63,30 @@ async def on_ready(self) -> None: ) try: - main_guild: discord.Guild | None = self.tex_bot.main_guild + main_guild: discord.Guild | None = self.bot.main_guild except GuildDoesNotExistError: - main_guild = self.tex_bot.get_guild(settings["_DISCORD_MAIN_GUILD_ID"]) + main_guild = self.bot.get_guild(settings["_DISCORD_MAIN_GUILD_ID"]) if main_guild: - self.tex_bot.set_main_guild(main_guild) + self.bot.set_main_guild(main_guild) if not main_guild: - if self.tex_bot.application_id: + if self.bot.application_id: logger.info( "Invite URL: %s", utils.generate_invite_url( - self.tex_bot.application_id, + self.bot.application_id, settings["_DISCORD_MAIN_GUILD_ID"]), ) logger.critical(GuildDoesNotExistError( guild_id=settings["_DISCORD_MAIN_GUILD_ID"]), ) - await self.tex_bot.close() + await self.bot.close() - if self.tex_bot.application_id: + if self.bot.application_id: logger.debug( "Invite URL: %s", utils.generate_invite_url( - self.tex_bot.application_id, + self.bot.application_id, settings["_DISCORD_MAIN_GUILD_ID"]), ) @@ -137,6 +137,6 @@ async def on_ready(self) -> None: ), repr("DM"), ) - await self.tex_bot.close() + await self.bot.close() - logger.info("Ready! Logged in as %s", self.tex_bot.user) + logger.info("Ready! Logged in as %s", self.bot.user) diff --git a/cogs/stats.py b/cogs/stats.py index 92ed58079..7067c7f4e 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -188,7 +188,7 @@ async def channel_stats(self, ctx: TeXBotApplicationContext, str_channel_id: str 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 + main_guild: discord.Guild = self.bot.main_guild channel_id: int = ctx.channel_id @@ -301,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 - main_guild: discord.Guild = self.tex_bot.main_guild - guest_role: discord.Role = await self.tex_bot.guest_role + main_guild: discord.Guild = self.bot.main_guild + guest_role: discord.Role = await self.bot.guest_role await ctx.defer(ephemeral=True) @@ -386,12 +386,12 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: ), title=( "Most Active Roles in " - f"the {self.tex_bot.group_short_name} Discord Server" + f"the {self.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.tex_bot.group_short_name} Discord server." + f"in the {self.bot.group_short_name} Discord server." ), extra_text=( "Messages sent by members with multiple roles are counted once " @@ -412,12 +412,12 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: ), title=( "Most Active Channels " - f"in the {self.tex_bot.group_short_name} Discord Server" + f"in the {self.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.tex_bot.group_short_name} Discord server." + f"in the {self.bot.group_short_name} Discord server." ), ), ], @@ -436,16 +436,16 @@ 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 - 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 + main_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 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.tex_bot.group_short_name} Discord server " + f"of the {self.bot.group_short_name} Discord server " "to use this command." ), ) @@ -500,13 +500,13 @@ async def user_stats(self, ctx: TeXBotApplicationContext) -> None: ), title=( "Your Most Active Channels " - f"in the {self.tex_bot.group_short_name} Discord Server" + f"in the {self.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} " "in different channels in " - f"the {self.tex_bot.group_short_name} Discord server." + f"the {self.bot.group_short_name} Discord server." ), ), ) @@ -524,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 - main_guild: discord.Guild = self.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild await ctx.defer(ephemeral=True) @@ -568,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.tex_bot.group_short_name} Discord Server" + f"the {self.bot.group_short_name} Discord Server" ), title=( "Most Common Roles that Members had when they left " - f"the {self.tex_bot.group_short_name} Discord Server" + f"the {self.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.tex_bot.group_short_name} Discord server." + f"that have left the {self.bot.group_short_name} Discord server." ), extra_text=( "Members that left with multiple roles " @@ -591,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.tex_bot.main_guild or member.bot: + if member.guild != self.bot.main_guild or member.bot: return await LeftDiscordMember.objects.acreate( diff --git a/cogs/strike.py b/cogs/strike.py index 64161ff46..d335c870c 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -226,13 +226,13 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me # noinspection PyUnusedLocal rules_channel_mention: str = "**`#welcome`**" with contextlib.suppress(RulesChannelDoesNotExistError): - rules_channel_mention = (await self.tex_bot.rules_channel).mention + rules_channel_mention = (await self.bot.rules_channel).mention includes_ban_message: str = ( ( "\nBecause you now have been given 3 strikes, you have been banned from " - f"the {self.tex_bot.group_short_name} Discord server " - f"and we have contacted {self.tex_bot.group_moderation_contact} for " + f"the {self.bot.group_short_name} Discord server " + f"and we have contacted {self.bot.group_moderation_contact} for " "further action & advice." ) if member_strikes.strikes >= 3 @@ -247,13 +247,13 @@ 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.tex_bot.group_short_name} Discord server's rules.\n" + f"the {self.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, " "you can view " - f"the {self.tex_bot.group_short_name} Discord server moderation document " + f"the {self.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 " @@ -266,7 +266,7 @@ async def _confirm_perform_moderation_action(self, message_sender_component: Mes view=ConfirmStrikeMemberView(), ) - button_interaction: discord.Interaction = await self.tex_bot.wait_for( + button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component @@ -427,14 +427,14 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me if user.bot: fetch_log_channel_error: RuntimeError try: - return await self.tex_bot.fetch_log_channel() + return await self.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.tex_bot.get_user(user.id) + self.bot.get_user(user.id) if isinstance(user, discord.Member) else user ) @@ -450,7 +450,7 @@ 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.tex_bot.main_guild.text_channels, + self.bot.main_guild.text_channels, name=settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"], ) if not guild_confirmation_message_channel: @@ -468,8 +468,8 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me @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.tex_bot.main_guild - committee_role: discord.Role = await self.tex_bot.committee_role + main_guild: discord.Guild = self.bot.main_guild + committee_role: discord.Role = await self.bot.committee_role try: # noinspection PyTypeChecker @@ -499,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.tex_bot.user: + if applied_action_user == self.bot.user: return fetch_log_channel_error: RuntimeError @@ -507,7 +507,7 @@ 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.tex_bot.fetch_log_channel() + else await self.bot.fetch_log_channel() ) except RuntimeError as fetch_log_channel_error: raise StrikeTrackingError( @@ -549,7 +549,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M ) out_of_sync_ban_button_interaction: discord.Interaction = ( - await self.tex_bot.wait_for( + await self.bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component @@ -639,7 +639,7 @@ async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.M view=ConfirmManualModerationView(), ) - button_interaction: discord.Interaction = await self.tex_bot.wait_for( + button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component @@ -679,7 +679,7 @@ 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.tex_bot.get_user( + interaction_user: discord.User | None = self.bot.get_user( applied_action_user.id, ) if not interaction_user: @@ -701,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.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: return @@ -734,10 +734,10 @@ 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.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild MEMBER_REMOVED_BECAUSE_OF_MANUALLY_APPLIED_KICK: Final[bool] = bool( - member.guild == self.tex_bot.main_guild + member.guild == self.bot.main_guild and not member.bot and not await asyncany( ban.user == member async for ban in main_guild.bans() @@ -756,7 +756,7 @@ 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.tex_bot.main_guild or user.bot: + if guild != self.bot.main_guild or user.bot: return await self._confirm_manual_add_strike( @@ -824,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.tex_bot.get_member_from_str_id( + strike_member: discord.Member = await self.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 79789c7c5..8c3503c9d 100644 --- a/cogs/write_roles.py +++ b/cogs/write_roles.py @@ -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.tex_bot.roles_channel + roles_channel: discord.TextChannel = await self.bot.roles_channel roles_message: str for roles_message in settings["ROLES_MESSAGES"]: await roles_channel.send( - roles_message.replace("", self.tex_bot.group_short_name), + roles_message.replace("", self.bot.group_short_name), ) await ctx.respond("All messages sent successfully.", ephemeral=True) diff --git a/main.py b/main.py index 66d1b1509..a237c2054 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ from collections.abc import Sequence -__all__: Sequence[str] = ("tex_bot",) +__all__: Sequence[str] = ("bot",) from typing import NoReturn @@ -27,19 +27,25 @@ # noinspection PyDunderSlots,PyUnresolvedReferences intents.members = True - tex_bot: TeXBot = TeXBot(intents=intents) + # NOTE: The variable name `bot` is used here for consistency. + # NOTE: `tex_bot` would be preferred but would be inconsitent with the required attribute name of Pycord's context classes + # NOTE: See https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/261 + bot: TeXBot = TeXBot(intents=intents) - tex_bot.load_extension("cogs") + bot.load_extension("cogs") -def _run_tex_bot() -> NoReturn: - tex_bot.run(settings["DISCORD_BOT_TOKEN"]) +# NOTE: The function name `_run_bot()` is used here for consistency. +# NOTE: `_run_tex_bot()` would be preferred but would be inconsitent with the required attribute name of Pycord's context classes +# NOTE: See https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/261 +def _run_bot() -> NoReturn: + bot.run(settings["DISCORD_BOT_TOKEN"]) - if tex_bot.EXIT_WAS_DUE_TO_KILL_COMMAND: + if bot.EXIT_WAS_DUE_TO_KILL_COMMAND: raise SystemExit(0) raise SystemExit(1) if __name__ == "__main__": - _run_tex_bot() + _run_bot() diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index 397fa0962..2d9de62fd 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -69,7 +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.tex_bot.close() + await self.bot.close() return wrapper # type: ignore[return-value] @staticmethod diff --git a/utils/tex_bot.py b/utils/tex_bot.py index 701733257..02700b270 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -92,7 +92,7 @@ def main_guild(self) -> discord.Guild: """ MAIN_GUILD_EXISTS: Final[bool] = bool( self._main_guild - and self._tex_bot_has_guild(settings["_DISCORD_MAIN_GUILD_ID"]) # noqa: COM812 + and self._check_guild_accessible(settings["_DISCORD_MAIN_GUILD_ID"]) # noqa: COM812 ) if not MAIN_GUILD_EXISTS: raise GuildDoesNotExistError(guild_id=settings["_DISCORD_MAIN_GUILD_ID"]) @@ -384,7 +384,7 @@ def group_moderation_contact(self) -> str: else "our community moderators" ) - def _tex_bot_has_guild(self, guild_id: int) -> bool: + def _check_guild_accessible(self, guild_id: int) -> bool: return bool(discord.utils.get(self.guilds, id=guild_id)) def _guild_has_role(self, role: discord.Role) -> bool: diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 1b26397a5..a66cdef33 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -63,13 +63,16 @@ class TeXBotBaseCog(Cog): } @override - def __init__(self, tex_bot: TeXBot) -> None: + def __init__(self, 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 + # NOTE: The attribute/variable name `bot` is used here for consistency. + # NOTE: `tex_bot` would be preferred but would be inconsitent with the required attribute name of Pycord's context classes + # NOTE: See https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/261 + self.bot: TeXBot = 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 """ @@ -87,7 +90,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, error_code: st ) await self.send_error( - self.tex_bot, + self.bot, ctx.interaction, interaction_name=COMMAND_NAME, error_code=error_code, @@ -96,7 +99,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, error_code: st ) @classmethod - 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 + 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 """ Construct & format an error message from the given details. @@ -109,7 +112,7 @@ async def send_error(cls, tex_bot: TeXBot, interaction: discord.Interaction, int committee_mention: str = "committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_mention = (await tex_bot.committee_role).mention + committee_mention = (await bot.committee_role).mention construct_error_message = ( f"**Contact a {committee_mention} member, referencing error code: " From 14c68cbd5ae7216a12f918f1ea0e8e11a78349bb Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 25 Jul 2024 19:57:18 +0100 Subject: [PATCH 123/128] Rename `tex_bot` to `bot` See #261 for more details --- cogs/annual_handover_and_reset.py | 10 +++--- cogs/archive.py | 14 ++++----- cogs/change_config.py | 24 +++++++-------- cogs/command_error.py | 4 +-- cogs/edit_message.py | 2 +- cogs/get_token_authorisation.py | 2 +- cogs/induct.py | 34 ++++++++++---------- cogs/kill.py | 4 +-- cogs/make_applicant.py | 4 +-- cogs/make_member.py | 14 ++++----- cogs/remind_me.py | 4 +-- cogs/send_get_roles_reminders.py | 6 ++-- cogs/send_introduction_reminders.py | 16 +++++----- cogs/startup.py | 20 ++++++------ cogs/stats.py | 16 +++++----- cogs/strike.py | 32 +++++++++---------- cogs/write_roles.py | 4 +-- utils/command_checks.py | 4 +-- utils/tex_bot_base_cog.py | 18 +++++------ utils/tex_bot_contexts.py | 48 +++++------------------------ 20 files changed, 123 insertions(+), 157 deletions(-) diff --git a/cogs/annual_handover_and_reset.py b/cogs/annual_handover_and_reset.py index 1422dd1f8..71ce01ee7 100644 --- a/cogs/annual_handover_and_reset.py +++ b/cogs/annual_handover_and_reset.py @@ -45,9 +45,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.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 + 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 initial_response: discord.Interaction | discord.WebhookMessage = await ctx.respond( content=":hourglass: Running handover procedures... :hourglass:", @@ -181,8 +181,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.tex_bot.main_guild - member_role: discord.Role = await self.tex_bot.member_role + main_guild: discord.Guild = self.bot.main_guild + member_role: discord.Role = await self.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 36addf71e..ff234dcae 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -160,13 +160,13 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> unless they 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.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() + 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.fullmatch(r"\A\d{17,20}\Z", str_category_id): await self.command_send_error( diff --git a/cogs/change_config.py b/cogs/change_config.py index 9aad8bb54..db0cd2e3f 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -216,7 +216,7 @@ async def check_config_file_changed(self) -> None: @check_config_file_changed.before_loop async def before_tasks(self) -> None: """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" - await self.tex_bot.wait_until_ready() + await self.bot.wait_until_ready() class ConfigChangeCommandsCog(TeXBotBaseCog): @@ -246,7 +246,7 @@ async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> Set return set() try: - if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -260,7 +260,7 @@ async def autocomplete_get_unsetable_settings_names(ctx: TeXBotAutocompleteConte return set() try: - if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -282,7 +282,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext return set() try: - if not await ctx.tex_bot.check_user_has_committee_role(ctx.interaction.user): # type: ignore[arg-type] + if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user): # type: ignore[arg-type] return set() except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -478,7 +478,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext for domain, path in itertools.product( ("github", "raw.githubusercontent"), - (f"{urllib.parse.quote(ctx.tex_bot.group_short_name)}/", ""), + (f"{urllib.parse.quote(ctx.bot.group_short_name)}/", ""), ) } | { f"https://{subdomain}dropbox{domain_suffix}.com/{path}" @@ -493,7 +493,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext return {"https://"} try: - main_guild: discord.Guild = ctx.tex_bot.main_guild + main_guild: discord.Guild = ctx.bot.main_guild except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): return set() @@ -517,7 +517,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext } try: - interaction_member: discord.Member = await ctx.tex_bot.get_main_guild_member( + interaction_member: discord.Member = await ctx.bot.get_main_guild_member( ctx.interaction.user, # type: ignore[arg-type] ) except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError): @@ -632,13 +632,13 @@ async def help_config_setting(self, ctx: TeXBotApplicationContext, config_settin f"{ config_setting_help.description.replace( "**`@TeX-Bot`**", - self.tex_bot.user.mention if self.tex_bot.user else "**`@TeX-Bot`**", + self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", ).replace( "TeX-Bot", - self.tex_bot.user.mention if self.tex_bot.user else "**`@TeX-Bot`**", + self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", ).replace( "the bot", - self.tex_bot.user.mention if self.tex_bot.user else "**`@TeX-Bot`**", + self.bot.user.mention if self.bot.user else "**`@TeX-Bot`**", ) }\n\n" f"{ @@ -711,14 +711,14 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n committee_role: discord.Role | None = None with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_role = await self.tex_bot.committee_role + committee_role = await self.bot.committee_role confirmation_message: discord.Message = ( response if isinstance(response, discord.Message) else await response.original_response() ) - button_interaction: discord.Interaction = await self.tex_bot.wait_for( + button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component diff --git a/cogs/command_error.py b/cogs/command_error.py index 75679daf9..71ceb1ef2 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -90,7 +90,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.tex_bot.committee_role).mention + committee_role_mention = (await self.bot.committee_role).mention message = f"Only {committee_role_mention} members can run this command." await self.command_send_error( @@ -134,4 +134,4 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro or type(error.original) is Exception ) if TEX_BOT_NEEDS_CLOSING: - await self.tex_bot.close() + await self.bot.close() diff --git a/cogs/edit_message.py b/cogs/edit_message.py index e81946e73..f6139efb0 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -83,7 +83,7 @@ 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.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild if not re.fullmatch(r"\A\d{17,20}\Z", str_channel_id): await self.command_send_error( diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index cd3fdff42..44f0cb570 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -111,7 +111,7 @@ async def get_token_authorisation(self, ctx: TeXBotApplicationContext) -> None: # noinspection PyUnusedLocal guest_role: discord.Role | None = None with contextlib.suppress(GuestRoleDoesNotExistError): - guest_role = await ctx.tex_bot.guest_role + guest_role = await ctx.bot.guest_role await ctx.respond( f"Admin token has access to the following MSL Organisations as " diff --git a/cogs/induct.py b/cogs/induct.py index 6fd56b483..350b98d76 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -61,7 +61,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) return try: - guest_role: discord.Role = await self.tex_bot.guest_role + guest_role: discord.Role = await self.bot.guest_role except GuestRoleDoesNotExistError: return @@ -89,21 +89,21 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) # noinspection PyUnusedLocal rules_channel_mention: str = "**`#welcome`**" with contextlib.suppress(RulesChannelDoesNotExistError): - rules_channel_mention = (await self.tex_bot.rules_channel).mention + rules_channel_mention = (await self.bot.rules_channel).mention # noinspection PyUnusedLocal roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.tex_bot.roles_channel).mention + roles_channel_mention = (await self.bot.roles_channel).mention user_type: Literal["guest", "member"] = "guest" with contextlib.suppress(MemberRoleDoesNotExistError): - if await self.tex_bot.member_role in after.roles: + if await self.bot.member_role in after.roles: user_type = "member" try: await after.send( - f"**Congrats on joining the {self.tex_bot.group_short_name} Discord server " + f"**Congrats on joining the {self.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" @@ -162,7 +162,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.tex_bot.committee_role).mention + committee_role_mention: str = (await self.bot.committee_role).mention except CommitteeRoleDoesNotExistError: return await self.get_random_welcome_message(induction_member) else: @@ -183,7 +183,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.tex_bot.group_short_name, + self.bot.group_short_name, ) return random_welcome_message.strip() @@ -221,12 +221,12 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb return if not silent: - general_channel: discord.TextChannel = await self.tex_bot.general_channel + general_channel: discord.TextChannel = await self.bot.general_channel # noinspection PyUnusedLocal roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.tex_bot.roles_channel).mention + roles_channel_mention = (await self.bot.roles_channel).mention message_already_sent: bool = False message: discord.Message @@ -253,7 +253,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb # noinspection PyUnusedLocal applicant_role: discord.Role | None = None with contextlib.suppress(ApplicantRoleDoesNotExistError): - applicant_role = await ctx.tex_bot.applicant_role + applicant_role = await ctx.bot.applicant_role if applicant_role and applicant_role in induction_member.roles: await induction_member.remove_roles( @@ -261,7 +261,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.tex_bot.get_emoji(743218410409820213) + tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") @@ -351,7 +351,7 @@ async def induct(self, ctx: TeXBotApplicationContext, str_induct_member_id: str, """ member_id_not_integer_error: ValueError try: - induct_member: discord.Member = await self.tex_bot.get_main_guild_member( + induct_member: discord.Member = await self.bot.get_main_guild_member( str_induct_member_id, ) except ValueError as member_id_not_integer_error: @@ -405,7 +405,7 @@ async def non_silent_message_induct(self, ctx: TeXBotApplicationContext, message by giving them the "Guest" role. """ try: - member: discord.Member = await self.tex_bot.get_main_guild_member( + member: discord.Member = await self.bot.get_main_guild_member( str(message.author.id), ) except ValueError: @@ -434,7 +434,7 @@ async def silent_message_induct(self, ctx: TeXBotApplicationContext, message: di by giving them the "Guest" role, only without broadcasting a welcome message. """ try: - member: discord.Member = await self.tex_bot.get_main_guild_member( + member: discord.Member = await self.bot.get_main_guild_member( str(message.author.id), ) except ValueError: @@ -470,9 +470,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.tex_bot.main_guild - member_role: discord.Role = await self.tex_bot.member_role - guest_role: discord.Role = await self.tex_bot.guest_role + 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 await ctx.defer(ephemeral=True) diff --git a/cogs/kill.py b/cogs/kill.py index 6fb5001d4..da91ce7de 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -59,7 +59,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: """ committee_role: discord.Role | None = None with contextlib.suppress(CommitteeRoleDoesNotExistError): - committee_role = await self.tex_bot.committee_role + committee_role = await self.bot.committee_role response: discord.Message | discord.Interaction = await ctx.respond( content=( @@ -78,7 +78,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: else await response.original_response() ) - button_interaction: discord.Interaction = await self.tex_bot.wait_for( + button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index c476547cc..03c51876b 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -146,7 +146,7 @@ async def make_applicant(self, ctx: TeXBotApplicationContext, str_applicant_memb """ member_id_not_integer_error: ValueError try: - applicant_member: discord.Member = await self.tex_bot.get_main_guild_member( + applicant_member: discord.Member = await self.bot.get_main_guild_member( str_applicant_member_id, ) except ValueError as member_id_not_integer_error: @@ -184,7 +184,7 @@ async def message_make_applicant(self, ctx: TeXBotApplicationContext, message: d "Applicant" role and removes the "Guest" role if they have it. """ try: - member: discord.Member = await self.tex_bot.get_main_guild_member( + member: discord.Member = await self.bot.get_main_guild_member( str(message.author.id), ) except ValueError: diff --git a/cogs/make_member.py b/cogs/make_member.py index 98927e9ea..c6e77b4a7 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -133,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.tex_bot.group_member_id_type, + self.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.tex_bot.committee_role).mention + committee_mention = (await self.bot.committee_role).mention await ctx.respond( ( @@ -213,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.tex_bot.group_full_name} " + f"You must be a member of {self.bot.group_full_name} " "to use this command.\n" f"The provided {_GROUP_MEMBER_ID_ARGUMENT_NAME} must match " - f"the {self.tex_bot.group_member_id_type} ID " - f"that you purchased your {self.tex_bot.group_short_name} membership with." + f"the {self.bot.group_member_id_type} ID " + f"that you purchased your {self.bot.group_short_name} membership with." ), ) return @@ -246,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.tex_bot.guest_role + guest_role: discord.Role = await self.bot.guest_role except GuestRoleDoesNotExistError: logger.warning( "\"/makemember\" command used but the \"Guest\" role does not exist. " @@ -263,7 +263,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) # noinspection PyUnusedLocal applicant_role: discord.Role | None = None with contextlib.suppress(ApplicantRoleDoesNotExistError): - applicant_role = await ctx.tex_bot.applicant_role + applicant_role = await ctx.bot.applicant_role if applicant_role and applicant_role in interaction_member.roles: await interaction_member.remove_roles( diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 5cda43eb9..7a660406f 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -304,7 +304,7 @@ async def clear_reminders_backlog(self) -> None: ), _reminder=reminder, ), - self.tex_bot.users, + self.bot.users, ) if not user: @@ -348,4 +348,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.tex_bot.wait_until_ready() + await self.bot.wait_until_ready() diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 906490aed..d0379521f 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -75,7 +75,7 @@ async def send_get_roles_reminders(self) -> None: # noinspection PyUnusedLocal roles_channel_mention: str = "**`#roles`**" with contextlib.suppress(RolesChannelDoesNotExistError): - roles_channel_mention = (await self.tex_bot.roles_channel).mention + roles_channel_mention = (await self.bot.roles_channel).mention # noinspection SpellCheckingInspection OPT_IN_ROLE_NAMES: Final[frozenset[str]] = frozenset( @@ -168,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.tex_bot.group_short_name} Discord server " + f"on the {self.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 " @@ -185,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.tex_bot.wait_until_ready() + await self.bot.wait_until_ready() diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 5ae5e2360..be9f7b595 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -60,8 +60,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.tex_bot.add_view( - self.OptOutIntroductionRemindersView(self.tex_bot), + self.bot.add_view( + self.OptOutIntroductionRemindersView(self.bot), ) @classmethod @@ -157,13 +157,13 @@ async def send_introduction_reminders(self) -> None: await member.send( content=( "Hey! It seems like you joined " - f"the {self.tex_bot.group_short_name} Discord server " + f"the {self.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.tex_bot) + self.OptOutIntroductionRemindersView(self.bot) if settings["SEND_INTRODUCTION_REMINDERS_ENABLED"] == "interval" else None # type: ignore[arg-type] ), @@ -208,7 +208,7 @@ async def send_error(self, interaction: discord.Interaction, error_code: str | N to the given interaction. """ await TeXBotBaseCog.send_error( - self.tex_bot, + self.bot, interaction, interaction_name="opt_out_introduction_reminders", error_code=error_code, @@ -257,7 +257,7 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B return try: - interaction_member: discord.Member = await self.tex_bot.get_main_guild_member( + interaction_member: discord.Member = await self.bot.get_main_guild_member( interaction.user, ) except DiscordMemberNotInMainGuildError: @@ -265,7 +265,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.tex_bot.group_short_name} Discord server " + f"of the {self.bot.group_short_name} Discord server " f"""to opt{ "-out of" if BUTTON_WILL_MAKE_OPT_OUT else " back in to" } introduction reminders.""" @@ -323,4 +323,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.tex_bot.wait_until_ready() + await self.bot.wait_until_ready() diff --git a/cogs/startup.py b/cogs/startup.py index ecb1c7a34..170d50165 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -59,14 +59,14 @@ def _setup_discord_log_channel(self) -> None: existing_discord_logging_handler.name if existing_discord_logging_handler.name != DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME # noqa: E501 else ( - self.tex_bot.user.name - if self.tex_bot.user + self.bot.user.name + if self.bot.user else DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME) ), settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"], avatar_url=( - self.tex_bot.user.avatar.url - if self.tex_bot.user and self.tex_bot.user.avatar + self.bot.user.avatar.url + if self.bot.user and self.bot.user.avatar else None ), ) @@ -89,14 +89,14 @@ def _setup_discord_log_channel(self) -> None: async def _initialise_main_guild(self) -> None: try: - main_guild: discord.Guild | None = self.tex_bot.main_guild + main_guild: discord.Guild | None = self.bot.main_guild except GuildDoesNotExistError: main_guild = self.bot.get_guild(settings["_DISCORD_MAIN_GUILD_ID"]) if main_guild: - self.tex_bot.set_main_guild(main_guild) + self.bot.set_main_guild(main_guild) if not main_guild: - if self.tex_bot.application_id: + if self.bot.application_id: logger.info( "Invite URL: %s", utils.generate_invite_url( @@ -147,7 +147,7 @@ async def _check_strike_performed_manually_warning_location_exists(self) -> None await self.bot.close() async def _check_all_shortcut_accessors(self) -> None: - main_guild: discord.Guild = self.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild if not discord.utils.get(main_guild.roles, name="Committee"): logger.warning(CommitteeRoleDoesNotExistError()) @@ -184,10 +184,10 @@ async def on_ready(self) -> None: logger.debug( "Invite URL: %s", utils.generate_invite_url( - self.tex_bot.application_id, + self.bot.application_id, settings["_DISCORD_MAIN_GUILD_ID"]), ) await self._check_all_shortcut_accessors() - logger.info("Ready! Logged in as %s", self.tex_bot.user) + logger.info("Ready! Logged in as %s", self.bot.user) diff --git a/cogs/stats.py b/cogs/stats.py index 6928c58f5..7067c7f4e 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -391,7 +391,7 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: filename="roles_server_stats.png", description=( "Bar chart of the number of messages sent by different roles " - f"in the {self.tex_bot.group_short_name} Discord server." + f"in the {self.bot.group_short_name} Discord server." ), extra_text=( "Messages sent by members with multiple roles are counted once " @@ -412,12 +412,12 @@ async def server_stats(self, ctx: TeXBotApplicationContext) -> None: ), title=( "Most Active Channels " - f"in the {self.tex_bot.group_short_name} Discord Server" + f"in the {self.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.tex_bot.group_short_name} Discord server." + f"in the {self.bot.group_short_name} Discord server." ), ), ], @@ -500,7 +500,7 @@ async def user_stats(self, ctx: TeXBotApplicationContext) -> None: ), title=( "Your Most Active Channels " - f"in the {self.tex_bot.group_short_name} Discord Server" + f"in the {self.bot.group_short_name} Discord Server" ), filename=f"{ctx.user}_stats.png", description=( @@ -568,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.tex_bot.group_short_name} Discord Server" + f"the {self.bot.group_short_name} Discord Server" ), title=( "Most Common Roles that Members had when they left " - f"the {self.tex_bot.group_short_name} Discord Server" + f"the {self.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.tex_bot.group_short_name} Discord server." + f"that have left the {self.bot.group_short_name} Discord server." ), extra_text=( "Members that left with multiple roles " @@ -591,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.tex_bot.main_guild or member.bot: + if member.guild != self.bot.main_guild or member.bot: return await LeftDiscordMember.objects.acreate( diff --git a/cogs/strike.py b/cogs/strike.py index 48167eeac..f5b91ed9e 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -239,13 +239,13 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me # noinspection PyUnusedLocal rules_channel_mention: str = "**`#welcome`**" with contextlib.suppress(RulesChannelDoesNotExistError): - rules_channel_mention = (await self.tex_bot.rules_channel).mention + rules_channel_mention = (await self.bot.rules_channel).mention includes_ban_message: str = ( ( "\nBecause you now have been given 3 strikes, you have been banned from " - f"the {self.tex_bot.group_short_name} Discord server " - f"and we have contacted {self.tex_bot.group_moderation_contact} for " + f"the {self.bot.group_short_name} Discord server " + f"and we have contacted {self.bot.group_moderation_contact} for " "further action & advice." ) if member_strikes.strikes >= 3 @@ -260,7 +260,7 @@ 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.tex_bot.group_short_name} Discord server's rules.\n" + f"the {self.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. " @@ -279,7 +279,7 @@ async def _confirm_perform_moderation_action(self, message_sender_component: Mes view=ConfirmStrikeMemberView(), ) - button_interaction: discord.Interaction = await self.tex_bot.wait_for( + button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( interaction.type == discord.InteractionType.component @@ -441,14 +441,14 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me if user.bot: fetch_log_channel_error: RuntimeError try: - return await self.tex_bot.fetch_log_channel() + return await self.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.tex_bot.get_user(user.id) + self.bot.get_user(user.id) if isinstance(user, discord.Member) else user ) @@ -482,8 +482,8 @@ async def get_confirmation_message_channel(self, user: discord.User | discord.Me @capture_strike_tracking_error async def _confirm_manual_add_strike(self, strike_user: discord.User | discord.Member, action: KnownModerationActions) -> 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.tex_bot.main_guild - committee_role: discord.Role = await self.tex_bot.committee_role + main_guild: discord.Guild = self.bot.main_guild + committee_role: discord.Role = await self.bot.committee_role try: # noinspection PyTypeChecker @@ -513,7 +513,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.tex_bot.user: + if applied_action_user == self.bot.user: return fetch_log_channel_error: RuntimeError @@ -521,7 +521,7 @@ 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.tex_bot.fetch_log_channel() + else await self.bot.fetch_log_channel() ) except RuntimeError as fetch_log_channel_error: raise StrikeTrackingError( @@ -718,7 +718,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.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: return @@ -747,10 +747,10 @@ 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.tex_bot.main_guild + main_guild: discord.Guild = self.bot.main_guild MEMBER_REMOVED_BECAUSE_OF_MANUALLY_APPLIED_KICK: Final[bool] = bool( - member.guild == self.tex_bot.main_guild + member.guild == self.bot.main_guild and not member.bot and not await asyncany( ban.user == member async for ban in main_guild.bans() @@ -769,7 +769,7 @@ 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.tex_bot.main_guild or user.bot: + if guild != self.bot.main_guild or user.bot: return await self._confirm_manual_add_strike( @@ -834,7 +834,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.tex_bot.get_main_guild_member( + strike_member: discord.Member = await self.bot.get_main_guild_member( 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 6d7127ee6..700cc062a 100644 --- a/cogs/write_roles.py +++ b/cogs/write_roles.py @@ -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.tex_bot.roles_channel + roles_channel: discord.TextChannel = await self.bot.roles_channel roles_message: str for roles_message in messages["OPT_IN_ROLES_SELECTORS"]: await roles_channel.send( - roles_message.replace("", self.tex_bot.group_short_name), + roles_message.replace("", self.bot.group_short_name), ) await ctx.respond("All messages sent successfully.", ephemeral=True) diff --git a/utils/command_checks.py b/utils/command_checks.py index fc5bba769..12f08e05f 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.tex_bot.get_main_guild_member(ctx.user) + await ctx.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.tex_bot.check_user_has_committee_role(ctx.user) + return await ctx.bot.check_user_has_committee_role(ctx.user) check_interaction_user_has_committee_role: Callable[[T], T] """ diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 4558744ca..3d3396bb9 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -85,7 +85,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, *, error_code: and the bot will shortly close. """ await self._respond_with_error( - self.tex_bot, + self.bot, responder=( responder_component or SenderResponseComponent(ctx.interaction, ephemeral=True) ), @@ -103,7 +103,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, *, error_code: ) @classmethod - 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, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 + 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, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 """ Construct & format an error message from the given details. @@ -112,7 +112,7 @@ async def send_error(cls, tex_bot: TeXBot, interaction: discord.Interaction, *, and the bot will shortly close. """ await cls._respond_with_error( - bot=tex_bot, + bot=bot, responder=SenderResponseComponent(interaction, ephemeral=True), interaction_name=interaction_name, error_code=error_code, @@ -122,7 +122,7 @@ async def send_error(cls, tex_bot: TeXBot, interaction: discord.Interaction, *, ) @classmethod - async def _respond_with_error(cls, tex_bot: TeXBot, responder: GenericResponderComponent, *, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 + async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderComponent, *, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 construct_error_message: str = ":warning:" if is_fatal: @@ -130,7 +130,7 @@ async def _respond_with_error(cls, tex_bot: TeXBot, responder: GenericResponderC fatal_committee_mention: str = "committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - fatal_committee_mention = (await tex_bot.committee_role).mention + fatal_committee_mention = (await bot.committee_role).mention construct_error_message += ( "A fatal error occurred, " @@ -148,7 +148,7 @@ async def _respond_with_error(cls, tex_bot: TeXBot, responder: GenericResponderC non_fatal_committee_mention: str = "committee" with contextlib.suppress(CommitteeRoleDoesNotExistError): - non_fatal_committee_mention = (await tex_bot.committee_role).mention + non_fatal_committee_mention = (await bot.committee_role).mention construct_error_message = ( f"**Contact a {non_fatal_committee_mention} member, " @@ -214,14 +214,14 @@ async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[ return set() try: - main_guild: discord.Guild = ctx.tex_bot.main_guild + main_guild: discord.Guild = ctx.bot.main_guild # noinspection PyUnusedLocal - channel_permissions_limiter: MentionableMember = await ctx.tex_bot.guest_role + channel_permissions_limiter: MentionableMember = await ctx.bot.guest_role except BaseDoesNotExistError: return set() with contextlib.suppress(DiscordMemberNotInMainGuildError): - channel_permissions_limiter = await ctx.tex_bot.get_main_guild_member( + channel_permissions_limiter = await ctx.bot.get_main_guild_member( ctx.interaction.user, ) diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 62f3b729b..19dee3450 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. +Type-hinting classes that override Pycord's Context classes. -These custom, overridden classes contain a reference to the custom bot class TeXBot, +These custom overriden classes contain a reference to the custom bot class TeXBot, rather than Pycord's default Bot class. """ @@ -10,64 +10,30 @@ __all__: Sequence[str] = ("TeXBotAutocompleteContext", "TeXBotApplicationContext") -from typing import override - import discord -from .tex_bot import TeXBot +from utils.tex_bot import TeXBot class TeXBotAutocompleteContext(discord.AutocompleteContext): """ Type-hinting class overriding AutocompleteContext's reference to the Bot class. - Pycord's default AutocompleteContext references Pycord's standard Bot class, + Pycord's default AutocompleteContext references the standard discord.Bot class, but cogs require a reference to the TeXBot class, so this AutocompleteContext subclass should be used in cogs instead. """ - @override - def __init__(self, tex_bot: TeXBot, interaction: discord.Interaction) -> None: - self._tex_bot: TeXBot = tex_bot - - super().__init__(tex_bot, interaction) - - @property - def tex_bot(self) -> TeXBot: - return self._tex_bot - - @property - def bot(self) -> discord.Bot: - raise DeprecationWarning - - @bot.setter - def bot(self, __value: discord.Bot, /) -> None: - raise DeprecationWarning + bot: TeXBot class TeXBotApplicationContext(discord.ApplicationContext): """ Type-hinting class overriding ApplicationContext's reference to the Bot class. - Pycord's default ApplicationContext references Pycord's standard Bot class, + Pycord's default ApplicationContext references the standard discord.Bot class, but cogs require a reference to the TeXBot class, so this ApplicationContext subclass should be used in cogs instead. """ - @override - def __init__(self, tex_bot: TeXBot, interaction: discord.Interaction) -> None: - self._tex_bot: TeXBot = tex_bot - - super().__init__(tex_bot, interaction) - - @property - def tex_bot(self) -> TeXBot: - return self._tex_bot - - @property - def bot(self) -> discord.Bot: - raise DeprecationWarning - - @bot.setter - def bot(self, __value: discord.Bot, /) -> None: - raise DeprecationWarning + bot: TeXBot From cb8083d5163f128b5274c8b52d2a84358e2a9865 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Thu, 25 Jul 2024 20:01:34 +0100 Subject: [PATCH 124/128] Update dependencies --- poetry.lock | 150 ++++++++++++++++++++++++++-------------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/poetry.lock b/poetry.lock index 47b513593..725e2f861 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1214,56 +1214,56 @@ files = [ [[package]] name = "numpy" -version = "2.0.0" +version = "2.0.1" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {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"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, + {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, + {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, + {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, + {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, + {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, + {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, + {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, + {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, + {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, ] [[package]] @@ -1485,20 +1485,20 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -1634,29 +1634,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.5.1" +version = "0.5.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {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"}, + {file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"}, + {file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"}, + {file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"}, + {file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"}, + {file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"}, + {file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"}, + {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"}, ] [[package]] @@ -1720,13 +1720,13 @@ files = [ [[package]] name = "sqlparse" -version = "0.5.0" +version = "0.5.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, - {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, + {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, + {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, ] [package.extras] @@ -1807,13 +1807,13 @@ files = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20240311" +version = "6.0.12.20240724" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" files = [ - {file = "types-PyYAML-6.0.12.20240311.tar.gz", hash = "sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342"}, - {file = "types_PyYAML-6.0.12.20240311-py3-none-any.whl", hash = "sha256:b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6"}, + {file = "types-PyYAML-6.0.12.20240724.tar.gz", hash = "sha256:cf7b31ae67e0c5b2919c703d2affc415485099d3fe6666a6912f040fd05cb67f"}, + {file = "types_PyYAML-6.0.12.20240724-py3-none-any.whl", hash = "sha256:e5becec598f3aa3a2ddf671de4a75fa1c6856fbf73b2840286c9d50fae2d5d48"}, ] [[package]] From 6cf83bf468cd1b9b6faacfc39aea7db8b675599d Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 29 Jul 2024 15:26:30 +0100 Subject: [PATCH 125/128] Fix formatting errors --- cogs/archive.py | 16 +-- cogs/change_config.py | 103 ++++++++-------- cogs/command_error.py | 4 +- cogs/get_token_authorisation.py | 3 +- cogs/induct.py | 8 +- cogs/send_introduction_reminders.py | 3 +- cogs/startup.py | 9 +- cogs/strike.py | 38 +++--- config/_messages/__init__.py | 11 +- config/_settings/__init__.py | 111 ++++++------------ .../_yaml/custom_scalar_validators.py | 5 +- config/constants.py | 17 ++- db/_settings.py | 3 +- db/core/models/__init__.py | 4 +- db/core/models/managers.py | 5 +- db/core/models/utils.py | 3 +- exceptions/does_not_exist.py | 4 +- main.py | 1 + tests/test_utils.py | 9 +- utils/__init__.py | 4 +- utils/tex_bot_base_cog.py | 19 ++- 21 files changed, 155 insertions(+), 225 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index ff234dcae..25bc5152a 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -58,18 +58,18 @@ async def autocomplete_get_categories(ctx: TeXBotAutocompleteContext) -> Set[dis return { discord.OptionChoice(name=category.name, value=str(category.id)) - for category - in main_guild.categories + for category in main_guild.categories if category.permissions_for(interaction_user).is_superset( discord.Permissions(send_messages=True, view_channel=True), ) } async def _set_permissions(self, channel: AllChannelTypes, ctx: TeXBotApplicationContext, interaction_member: discord.Member, *, committee_role: discord.Role, guest_role: discord.Role, member_role: discord.Role, archivist_role: discord.Role, everyone_role: discord.Role) -> None: # noqa: PLR0913,E501 - CHANNEL_NEEDS_COMMITTEE_ARCHIVING: Final[bool] = ( + CHANNEL_NEEDS_COMMITTEE_ARCHIVING: Final[bool] = bool( channel.permissions_for(committee_role).is_superset( discord.Permissions(view_channel=True), - ) and not channel.permissions_for(guest_role).is_superset( + ) + and not channel.permissions_for(guest_role).is_superset( discord.Permissions(view_channel=True), ) ) @@ -96,10 +96,10 @@ async def _set_permissions(self, channel: AllChannelTypes, ctx: TeXBotApplicatio ) return - CHANNEL_NEEDS_NORMAL_ARCHIVING: Final[bool] = ( - channel.permissions_for(guest_role).is_superset( - discord.Permissions(view_channel=True), - ) + CHANNEL_NEEDS_NORMAL_ARCHIVING: Final[bool] = channel.permissions_for( + guest_role + ).is_superset( + discord.Permissions(view_channel=True), ) if CHANNEL_NEEDS_NORMAL_ARCHIVING: await channel.set_permissions( diff --git a/cogs/change_config.py b/cogs/change_config.py index db0cd2e3f..4ee94573d 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -229,15 +229,13 @@ class ConfigChangeCommandsCog(TeXBotBaseCog): @classmethod def get_formatted_change_delay_message(cls) -> str: - return ( - f"Changes could take up to { - ( - str(int(settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1)) - if (settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1) % 1 == 0 - else f"{settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1:.2f}" - ) - } seconds to take effect." - ) + return f"Changes could take up to { + ( + str(int(settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1)) + if (settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1) % 1 == 0 + else f"{settings["CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS"] * 2.1:.2f}" + ) + } seconds to take effect." @staticmethod async def autocomplete_get_settings_names(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 @@ -267,8 +265,7 @@ async def autocomplete_get_unsetable_settings_names(ctx: TeXBotAutocompleteConte return { setting_name - for setting_name, setting_help - in config.CONFIG_SETTINGS_HELPS.items() + for setting_name, setting_help in config.CONFIG_SETTINGS_HELPS.items() if setting_help.default is not None } @@ -373,18 +370,16 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext if "send-introduction-reminders:enable" in setting_name: return { str(flag_value).lower() - for flag_value - in getattr(SendIntroductionRemindersFlagType, "__args__") # noqa: B009 + for flag_value in getattr(SendIntroductionRemindersFlagType, "__args__") # noqa: B009 } if "send-get-roles-reminders:enable" in setting_name: return {"true", "false"} - SETTING_NAME_IS_TIMEDELTA: Final[bool] = ( + SETTING_NAME_IS_TIMEDELTA: Final[bool] = bool( any( part in setting_name - for part - in ( + for part in ( ":timeout-duration:", ":delay:", ":interval:", @@ -448,10 +443,8 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext in selected_timedelta_scales ), ) - for _ - in range(4) - for selected_timedelta_scales - in itertools.product( + for _ in range(4) + for selected_timedelta_scales in itertools.product( *(("", timedelta_scale) for timedelta_scale in timedelta_scales), ) if any(selected_timedelta_scales) @@ -467,28 +460,30 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext if "document" in setting_name: # noinspection SpellCheckingInspection - return { - "https://", - "https://drive.google.com/file/d/", - "https://docs.google.com/document/d/", - "https://onedrive.live.com/edit.aspx?resid=", - "https://1drv.ms/p/", - } | { - f"https://{domain}.com/{path}" - for domain, path - in itertools.product( - ("github", "raw.githubusercontent"), - (f"{urllib.parse.quote(ctx.bot.group_short_name)}/", ""), - ) - } | { - f"https://{subdomain}dropbox{domain_suffix}.com/{path}" - for subdomain, domain_suffix, path - in itertools.product( - ("dl.", ""), - ("usercontent", ""), - ("shared/", "", "s/", "scl/fi/"), - ) - } + return ( + { + "https://", + "https://drive.google.com/file/d/", + "https://docs.google.com/document/d/", + "https://onedrive.live.com/edit.aspx?resid=", + "https://1drv.ms/p/", + } + | { + f"https://{domain}.com/{path}" + for domain, path in itertools.product( + ("github", "raw.githubusercontent"), + (f"{urllib.parse.quote(ctx.bot.group_short_name)}/", ""), + ) + } + | { + f"https://{subdomain}dropbox{domain_suffix}.com/{path}" + for subdomain, domain_suffix, path in itertools.product( + ("dl.", ""), + ("usercontent", ""), + ("shared/", "", "s/", "scl/fi/"), + ) + } + ) return {"https://"} @@ -524,17 +519,13 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext return set() if ":performed-manually-warning-location" in setting_name: - return ( - {"DM"} - | { - channel.name - for channel - in main_guild.text_channels - if channel.permissions_for(interaction_member).is_superset( - discord.Permissions(send_messages=True), - ) - } - ) + return {"DM"} | { + channel.name + for channel in main_guild.text_channels + if channel.permissions_for(interaction_member).is_superset( + discord.Permissions(send_messages=True), + ) + } return set() @@ -720,13 +711,11 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n ) button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", - check=lambda interaction: ( + check=lambda interaction: bool( interaction.type == discord.InteractionType.component and interaction.message.id == confirmation_message.id and ( - (committee_role in interaction.user.roles) - if committee_role - else True + (committee_role in interaction.user.roles) if committee_role else True ) and "custom_id" in interaction.data and interaction.data["custom_id"] in { diff --git a/cogs/command_error.py b/cogs/command_error.py index a1c4074cc..b66f2a8e3 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -61,9 +61,9 @@ def _get_error_code_from_error(cls, error: discord.ApplicationCommandInvokeError @TeXBotBaseCog.listener() async def on_application_command_error(self, ctx: TeXBotApplicationContext, error: discord.ApplicationCommandError) -> None: # noqa: E501 """Log any major command errors in the logging channel & stderr.""" - IS_FATAL: Final[bool] = ( + IS_FATAL: Final[bool] = bool( isinstance(error, discord.ApplicationCommandInvokeError) - and ( + and bool( isinstance(error.original, RuntimeError | NotImplementedError) or type(error.original) is Exception ) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 9d5e59c87..31e168234 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -121,7 +121,8 @@ async def get_token_authorisation(self, ctx: TeXBotApplicationContext) -> None: organisation for organisation in organisations )}", ephemeral=bool( - (not guest_role) or ctx.channel.permissions_for(guest_role).is_superset( + (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 fe70ead47..238df0979 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -186,16 +186,13 @@ async def get_random_welcome_message(self, induction_member: discord.User | disc return random_welcome_message.strip() - 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 main_guild: discord.Guild = self.bot.main_guild guest_role: discord.Role = await self.bot.guest_role - INDUCT_AUDIT_MESSAGE: Final[str] = ( - f"{ctx.user} used TeX Bot slash-command: \"/induct\"" - ) + INDUCT_AUDIT_MESSAGE: Final[str] = f"{ctx.user} used TeX Bot slash-command: \"/induct\"" intro_channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, @@ -380,7 +377,6 @@ async def non_silent_user_induct(self, ctx: TeXBotApplicationContext, member: di """ 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 @@ -395,7 +391,6 @@ async def silent_user_induct(self, ctx: TeXBotApplicationContext, member: discor """ await self._perform_induction(ctx, member, silent=True) - @discord.message_command(name="Induct Message Author") # type: ignore[no-untyped-call, misc] @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild @@ -425,7 +420,6 @@ async def non_silent_message_induct(self, ctx: TeXBotApplicationContext, message await self._perform_induction(ctx, member, silent=False) - @discord.message_command(name="Silently Induct Message Author") # type: ignore[no-untyped-call, misc] @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 413792ec5..0e54fb4fe 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -282,8 +282,7 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B "hashed_member_id" in create_introduction_reminder_opt_out_member_error.message_dict # noqa: E501 and any( "already exists" in error - for error - in create_introduction_reminder_opt_out_member_error.message_dict[ + for error in create_introduction_reminder_opt_out_member_error.message_dict[ "hashed_member_id" ] ) diff --git a/cogs/startup.py b/cogs/startup.py index 0a55c323d..f05d2febe 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -61,7 +61,8 @@ def _setup_discord_log_channel(self) -> None: else ( self.bot.user.name if self.bot.user - else DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME) + else DEFAULT_DISCORD_LOGGING_HANDLER_DISPLAY_NAME + ) ), settings["DISCORD_LOG_CHANNEL_WEBHOOK_URL"], avatar_url=( @@ -131,8 +132,10 @@ async def _check_strike_performed_manually_warning_location_exists(self) -> None ) strike_performed_manually_warning_location_similar_to_dm: bool = ( - settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"].lower() - in ("dm", "dms") + settings["STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION"].lower() in ( + "dm", + "dms", + ) ) if strike_performed_manually_warning_location_similar_to_dm: logger.info( diff --git a/cogs/strike.py b/cogs/strike.py index 6694ec4c4..2495b6945 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -57,7 +57,7 @@ # noinspection PyTypeHints PossibleMuteModerationActions: TypeAlias = Literal[ - discord.AuditLogAction.member_update + discord.AuditLogAction.member_update | discord.AuditLogAction.auto_moderation_user_communication_disabled ] @@ -553,24 +553,22 @@ 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.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", + } + ), ) match out_of_sync_ban_button_interaction.data["custom_id"]: # type: ignore[index, typeddict-item] @@ -723,7 +721,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) async for audit_log_entry in main_guild.audit_logs(limit=5): FOUND_CORRECT_AUDIT_LOG_ENTRY: bool = ( audit_log_entry.target.id == after.id - and audit_log_entry.action == (discord.AuditLogAction.auto_moderation_user_communication_disabled) # noqa: E501 + and audit_log_entry.action == discord.AuditLogAction.auto_moderation_user_communication_disabled # noqa: E501 ) if FOUND_CORRECT_AUDIT_LOG_ENTRY: mute_action_type = audit_log_entry.action # type: ignore[assignment] diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index 32f13c16f..3bc0dba51 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -37,8 +37,8 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 raise AttributeError(MISSING_ATTRIBUTE_MESSAGE) if item not in self._messages: - INVALID_MESSAGE_ID_MESSAGE: Final[str] = ( - self.format_invalid_message_id_message(item) + INVALID_MESSAGE_ID_MESSAGE: Final[str] = self.format_invalid_message_id_message( + item, ) raise AttributeError(INVALID_MESSAGE_ID_MESSAGE) @@ -52,7 +52,7 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 except AttributeError as attribute_not_exist_error: key_error_message: str = item - ERROR_WAS_FROM_INVALID_KEY_NAME: Final[bool] = ( + ERROR_WAS_FROM_INVALID_KEY_NAME: Final[bool] = bool( self.format_invalid_message_id_message(item) in str( attribute_not_exist_error, ) @@ -83,8 +83,9 @@ async def _public_load(cls, messages_locale_code: str) -> None: # noinspection PyTypeChecker messages_locale_file_path: AsyncPath = await anext( path - async for path - in (AsyncPath(PROJECT_ROOT) / "config/_messages/locales/").iterdir() + async for path in ( + AsyncPath(PROJECT_ROOT) / "config/_messages/locales/" + ).iterdir() if path.stem == messages_locale_code ) except StopIteration: diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index 036f378d6..b9c5c8146 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -71,15 +71,13 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 raise RuntimeError(YAML_NOT_LOADED_MESSAGE) if item not in self._settings: - INVALID_SETTINGS_KEY_MESSAGE: Final[str] = ( - self._get_invalid_settings_key_message(item) + INVALID_SETTINGS_KEY_MESSAGE: Final[str] = self._get_invalid_settings_key_message( + item ) raise AttributeError(INVALID_SETTINGS_KEY_MESSAGE) ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: Final[bool] = bool( - "bot" in item.lower() - and "token" in item.lower() - and utils.is_running_in_async() # noqa: COM812 + "bot" in item.lower() and "token" in item.lower() and utils.is_running_in_async() # noqa: COM812 ) if ATTEMPTING_TO_ACCESS_BOT_TOKEN_WHEN_ALREADY_RUNNING: TEX_BOT_ALREADY_RUNNING_MESSAGE: Final[str] = ( @@ -212,8 +210,7 @@ def _reload_console_logging(cls, console_logging_settings: YAML) -> set[str]: # stream_handlers: set[logging.StreamHandler[TextIO]] = { handler - for handler - in ALL_HANDLERS + for handler in ALL_HANDLERS if ( isinstance(handler, type(console_logging_handler)) and handler.stream == console_logging_handler.stream @@ -296,8 +293,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y and cls._most_recent_yaml["logging"].get("discord-channel", None) is not None and all( value == cls._most_recent_yaml["logging"]["discord-channel"].get(key, None) - for key, value - in discord_channel_logging_settings.items() + for key, value in discord_channel_logging_settings.items() if key != "log-level" ) # noqa: COM812 ) @@ -351,9 +347,7 @@ def _reload_discord_log_channel_logging(cls, discord_channel_logging_settings: Y DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or cls._most_recent_yaml["logging"].get("discord-channel", None) is None - or discord_channel_logging_settings["log-level"] != cls._most_recent_yaml[ - "logging" - ]["discord-channel"]["log-level"] # noqa: COM812 + or discord_channel_logging_settings["log-level"] != cls._most_recent_yaml["logging"]["discord-channel"]["log-level"] # noqa: COM812, E501 ) if DISCORD_LOG_CHANNEL_LOG_LEVEL_CHANGED: changed_settings.add("logging:discord-channel:log-level") @@ -417,9 +411,7 @@ def _reload_group_full_name(cls, group_full_name: YAML | None) -> set[str]: # t return set() cls._settings["_GROUP_FULL_NAME"] = ( - group_full_name - if group_full_name is None - else group_full_name.data + group_full_name if group_full_name is None else group_full_name.data ) return {"community-group:full-name"} @@ -443,9 +435,7 @@ def _reload_group_short_name(cls, group_short_name: YAML | None) -> set[str]: # return set() cls._settings["_GROUP_SHORT_NAME"] = ( - group_short_name - if group_short_name is None - else group_short_name.data + group_short_name if group_short_name is None else group_short_name.data ) return {"community-group:short-name"} @@ -460,9 +450,10 @@ def _reload_purchase_membership_link(cls, purchase_membership_link: YAML | None) PURCHASE_MEMBERSHIP_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "PURCHASE_MEMBERSHIP_LINK" not in cls._settings - or purchase_membership_link != cls._most_recent_yaml["community-group"][ - "links" - ].get("purchase-membership", None) # noqa: COM812 + or purchase_membership_link != cls._most_recent_yaml["community-group"]["links"].get( # noqa: E501 + "purchase-membership", + None, + ) # noqa: COM812 ) if not PURCHASE_MEMBERSHIP_LINK_CHANGED: return set() @@ -485,9 +476,10 @@ def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> se MEMBERSHIP_PERKS_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "MEMBERSHIP_PERKS_LINK" not in cls._settings - or membership_perks_link != cls._most_recent_yaml["community-group"][ - "links" - ].get("membership-perks", None) # noqa: COM812 + or membership_perks_link != cls._most_recent_yaml["community-group"]["links"].get( # noqa: E501 + "membership-perks", + None, + ) # noqa: COM812 ) if not MEMBERSHIP_PERKS_LINK_CHANGED: return set() @@ -510,9 +502,7 @@ def _reload_moderation_document_link(cls, moderation_document_link: YAML) -> set MODERATION_DOCUMENT_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "MODERATION_DOCUMENT_LINK" not in cls._settings - or moderation_document_link != cls._most_recent_yaml["community-group"]["links"][ - "moderation-document" - ] # noqa: COM812 + or moderation_document_link != cls._most_recent_yaml["community-group"]["links"]["moderation-document"] # noqa: COM812, E501 ) if not MODERATION_DOCUMENT_LINK_CHANGED: return set() @@ -531,9 +521,7 @@ def _reload_members_list_url(cls, members_list_url: YAML) -> set[str]: # type: MEMBERS_LIST_URL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "MEMBERS_LIST_URL" not in cls._settings - or members_list_url != cls._most_recent_yaml["community-group"]["members-list"][ - "url" - ] # noqa: COM812 + or members_list_url != cls._most_recent_yaml["community-group"]["members-list"]["url"] # noqa: COM812, E501 ) if not MEMBERS_LIST_URL_CHANGED: return set() @@ -552,9 +540,7 @@ def _reload_members_list_auth_session_cookie(cls, members_list_auth_session_cook MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "MEMBERS_LIST_AUTH_SESSION_COOKIE" not in cls._settings - or members_list_auth_session_cookie != cls._most_recent_yaml["community-group"][ - "members-list" - ]["auth-session-cookie"] # noqa: COM812 + or members_list_auth_session_cookie != cls._most_recent_yaml["community-group"]["members-list"]["auth-session-cookie"] # noqa: COM812, E501 ) if not MEMBERS_LIST_AUTH_SESSION_COOKIE_CHANGED: return set() @@ -575,9 +561,7 @@ def _reload_members_list_id_format(cls, members_list_id_format: YAML) -> set[str MEMBERS_LIST_ID_FORMAT_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "MEMBERS_LIST_ID_FORMAT" not in cls._settings - or members_list_id_format != cls._most_recent_yaml["community-group"][ - "members-list" - ]["id-format"] # noqa: COM812 + or members_list_id_format != cls._most_recent_yaml["community-group"]["members-list"]["id-format"] # noqa: COM812, E501 ) if not MEMBERS_LIST_ID_FORMAT_CHANGED: return set() @@ -596,9 +580,7 @@ def _reload_ping_command_easter_egg_probability(cls, ping_command_easter_egg_pro PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "PING_COMMAND_EASTER_EGG_PROBABILITY" not in cls._settings - or ping_command_easter_egg_probability != cls._most_recent_yaml["commands"][ - "ping" - ]["easter-egg-probability"] # noqa: COM812 + or ping_command_easter_egg_probability != cls._most_recent_yaml["commands"]["ping"]["easter-egg-probability"] # noqa: COM812, E501 ) if not PING_COMMAND_EASTER_EGG_PROBABILITY_CHANGED: return set() @@ -619,9 +601,7 @@ def _reload_stats_command_lookback_days(cls, stats_command_lookback_days: YAML) STATS_COMMAND_LOOKBACK_DAYS_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "STATS_COMMAND_LOOKBACK_DAYS" not in cls._settings - or stats_command_lookback_days != cls._most_recent_yaml["commands"][ - "stats" - ]["lookback-days"] # noqa: COM812 + or stats_command_lookback_days != cls._most_recent_yaml["commands"]["stats"]["lookback-days"] # noqa: COM812, E501 ) if not STATS_COMMAND_LOOKBACK_DAYS_CHANGED: return set() @@ -642,9 +622,7 @@ def _reload_stats_command_displayed_roles(cls, stats_command_displayed_roles: YA STATS_COMMAND_DISPLAYED_ROLES_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "STATS_COMMAND_DISPLAYED_ROLES" not in cls._settings - or stats_command_displayed_roles != cls._most_recent_yaml["commands"][ - "stats" - ]["displayed-roles"] # noqa: COM812 + or stats_command_displayed_roles != cls._most_recent_yaml["commands"]["stats"]["displayed-roles"] # noqa: COM812, E501 ) if not STATS_COMMAND_DISPLAYED_ROLES_CHANGED: return set() @@ -663,9 +641,7 @@ def _reload_strike_command_timeout_duration(cls, strike_command_timeout_duration STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "STRIKE_COMMAND_TIMEOUT_DURATION" not in cls._settings - or strike_command_timeout_duration != cls._most_recent_yaml["commands"][ - "strike" - ]["timeout-duration"] # noqa: COM812 + or strike_command_timeout_duration != cls._most_recent_yaml["commands"]["strike"]["timeout-duration"] # noqa: COM812, E501 ) if not STRIKE_COMMAND_TIMEOUT_DURATION_CHANGED: return set() @@ -684,9 +660,7 @@ def _reload_strike_performed_manually_warning_location(cls, strike_performed_man STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION" not in cls._settings - or strike_performed_manually_warning_location != cls._most_recent_yaml["commands"][ - "strike" - ]["performed-manually-warning-location"] # noqa: COM812 + or strike_performed_manually_warning_location != cls._most_recent_yaml["commands"]["strike"]["performed-manually-warning-location"] # noqa: COM812, E501 ) if not STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_CHANGED: return set() @@ -726,9 +700,7 @@ def _reload_send_introduction_reminders_enabled(cls, send_introduction_reminders SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "SEND_INTRODUCTION_REMINDERS_ENABLED" not in cls._settings - or send_introduction_reminders_enabled != cls._most_recent_yaml["reminders"][ - "send-introduction-reminders" - ]["enabled"] # noqa: COM812 + or send_introduction_reminders_enabled != cls._most_recent_yaml["reminders"]["send-introduction-reminders"]["enabled"] # noqa: COM812, E501 ) if not SEND_INTRODUCTION_REMINDERS_ENABLED_CHANGED: return set() @@ -751,9 +723,7 @@ def _reload_send_introduction_reminders_delay(cls, send_introduction_reminders_d SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "SEND_INTRODUCTION_REMINDERS_DELAY" not in cls._settings - or send_introduction_reminders_delay != cls._most_recent_yaml["reminders"][ - "send-introduction-reminders" - ]["delay"] # noqa: COM812 + or send_introduction_reminders_delay != cls._most_recent_yaml["reminders"]["send-introduction-reminders"]["delay"] # noqa: COM812, E501 ) if not SEND_INTRODUCTION_REMINDERS_DELAY_CHANGED: return set() @@ -774,9 +744,7 @@ def _reload_send_introduction_reminders_interval(cls, send_introduction_reminder SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS" not in cls._settings - or send_introduction_reminders_interval != cls._most_recent_yaml["reminders"][ - "send-introduction-reminders" - ]["interval"] # noqa: COM812 + or send_introduction_reminders_interval != cls._most_recent_yaml["reminders"]["send-introduction-reminders"]["interval"] # noqa: COM812, E501 ) if not SEND_INTRODUCTION_REMINDERS_INTERVAL_CHANGED: return set() @@ -797,9 +765,7 @@ def _reload_send_get_roles_reminders_enabled(cls, send_get_roles_reminders_enabl SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "SEND_GET_ROLES_REMINDERS_ENABLED" not in cls._settings - or send_get_roles_reminders_enabled != cls._most_recent_yaml["reminders"][ - "send-get-roles-reminders" - ]["enabled"] # noqa: COM812 + or send_get_roles_reminders_enabled != cls._most_recent_yaml["reminders"]["send-get-roles-reminders"]["enabled"] # noqa: COM812, E501 ) if not SEND_GET_ROLES_REMINDERS_ENABLED_CHANGED: return set() @@ -823,16 +789,12 @@ def _reload_send_get_roles_reminders_delay(cls, send_get_roles_reminders_delay: SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "SEND_GET_ROLES_REMINDERS_DELAY" not in cls._settings - or send_get_roles_reminders_delay != cls._most_recent_yaml["reminders"][ - "send-get-roles-reminders" - ]["delay"] # noqa: COM812 + or send_get_roles_reminders_delay != cls._most_recent_yaml["reminders"]["send-get-roles-reminders"]["delay"] # noqa: COM812, E501 ) if not SEND_GET_ROLES_REMINDERS_DELAY_CHANGED: return set() - cls._settings["SEND_GET_ROLES_REMINDERS_DELAY"] = ( - send_get_roles_reminders_delay.data - ) + cls._settings["SEND_GET_ROLES_REMINDERS_DELAY"] = send_get_roles_reminders_delay.data return {"reminders:send-get-roles-reminders:delay"} @@ -846,9 +808,7 @@ def _reload_send_get_roles_reminders_interval(cls, send_get_roles_reminders_inte SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS" not in cls._settings - or send_get_roles_reminders_interval != cls._most_recent_yaml["reminders"][ - "send-get-roles-reminders" - ]["interval"] # noqa: COM812 + or send_get_roles_reminders_interval != cls._most_recent_yaml["reminders"]["send-get-roles-reminders"]["interval"] # noqa: COM812, E501 ) if not SEND_GET_ROLES_REMINDERS_INTERVAL_CHANGED: return set() @@ -869,9 +829,7 @@ def _reload_check_if_config_changed_interval(cls, check_if_config_changed_interv CHECK_IF_CONFIG_CHANGED_INTERVAL_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "CHECK_CONFIG_FILE_CHANGED_INTERVAL_SECONDS" not in cls._settings - or check_if_config_changed_interval != cls._most_recent_yaml[ - "check-if-config-changed-interval" - ] # noqa: COM812 + or check_if_config_changed_interval != cls._most_recent_yaml["check-if-config-changed-interval"] # noqa: COM812, E501 ) if not CHECK_IF_CONFIG_CHANGED_INTERVAL_CHANGED: return set() @@ -989,8 +947,7 @@ def _set_scalar_or_sequence_value(cls, config_setting_name: str, new_config_sett if yaml_settings_tree[config_setting_name].is_sequence(): yaml_settings_tree[config_setting_name] = [ sequence_value.strip() - for sequence_value - in new_config_setting_value.strip().split(",") + for sequence_value in new_config_setting_value.strip().split(",") ] return yaml_settings_tree diff --git a/config/_settings/_yaml/custom_scalar_validators.py b/config/_settings/_yaml/custom_scalar_validators.py index 01b639480..72477a9e8 100644 --- a/config/_settings/_yaml/custom_scalar_validators.py +++ b/config/_settings/_yaml/custom_scalar_validators.py @@ -40,10 +40,7 @@ def validate_scalar(self, chunk: YAMLChunk) -> LogLevels: # type: ignore[misc] if val not in LogLevels: chunk.expecting_but_found( - ( - "when expecting a valid log-level " - f"(one of: '{"', '".join(LogLevels)}')" - ), + "when expecting a valid log-level " f"(one of: '{"', '".join(LogLevels)}')", ) raise RuntimeError diff --git a/config/constants.py b/config/constants.py index cb8cc80d4..014a8edeb 100644 --- a/config/constants.py +++ b/config/constants.py @@ -74,14 +74,12 @@ def _selectable_required_format_message(options: Iterable[str]) -> str: def _custom_required_format_message(type_value: str, info_link: str | None = None) -> str: - return ( - f"Must be a valid { - type_value.lower().replace("discord", "Discord").replace( - "id", - "ID", - ).replace("url", "URL").replace("dm", "DM").strip(".") - }{f" (see <{info_link}>)" if info_link else ""}." - ) + return f"Must be a valid { + type_value.lower().replace("discord", "Discord").replace( + "id", + "ID", + ).replace("url", "URL").replace("dm", "DM").strip(".") + }{f" (see <{info_link}>)" if info_link else ""}." PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.resolve() @@ -375,8 +373,7 @@ def _custom_required_format_message(type_value: str, info_link: str | None = Non value_type_message=_selectable_required_format_message( ( str(flag_value).lower() - for flag_value - in getattr(SendIntroductionRemindersFlagType, "__args__") # noqa: B009 + for flag_value in getattr(SendIntroductionRemindersFlagType, "__args__") # noqa: B009 ), ), requires_restart_after_changed=True, diff --git a/db/_settings.py b/db/_settings.py index fc93787bd..4ece05e3f 100644 --- a/db/_settings.py +++ b/db/_settings.py @@ -19,8 +19,7 @@ # NOTE: settings.py is called when setting up the mypy_django_plugin & when running Pytest. When mypy/Pytest runs no config settings variables are set, so they should not be accessed IMPORTED_BY_MYPY_OR_PYTEST: Final[bool] = any( "mypy_django_plugin" in frame.filename or "pytest" in frame.filename - for frame - in inspect.stack()[1:] + for frame in inspect.stack()[1:] if not frame.filename.startswith("<") ) if IMPORTED_BY_MYPY_OR_PYTEST: diff --git a/db/core/models/__init__.py b/db/core/models/__init__.py index bf57415d4..8ec207f7c 100644 --- a/db/core/models/__init__.py +++ b/db/core/models/__init__.py @@ -226,9 +226,7 @@ class DiscordReminder(BaseDiscordMemberWrapper): _channel_type = models.IntegerField( "Discord Channel Type of the channel that the reminder needs to be sent in", choices=[ - (channel_type.value, channel_type.name) - for channel_type - in discord.ChannelType + (channel_type.value, channel_type.name) for channel_type in discord.ChannelType ], null=True, blank=True, diff --git a/db/core/models/managers.py b/db/core/models/managers.py index bcd5e8900..b62f0ce6f 100644 --- a/db/core/models/managers.py +++ b/db/core/models/managers.py @@ -22,10 +22,7 @@ T_model = TypeVar("T_model", bound=AsyncBaseModel) -Defaults: TypeAlias = ( - MutableMapping[str, object | Callable[[], object]] - | None -) +Defaults: TypeAlias = MutableMapping[str, object | Callable[[], object]] | None logger: Final[Logger] = logging.getLogger("TeX-Bot") diff --git a/db/core/models/utils.py b/db/core/models/utils.py index 1645756d5..7d2acf290 100644 --- a/db/core/models/utils.py +++ b/db/core/models/utils.py @@ -41,8 +41,7 @@ def save(self, *, force_insert: bool = False, force_update: bool = False, using: def __init__(self, *args: object, **kwargs: object) -> None: proxy_fields: dict[str, object] = { field_name: kwargs.pop(field_name) - for field_name - in set(kwargs.keys()) & self.get_proxy_field_names() + for field_name in set(kwargs.keys()) & self.get_proxy_field_names() } super().__init__(*args, **kwargs) diff --git a/exceptions/does_not_exist.py b/exceptions/does_not_exist.py index afa764512..6f215887a 100644 --- a/exceptions/does_not_exist.py +++ b/exceptions/does_not_exist.py @@ -255,13 +255,13 @@ def ERROR_CODE(cls) -> str: # noqa: N805 # noinspection PyMethodParameters @classproperty @override - def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: # noqa: N805 return frozenset({"make_applicant"}) # noinspection PyMethodParameters @classproperty @override - def ROLE_NAME(cls) -> str: # noqa: N805 + def ROLE_NAME(cls) -> str: # noqa: N805 return "Applicant" diff --git a/main.py b/main.py index 1f17075c1..3d7b83fe9 100644 --- a/main.py +++ b/main.py @@ -43,5 +43,6 @@ def _run_bot() -> NoReturn: # NOTE: See https://github.com/CSSUoB/TeX-Bot-Py-V2 raise SystemExit(bot.EXIT_REASON.value) + if __name__ == "__main__": _run_bot() diff --git a/tests/test_utils.py b/tests/test_utils.py index 7a7a1054a..5718d9543 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -81,14 +81,17 @@ class TestGenerateInviteURL: def test_url_generates() -> None: """Test that the invite URL generates successfully when valid arguments are passed.""" DISCORD_BOT_APPLICATION_ID: Final[int] = random.randint( - 10000000000000000, 99999999999999999999, + 10000000000000000, + 99999999999999999999, ) DISCORD_MAIN_GUILD_ID: Final[int] = random.randint( - 10000000000000000, 99999999999999999999, + 10000000000000000, + 99999999999999999999, ) invite_url: str = utils.generate_invite_url( - DISCORD_BOT_APPLICATION_ID, DISCORD_MAIN_GUILD_ID, + DISCORD_BOT_APPLICATION_ID, + DISCORD_MAIN_GUILD_ID, ) assert re.fullmatch( diff --git a/utils/__init__.py b/utils/__init__.py index 07fcb9291..177d0605c 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -85,6 +85,4 @@ def is_member_inducted(member: discord.Member) -> bool: 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. """ - return any( - role.name.lower().strip("@ \n\t") not in ("news",) for role in member.roles - ) + return any(role.name.lower().strip("@ \n\t") not in ("news",) for role in member.roles) diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 59578f651..487adf2f1 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -91,7 +91,8 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, *, error_code: if ( hasattr(ctx.command, "callback") and not ctx.command.callback.__name__.startswith("_") - ) else ctx.command.qualified_name + ) + else ctx.command.qualified_name ), error_code=error_code, message=message, @@ -165,15 +166,13 @@ async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderCompo construct_error_message += ":warning:" if message: - construct_error_message += ( - f"\n`{ - re.sub( - r"<([@&#]?|(@[&#])?)\d+>", - lambda match: f"`{match.group(0)!s}`", - message.strip(), - ) - }`" - ) + construct_error_message += f"\n`{ + re.sub( + r"<([@&#]?|(@[&#])?)\d+>", + lambda match: f"`{match.group(0)!s}`", + message.strip(), + ) + }`" await responder.respond(content=construct_error_message, view=None) From 18893e221d93dfd9d5e741cf6e5d1b7c04c817d0 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 29 Jul 2024 21:14:56 +0100 Subject: [PATCH 126/128] Fix linting errors --- cogs/archive.py | 4 ++-- cogs/change_config.py | 4 ++-- cogs/command_error.py | 10 +++++++--- cogs/induct.py | 6 ++++-- cogs/send_introduction_reminders.py | 10 ++++------ config/_messages/__init__.py | 6 ++---- config/_settings/__init__.py | 4 ++-- main.py | 2 +- pyproject.toml | 2 +- utils/tex_bot_base_cog.py | 4 ++-- 10 files changed, 27 insertions(+), 25 deletions(-) diff --git a/cogs/archive.py b/cogs/archive.py index 25bc5152a..655bfd7cb 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -71,7 +71,7 @@ async def _set_permissions(self, channel: AllChannelTypes, ctx: TeXBotApplicatio ) and not channel.permissions_for(guest_role).is_superset( discord.Permissions(view_channel=True), - ) + ) # noqa: COM812 ) if CHANNEL_NEEDS_COMMITTEE_ARCHIVING: await channel.set_permissions( @@ -97,7 +97,7 @@ async def _set_permissions(self, channel: AllChannelTypes, ctx: TeXBotApplicatio return CHANNEL_NEEDS_NORMAL_ARCHIVING: Final[bool] = channel.permissions_for( - guest_role + guest_role, ).is_superset( discord.Permissions(view_channel=True), ) diff --git a/cogs/change_config.py b/cogs/change_config.py index 4ee94573d..8774e9847 100644 --- a/cogs/change_config.py +++ b/cogs/change_config.py @@ -400,7 +400,7 @@ async def autocomplete_get_example_setting_values(ctx: TeXBotAutocompleteContext "-delay", "-interval", ), - ) + ) # noqa: COM812 ) if SETTING_NAME_IS_TIMEDELTA: timedelta_scales: MutableSequence[str] = ["s", "m"] @@ -721,7 +721,7 @@ async def set_config_value(self, ctx: TeXBotApplicationContext, config_setting_n and interaction.data["custom_id"] in { "shutdown_confirm", "shutdown_cancel", - } + } # noqa: COM812 ), ) diff --git a/cogs/command_error.py b/cogs/command_error.py index b66f2a8e3..96bc84606 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -65,8 +65,8 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro isinstance(error, discord.ApplicationCommandInvokeError) and bool( isinstance(error.original, RuntimeError | NotImplementedError) - or type(error.original) is Exception - ) + or type(error.original) is Exception # noqa: COM812 + ) # noqa: COM812 ) error_code: str | None = None @@ -116,7 +116,11 @@ async def on_application_command_error(self, ctx: TeXBotApplicationContext, erro message_part for message_part in ( error.original.ERROR_CODE, - f"({command_name})" if command_name in self.ERROR_ACTIVITIES else "", + ( + f"({command_name})" + if command_name in self.ERROR_ACTIVITIES + else "" + ), str(error.original).rstrip(".:"), ) if message_part diff --git a/cogs/induct.py b/cogs/induct.py index 238df0979..8a285eedc 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -192,7 +192,9 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb main_guild: discord.Guild = self.bot.main_guild guest_role: discord.Role = await self.bot.guest_role - INDUCT_AUDIT_MESSAGE: Final[str] = f"{ctx.user} used TeX Bot slash-command: \"/induct\"" + INDUCT_AUDIT_MESSAGE: Final[str] = ( + f"{ctx.user} used TeX Bot slash-command: \"/induct\"" + ) intro_channel: discord.TextChannel | None = discord.utils.get( main_guild.text_channels, @@ -233,7 +235,7 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb async for message in general_channel.history(limit=10): message_already_sent = bool( message.author == self.bot.user - and "grab your roles" in message.content + and "grab your roles" in message.content # noqa: COM812 ) if message_already_sent: break diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 0e54fb4fe..59014fc6e 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -278,16 +278,14 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B discord_id=interaction_member.id, ) except ValidationError as create_introduction_reminder_opt_out_member_error: - error_is_already_exists: bool = ( + ERROR_IS_ALREADY_EXISTS: Final[bool] = bool( "hashed_member_id" in create_introduction_reminder_opt_out_member_error.message_dict # noqa: E501 and any( "already exists" in error - for error in create_introduction_reminder_opt_out_member_error.message_dict[ - "hashed_member_id" - ] - ) + for error in create_introduction_reminder_opt_out_member_error.message_dict["hashed_member_id"] # noqa: E501 + ) # noqa: COM812 ) - if not error_is_already_exists: + if not ERROR_IS_ALREADY_EXISTS: raise button.style = discord.ButtonStyle.green diff --git a/config/_messages/__init__.py b/config/_messages/__init__.py index 3bc0dba51..474a86ca0 100644 --- a/config/_messages/__init__.py +++ b/config/_messages/__init__.py @@ -52,10 +52,8 @@ def __getitem__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 except AttributeError as attribute_not_exist_error: key_error_message: str = item - ERROR_WAS_FROM_INVALID_KEY_NAME: Final[bool] = bool( - self.format_invalid_message_id_message(item) in str( - attribute_not_exist_error, - ) + ERROR_WAS_FROM_INVALID_KEY_NAME: Final[bool] = self.format_invalid_message_id_message(item) in str( # noqa: E501 + attribute_not_exist_error, ) if ERROR_WAS_FROM_INVALID_KEY_NAME: key_error_message = str(attribute_not_exist_error) diff --git a/config/_settings/__init__.py b/config/_settings/__init__.py index b9c5c8146..22e54294c 100644 --- a/config/_settings/__init__.py +++ b/config/_settings/__init__.py @@ -72,7 +72,7 @@ def __getattr__(self, item: str) -> Any: # type: ignore[misc] # noqa: ANN401 if item not in self._settings: INVALID_SETTINGS_KEY_MESSAGE: Final[str] = self._get_invalid_settings_key_message( - item + item, ) raise AttributeError(INVALID_SETTINGS_KEY_MESSAGE) @@ -476,7 +476,7 @@ def _reload_membership_perks_link(cls, membership_perks_link: YAML | None) -> se MEMBERSHIP_PERKS_LINK_CHANGED: Final[bool] = bool( cls._most_recent_yaml is None or "MEMBERSHIP_PERKS_LINK" not in cls._settings - or membership_perks_link != cls._most_recent_yaml["community-group"]["links"].get( # noqa: E501 + or membership_perks_link != cls._most_recent_yaml["community-group"]["links"].get( "membership-perks", None, ) # noqa: COM812 diff --git a/main.py b/main.py index 3d7b83fe9..0a13230e4 100644 --- a/main.py +++ b/main.py @@ -24,7 +24,7 @@ config.run_setup() intents: discord.Intents = discord.Intents.default() - setattr(intents, "members", True) + setattr(intents, "members", True) # noqa: B010 bot: TeXBot = TeXBot(intents=intents) # NOTE: See https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/261 diff --git a/pyproject.toml b/pyproject.toml index 7135cc17e..bf47501a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ mplcyberpunk = "^0.7" python-logging-discord-handler = "^0.1" classproperties = {git = "https://github.com/hottwaj/classproperties.git"} asyncstdlib = "~3.12" -setuptools = "^70.3" +setuptools = "^71.1" strictyaml = "^1.7" python-slugify = "^8.0" aiopath = "^0.7" diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 487adf2f1..660cc79a9 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -101,7 +101,7 @@ async def command_send_error(self, ctx: TeXBotApplicationContext, *, error_code: ) @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, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 + 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, is_fatal: bool = False) -> None: # noqa: E501 """ Construct & format an error message from the given details. @@ -120,7 +120,7 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, *, inte ) @classmethod - async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderComponent, *, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False) -> None: # noqa: PLR0913,E501 + async def _respond_with_error(cls, bot: TeXBot, responder: GenericResponderComponent, *, interaction_name: str, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, is_fatal: bool = False) -> None: # noqa: E501 construct_error_message: str = ":warning:" if is_fatal: From 0764b39368056193bef4167595cd30f7d42c01aa Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 29 Jul 2024 22:34:06 +0100 Subject: [PATCH 127/128] Fix mismatched dependencies --- poetry.lock | 159 ++++++++++++++++++++++++++++++++++++++++--------- pyproject.toml | 2 +- 2 files changed, 131 insertions(+), 30 deletions(-) diff --git a/poetry.lock b/poetry.lock index 695a74842..1fa9d2e64 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,22 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "aiofile" +version = "3.8.8" +description = "Asynchronous file operations." +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "aiofile-3.8.8-py3-none-any.whl", hash = "sha256:41e8845cce055779cd77713d949a339deb012eab605b857765e8f8e52a5ed811"}, + {file = "aiofile-3.8.8.tar.gz", hash = "sha256:41f3dc40bd730459d58610476e82e5efb2f84ae6e9fa088a9545385d838b8a43"}, +] + +[package.dependencies] +caio = ">=0.9.0,<0.10.0" + +[package.extras] +develop = ["aiomisc-pytest", "coveralls", "pytest", "pytest-cov", "pytest-rst"] + [[package]] name = "aiohttp" version = "3.9.5" @@ -95,6 +112,21 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns", "brotlicffi"] +[[package]] +name = "aiopath" +version = "0.7.7" +description = "📁 Async pathlib for Python" +optional = false +python-versions = ">=3.12" +files = [ + {file = "aiopath-0.7.7-py2.py3-none-any.whl", hash = "sha256:cd5d18de8ede167e1db659f02ee448fe085f923cb8e194407ccc568bffc4fe4e"}, + {file = "aiopath-0.7.7.tar.gz", hash = "sha256:ad4b9d09ae08ddf6d39dd06e7b0a353939e89528da571c0cd4f3fe071aefad4f"}, +] + +[package.dependencies] +aiofile = ">=3.8.8,<4" +anyio = ">=4.0.0,<5" + [[package]] name = "aiosignal" version = "1.3.1" @@ -109,6 +141,26 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "application-properties" version = "0.8.2" @@ -195,6 +247,30 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "caio" +version = "0.9.17" +description = "Asynchronous file IO for Linux MacOS or Windows." +optional = false +python-versions = "<4,>=3.7" +files = [ + {file = "caio-0.9.17-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3f69395fdd45c115b2ef59732e3c8664722a2b51de2d6eedb3d354b2f5f3be3c"}, + {file = "caio-0.9.17-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3028b746e9ec7f6d6ebb386a7fd8caf0eebed5d6e6b4f18c8ef25861934b1673"}, + {file = "caio-0.9.17-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:079730a353bbde03796fab681e969472eace09ffbe5000e584868a7fe389ba6f"}, + {file = "caio-0.9.17-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:549caa51b475877fe32856a26fe937366ae7a1c23a9727005b441db9abb12bcc"}, + {file = "caio-0.9.17-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0ddb253b145a53ecca76381677ce465bc5efeaecb6aaf493fac43ae79659f0fb"}, + {file = "caio-0.9.17-cp312-cp312-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e320b0ea371c810359934f8e8fe81777c493cc5fb4d41de44277cbe7336e74"}, + {file = "caio-0.9.17-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a39a49e279f82aa022f0786339d45d9550b5aa3e46eec7d08e0f351c503df0a5"}, + {file = "caio-0.9.17-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e96925b9f15f43e6ef1d42a83edfd937eb11a984cb6ef7c10527e963595497"}, + {file = "caio-0.9.17-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fca916240597005d2b734f1442fa3c3cfb612bf46e0978b5232e5492a371de38"}, + {file = "caio-0.9.17-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40bd0afbd3491d1e407bcf74e3a9e9cc67a7f290ed29518325194184d63cc2b6"}, + {file = "caio-0.9.17-py3-none-any.whl", hash = "sha256:c55d4dc6b3a36f93237ecd6360e1c131c3808bc47d4191a130148a99b80bb311"}, + {file = "caio-0.9.17.tar.gz", hash = "sha256:8f30511526814d961aeef389ea6885273abe6c655f1e08abbadb95d12fdd9b4f"}, +] + +[package.extras] +develop = ["aiomisc-pytest", "pytest", "pytest-cov"] + [[package]] name = "ccft-pymarkdown" version = "1.1.2" @@ -1441,20 +1517,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - [[package]] name = "python-logging-discord-handler" version = "0.1.4" @@ -1472,6 +1534,23 @@ discord-webhook = ">=1.0.0,<2.0.0" [package.extras] docs = ["Sphinx (>=4.4.0,<5.0.0)", "sphinx-autodoc-typehints[docs] (>=1.16.0,<2.0.0)", "sphinx-rtd-theme (>=1.0.0,<2.0.0)", "sphinx-sitemap (>=2.2.0,<3.0.0)"] +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1617,6 +1696,17 @@ files = [ {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "soupsieve" version = "2.5" @@ -1643,6 +1733,31 @@ files = [ dev = ["build", "hatch"] doc = ["sphinx"] +[[package]] +name = "strictyaml" +version = "1.7.3" +description = "Strict, typed YAML parser" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "strictyaml-1.7.3-py3-none-any.whl", hash = "sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7"}, + {file = "strictyaml-1.7.3.tar.gz", hash = "sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407"}, +] + +[package.dependencies] +python-dateutil = ">=2.6.0" + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -1740,20 +1855,6 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "validators" -version = "0.33.0" -description = "Python Data Validation for Humans™" -optional = false -python-versions = ">=3.8" -files = [ - {file = "validators-0.33.0-py3-none-any.whl", hash = "sha256:134b586a98894f8139865953899fc2daeb3d0c35569552c5518f089ae43ed075"}, - {file = "validators-0.33.0.tar.gz", hash = "sha256:535867e9617f0100e676a1257ba1e206b9bfd847ddc171e4d44811f07ff0bfbf"}, -] - -[package.extras] -crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] - [[package]] name = "virtualenv" version = "20.26.3" @@ -1891,4 +1992,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "247f394ba9db556f1db3e5cb3051a617618f4d74736eba9cea280f8cc4df43f8" +content-hash = "4a5bccba45f5df588edefe2d970c3e7cccd439978268e792dae7e315e84e3ced" diff --git a/pyproject.toml b/pyproject.toml index f76022fb0..f18d6c57b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ mplcyberpunk = "^0.7" python-logging-discord-handler = "^0.1" classproperties = {git = "https://github.com/hottwaj/classproperties.git"} asyncstdlib = "~3.12" -setuptools = "^71.1" +setuptools = "^70.3" strictyaml = "^1.7" python-slugify = "^8.0" aiopath = "^0.7" From eb252db1b1c72877333c2315026d5b283772d1b3 Mon Sep 17 00:00:00 2001 From: Matt Norton Date: Mon, 29 Jul 2024 22:34:20 +0100 Subject: [PATCH 128/128] Fix typing errors --- cogs/send_get_roles_reminders.py | 2 +- cogs/send_introduction_reminders.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 83c04604d..d3e6280dc 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -51,7 +51,7 @@ def cog_unload(self) -> None: """ self.send_get_roles_reminders.cancel() - @tasks.loop(seconds=settings["SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] + @tasks.loop(seconds=settings["SEND_GET_ROLES_REMINDERS_INTERVAL_SECONDS"]) @functools.partial( ErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError, diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 59014fc6e..8ebaadd85 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -91,7 +91,7 @@ async def _check_if_member_needs_reminder(cls, member_id: int, member_joined_at: and not MEMBER_OPTED_OUT_FROM_REMINDERS ) - @tasks.loop(seconds=settings["SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS"]) # type: ignore[misc] + @tasks.loop(seconds=settings["SEND_INTRODUCTION_REMINDERS_INTERVAL_SECONDS"]) @functools.partial( ErrorCaptureDecorators.capture_error_and_close, error_type=GuestRoleDoesNotExistError,