From a9cf7cfcd7e402a332dfb1526263855dce55402c Mon Sep 17 00:00:00 2001 From: thegamecracks <61257169+thegamecracks@users.noreply.github.com> Date: Thu, 10 Nov 2022 11:11:15 -0500 Subject: [PATCH 01/50] Update intents.md - More generally describes features enabled by intents - Adds a disclaimer for discord.py v2.0.0 requiring intents - Adds distinction between standard and privileged intents - Replaces `members` intent in code snippet with `message_content` in accordance with the usage of `commands.Bot` --- bot/resources/tags/intents.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bot/resources/tags/intents.md b/bot/resources/tags/intents.md index aa49d59ae7..7aab9e4700 100644 --- a/bot/resources/tags/intents.md +++ b/bot/resources/tags/intents.md @@ -1,19 +1,17 @@ **Using intents in discord.py** -Intents are a feature of Discord that tells the gateway exactly which events to send your bot. By default discord.py has all intents enabled except for `Members`, `Message Content`, and `Presences`. These are needed for features such as `on_member` events, to get access to message content, and to get members' statuses. +Intents are a feature of Discord that tells the gateway exactly which events to send your bot. Various features of discord.py rely on having particular intents enabled. Since discord.py v2.0.0, this has become **mandatory** for developers to define in their code. -To enable one of these intents, you need to first go to the [Discord developer portal](https://discord.com/developers/applications), then to the bot page of your bot's application. Scroll down to the `Privileged Gateway Intents` section, then enable the intents that you need. - -Next, in your bot you need to set the intents you want to connect with in the bot's constructor using the `intents` keyword argument, like this: +There are *standard* intents and *privileged* intents. The current privileged intents are `Presences`, `Server Members`, and `Message Content`. To use one of the privileged intents, you have to first enable them in the [Discord Developer Portal](https://discord.com/developers/applications). Go to the `Bot` page of your application, scroll down to the `Privileged Gateway Intents` section, and enable the privileged intents that you need. +Afterwards in your code, you need to set the intents you want to connect with in the bot's constructor using the `intents` keyword argument, like this: ```py from discord import Intents from discord.ext import commands intents = Intents.default() -intents.members = True +intents.message_content = True bot = commands.Bot(command_prefix="!", intents=intents) ``` - For more info about using intents, see the [discord.py docs on intents](https://discordpy.readthedocs.io/en/latest/intents.html), and for general information about them, see the [Discord developer documentation on intents](https://discord.com/developers/docs/topics/gateway#gateway-intents). From 50958801a6215446edb1c1d16065a3686ab58c2a Mon Sep 17 00:00:00 2001 From: thegamecracks <61257169+thegamecracks@users.noreply.github.com> Date: Thu, 10 Nov 2022 21:54:59 -0500 Subject: [PATCH 02/50] Add further clarification to intents.md - References discord.Intents documentation for detail on available intents and which features they impact - Explicitly describes no requirements for standard intents - Adds a code comment explaining the intents being enabled --- bot/resources/tags/intents.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bot/resources/tags/intents.md b/bot/resources/tags/intents.md index 7aab9e4700..99f14f9310 100644 --- a/bot/resources/tags/intents.md +++ b/bot/resources/tags/intents.md @@ -1,17 +1,19 @@ **Using intents in discord.py** -Intents are a feature of Discord that tells the gateway exactly which events to send your bot. Various features of discord.py rely on having particular intents enabled. Since discord.py v2.0.0, this has become **mandatory** for developers to define in their code. +Intents are a feature of Discord that tells the gateway exactly which events to send your bot. Various features of discord.py rely on having particular intents enabled, further detailed [in its documentation](https://discordpy.readthedocs.io/en/stable/api.html#intents). Since discord.py v2.0.0, it has become **mandatory** for developers to explicitly define the values of these intents in their code. -There are *standard* intents and *privileged* intents. The current privileged intents are `Presences`, `Server Members`, and `Message Content`. To use one of the privileged intents, you have to first enable them in the [Discord Developer Portal](https://discord.com/developers/applications). Go to the `Bot` page of your application, scroll down to the `Privileged Gateway Intents` section, and enable the privileged intents that you need. +There are *standard* and *privileged* intents. To use privileged intents like `Presences`, `Server Members`, and `Message Content`, you have to first enable them in the [Discord Developer Portal](https://discord.com/developers/applications). In there, go to the `Bot` page of your application, scroll down to the `Privileged Gateway Intents` section, and enable the privileged intents that you need. Standard intents can be used without any changes in the developer portal. Afterwards in your code, you need to set the intents you want to connect with in the bot's constructor using the `intents` keyword argument, like this: ```py from discord import Intents from discord.ext import commands +# Enable all standard intents and message content +# (prefix commands generally require message content) intents = Intents.default() intents.message_content = True bot = commands.Bot(command_prefix="!", intents=intents) ``` -For more info about using intents, see the [discord.py docs on intents](https://discordpy.readthedocs.io/en/latest/intents.html), and for general information about them, see the [Discord developer documentation on intents](https://discord.com/developers/docs/topics/gateway#gateway-intents). +For more info about using intents, see [discord.py's related guide](https://discordpy.readthedocs.io/en/latest/intents.html), and for general information about them, see the [Discord developer documentation on intents](https://discord.com/developers/docs/topics/gateway#gateway-intents). From e2798da2670c0f22684740275a03c09962e8a931 Mon Sep 17 00:00:00 2001 From: Keyacom <70766223+Keyacom@users.noreply.github.com> Date: Thu, 17 Nov 2022 21:06:58 +0100 Subject: [PATCH 03/50] Edited several tags (#2322) * Edited several tags --- bot/resources/tags/environments.md | 10 +++++++--- bot/resources/tags/identity.md | 18 ++++++++++-------- bot/resources/tags/inline.md | 2 ++ bot/resources/tags/listcomps.md | 4 ++-- bot/resources/tags/local-file.md | 4 ++-- bot/resources/tags/slicing.md | 2 +- bot/resources/tags/traceback.md | 1 + bot/resources/tags/venv.md | 12 +++++++++--- 8 files changed, 34 insertions(+), 19 deletions(-) diff --git a/bot/resources/tags/environments.md b/bot/resources/tags/environments.md index 7bc69bde4a..7402bbec4e 100644 --- a/bot/resources/tags/environments.md +++ b/bot/resources/tags/environments.md @@ -1,12 +1,16 @@ -**Python Environments** +--- +aliases: ["envs"] +embed: + title: "Python Environments" +--- The main purpose of Python [virtual environments](https://docs.Python.org/3/library/venv.html#venv-def) is to create an isolated environment for Python projects. This means that each project can have its own dependencies, such as third party packages installed using pip, regardless of what dependencies every other project has. To see the current environment in use by Python, you can run: ```py >>> import sys ->>> print(sys.executable) -/usr/bin/python3 +>>> sys.executable +'/usr/bin/python3' ``` To see the environment in use by pip, you can do `pip debug` (`pip3 debug` for Linux/macOS). The 3rd line of the output will contain the path in use e.g. `sys.executable: /usr/bin/python3`. diff --git a/bot/resources/tags/identity.md b/bot/resources/tags/identity.md index fb20107590..f9fb0925cd 100644 --- a/bot/resources/tags/identity.md +++ b/bot/resources/tags/identity.md @@ -13,12 +13,14 @@ if x == 3: ``` To check if two objects are actually the same thing in memory, use the identity comparison operator (`is`). ```py -list_1 = [1, 2, 3] -list_2 = [1, 2, 3] -if list_1 is [1, 2, 3]: - print("list_1 is list_2") -reference_to_list_1 = list_1 -if list_1 is reference_to_list_1: - print("list_1 is reference_to_list_1") -# Prints 'list_1 is reference_to_list_1' +>>> list_1 = [1, 2, 3] +>>> list_2 = [1, 2, 3] +>>> if list_1 is [1, 2, 3]: +... print("list_1 is list_2") +... +>>> reference_to_list_1 = list_1 +>>> if list_1 is reference_to_list_1: +... print("list_1 is reference_to_list_1") +... +list_1 is reference_to_list_1 ``` diff --git a/bot/resources/tags/inline.md b/bot/resources/tags/inline.md index 4ece74ef7e..1c02807270 100644 --- a/bot/resources/tags/inline.md +++ b/bot/resources/tags/inline.md @@ -4,4 +4,6 @@ Inline codeblocks look `like this`. To create them you surround text with single Note that backticks are not quotes, see [this](https://superuser.com/questions/254076/how-do-i-type-the-tick-and-backtick-characters-on-windows/254077#254077) if you are struggling to find the backtick key. +If the wrapped code itself has a backtick, wrap it with two backticks from each side: \`\`back \` tick\`\` would become ``back ` tick``. + For how to make multiline codeblocks see the `!codeblock` tag. diff --git a/bot/resources/tags/listcomps.md b/bot/resources/tags/listcomps.md index ba00a4bf77..ccede4fbaa 100644 --- a/bot/resources/tags/listcomps.md +++ b/bot/resources/tags/listcomps.md @@ -10,8 +10,8 @@ Using list comprehensions can make this both shorter and more readable. As a lis >>> [n ** 2 for n in range(5)] [0, 1, 4, 9, 16] ``` -List comprehensions also get an `if` statement: -```python +List comprehensions also get an `if` clause: +```py >>> [n ** 2 for n in range(5) if n % 2 == 0] [0, 4, 16] ``` diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index 4a80e87e3b..da4ac21ce0 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -7,8 +7,8 @@ file = discord.File("/this/is/path/to/my/file.png", filename="file.png") with open("/this/is/path/to/my/file.png", "rb") as f: file = discord.File(f) ``` -When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing `filename` to it is not necessary. -Please note that `filename` can't contain underscores. This is a Discord limitation. +When using the file-like object, you have to open it in `rb` ('read binary') mode. Also, in this case, passing `filename` to it is not necessary. +Please note that `filename` must not contain underscores. This is a Discord limitation. [`discord.Embed`](https://discordpy.readthedocs.io/en/stable/api.html#discord.Embed) instances have a [`set_image`](https://discordpy.readthedocs.io/en/stable/api.html#discord.Embed.set_image) method which can be used to set an attachment as an image: ```py diff --git a/bot/resources/tags/slicing.md b/bot/resources/tags/slicing.md index 717fc46b78..0d82c642cb 100644 --- a/bot/resources/tags/slicing.md +++ b/bot/resources/tags/slicing.md @@ -3,7 +3,7 @@ aliases: ["slice", "seqslice", "seqslicing", "sequence-slice", "sequence-slicing embed: title: "Sequence slicing" --- -*Slicing* is a way of accessing a part of a sequence by specifying a start, stop, and step. As with normal indexing, negative numbers can be used to count backwards. +**Slicing** is a way of accessing a part of a sequence by specifying a start, stop, and step. As with normal indexing, negative numbers can be used to count backwards. **Examples** ```py diff --git a/bot/resources/tags/traceback.md b/bot/resources/tags/traceback.md index e21fa6c6ef..e05d8b2597 100644 --- a/bot/resources/tags/traceback.md +++ b/bot/resources/tags/traceback.md @@ -10,6 +10,7 @@ Traceback (most recent call last): add_three("6") File "my_file.py", line 2, in add_three a = num + 3 + ~~~~^~~ TypeError: can only concatenate str (not "int") to str ``` If the traceback is long, use [our pastebin](https://paste.pythondiscord.com/). diff --git a/bot/resources/tags/venv.md b/bot/resources/tags/venv.md index a4fc62151e..99ff2a7077 100644 --- a/bot/resources/tags/venv.md +++ b/bot/resources/tags/venv.md @@ -1,4 +1,8 @@ -**Virtual Environments** +--- +aliases: ["virtualenv"] +embed: + title: "Virtual Environments" +--- Virtual environments are isolated Python environments, which make it easier to keep your system clean and manage dependencies. By default, when activated, only libraries and scripts installed in the virtual environment are accessible, preventing cross-project dependency conflicts, and allowing easy isolation of requirements. @@ -16,5 +20,7 @@ For more information, take a read of the [documentation](https://docs.python.org Tools such as [poetry](https://python-poetry.org/docs/basic-usage/) and [pipenv](https://pipenv.pypa.io/en/latest/) can manage the creation of virtual environments as well as project dependencies, making packaging and installing your project easier. -**Note:** When using Windows PowerShell, you may need to change the [execution policy](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies) first. This is only required once: -`Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` +**Note:** When using PowerShell in Windows, you may need to change the [execution policy](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies) first. This is only required once per user: +```ps1 +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` From af9612f264a544912b07fc50abb351990b2b351d Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 25 Nov 2022 13:14:51 +0100 Subject: [PATCH 04/50] Link directly to the sqlite3 placeholder howto --- bot/resources/tags/sql-fstring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/sql-fstring.md b/bot/resources/tags/sql-fstring.md index 538a0aa870..fa28b6e3b1 100644 --- a/bot/resources/tags/sql-fstring.md +++ b/bot/resources/tags/sql-fstring.md @@ -12,5 +12,5 @@ db.execute(query, params) Note: Different database libraries support different placeholder styles, e.g. `%s` and `$1`. Consult your library's documentation for details. **See Also** -• [Extended Example with SQLite](https://docs.python.org/3/library/sqlite3.html) (search for "Instead, use the DB-API's parameter substitution") +• [Python sqlite3 docs](https://docs.python.org/3/library/sqlite3.html#how-to-use-placeholders-to-bind-values-in-sql-queries) - How to use placeholders to bind values in SQL queries • [PEP-249](https://peps.python.org/pep-0249/) - A specification of how database libraries in Python should work From 7bb24dc542da18f6f2ea59b54c587da678b4dc20 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 1 Nov 2022 18:49:39 +0000 Subject: [PATCH 05/50] Update constants for new forum channel help system --- .gitignore | 2 +- bot/constants.py | 18 +----------------- config-default.yml | 40 ++-------------------------------------- 3 files changed, 4 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 1773459082..6691dbea18 100644 --- a/.gitignore +++ b/.gitignore @@ -114,7 +114,7 @@ log.* !log.py # Custom user configuration -config.yml +*config.yml docker-compose.override.yml metricity-config.toml diff --git a/bot/constants.py b/bot/constants.py index ba7d53ea84..24862059e1 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -387,9 +387,6 @@ class Categories(metaclass=YAMLGetter): section = "guild" subsection = "categories" - help_available: int - help_dormant: int - help_in_use: int moderators: int modmail: int voice: int @@ -416,8 +413,7 @@ class Channels(metaclass=YAMLGetter): meta: int python_general: int - cooldown: int - how_to_get_help: int + help_system_forum: int attachment_log: int filter_log: int @@ -620,18 +616,6 @@ class HelpChannels(metaclass=YAMLGetter): enable: bool cmd_whitelist: List[int] - idle_minutes_claimant: int - idle_minutes_others: int - deleted_idle_minutes: int - max_available: int - max_total_channels: int - name_prefix: str - notify_channel: int - notify_minutes: int - notify_none_remaining: bool - notify_none_remaining_roles: List[int] - notify_running_low: bool - notify_running_low_threshold: int class RedirectOutput(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index a5f4a5bdad..c9d043ff7c 100644 --- a/config-default.yml +++ b/config-default.yml @@ -139,9 +139,6 @@ guild: invite: "https://discord.gg/python" categories: - help_available: 691405807388196926 - help_dormant: 691405908919451718 - help_in_use: 696958401460043776 logs: &LOGS 468520609152892958 moderators: &MODS_CATEGORY 749736277464842262 modmail: &MODMAIL 714494672835444826 @@ -169,9 +166,8 @@ guild: meta: 429409067623251969 python_general: &PY_GENERAL 267624335836053506 - # Python Help: Available - cooldown: 720603994149486673 - how_to_get_help: 704250143020417084 + # Python Help + help_system_forum: 1035199133436354600 # Topical discord_bots: 343944376055103488 @@ -501,38 +497,6 @@ help_channels: cmd_whitelist: - *HELPERS_ROLE - # Allowed duration of inactivity by claimant before making a channel dormant - idle_minutes_claimant: 30 - - # Allowed duration of inactivity by others before making a channel dormant - # `idle_minutes_claimant` must also be met, before a channel is closed - idle_minutes_others: 10 - - # Allowed duration of inactivity when channel is empty (due to deleted messages) - # before message making a channel dormant - deleted_idle_minutes: 5 - - # Maximum number of channels to put in the available category - max_available: 3 - - # Maximum number of channels across all 3 categories - # Note Discord has a hard limit of 50 channels per category, so this shouldn't be > 50 - max_total_channels: 42 - - # Prefix for help channel names - name_prefix: 'help-' - - notify_channel: *HELPERS # Channel in which to send notifications messages - notify_minutes: 15 # Minimum interval between none_remaining or running_low notifications - - notify_none_remaining: true # Pinging notification for the Helper role when no dormant channels remain - notify_none_remaining_roles: # Mention these roles in the none_remaining notification - - *HELPERS_ROLE - - notify_running_low: true # Non-pinging notification which is triggered when the channel count is equal or less than the threshold - notify_running_low_threshold: 4 # The amount of channels at which a running_low notification will be sent - - redirect_output: delete_delay: 15 delete_invocation: true From d7d352a00d9e10537e709ff51fa27aa6150a40ad Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 1 Nov 2022 18:50:46 +0000 Subject: [PATCH 06/50] Remove unused caches --- bot/exts/help_channels/_caches.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/bot/exts/help_channels/_caches.py b/bot/exts/help_channels/_caches.py index 937c4ab57b..eb15b85606 100644 --- a/bot/exts/help_channels/_caches.py +++ b/bot/exts/help_channels/_caches.py @@ -1,26 +1,5 @@ from async_rediscache import RedisCache -# This dictionary maps a help channel to the time it was claimed -# RedisCache[discord.TextChannel.id, UtcPosixTimestamp] -claim_times = RedisCache(namespace="HelpChannels.claim_times") - -# This cache tracks which channels are claimed by which members. -# RedisCache[discord.TextChannel.id, t.Union[discord.User.id, discord.Member.id]] -claimants = RedisCache(namespace="HelpChannels.help_channel_claimants") - -# Stores the timestamp of the last message from the claimant of a help channel -# RedisCache[discord.TextChannel.id, UtcPosixTimestamp] -claimant_last_message_times = RedisCache(namespace="HelpChannels.claimant_last_message_times") - -# This cache maps a help channel to the timestamp of the last non-claimant message. -# This cache being empty for a given help channel indicates the question is unanswered. -# RedisCache[discord.TextChannel.id, UtcPosixTimestamp] -non_claimant_last_message_times = RedisCache(namespace="HelpChannels.non_claimant_last_message_times") - -# This cache keeps track of the dynamic message ID for -# the continuously updated message in the #How-to-get-help channel. -dynamic_message = RedisCache(namespace="HelpChannels.dynamic_message") - # This cache keeps track of who has help-dms on. # RedisCache[discord.User.id, bool] help_dm = RedisCache(namespace="HelpChannels.help_dm") From 451a2d87cdf8a007b4a7e58c47db785ffc9418ea Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 1 Nov 2022 18:52:28 +0000 Subject: [PATCH 07/50] Update help channel system to use forum channels This leverages Discord's new forum chanel feature, which removes the need for a lot of our custom logic, simplifying the help channel cog significantly. --- bot/exts/help_channels/__init__.py | 40 +- bot/exts/help_channels/_channel.py | 281 +++++------- bot/exts/help_channels/_cog.py | 674 +++-------------------------- bot/exts/help_channels/_message.py | 311 +++---------- bot/exts/help_channels/_name.py | 69 --- bot/exts/help_channels/_stats.py | 50 ++- 6 files changed, 263 insertions(+), 1162 deletions(-) delete mode 100644 bot/exts/help_channels/_name.py diff --git a/bot/exts/help_channels/__init__.py b/bot/exts/help_channels/__init__.py index b9c9401839..00b4a735bd 100644 --- a/bot/exts/help_channels/__init__.py +++ b/bot/exts/help_channels/__init__.py @@ -1,40 +1,8 @@ -from bot import constants -from bot.bot import Bot -from bot.exts.help_channels._channel import MAX_CHANNELS_PER_CATEGORY -from bot.log import get_logger - -log = get_logger(__name__) - - -def validate_config() -> None: - """Raise a ValueError if the cog's config is invalid.""" - log.trace("Validating config.") - total = constants.HelpChannels.max_total_channels - available = constants.HelpChannels.max_available - if total == 0 or available == 0: - raise ValueError("max_total_channels and max_available and must be greater than 0.") - - if total < available: - raise ValueError( - f"max_total_channels ({total}) must be greater than or equal to max_available " - f"({available})." - ) - - if total > MAX_CHANNELS_PER_CATEGORY: - raise ValueError( - f"max_total_channels ({total}) must be less than or equal to " - f"{MAX_CHANNELS_PER_CATEGORY} due to Discord's limit on channels per category." - ) +from bot.bot import Bot +from bot.exts.help_channels._cog import HelpForum async def setup(bot: Bot) -> None: - """Load the HelpChannels cog.""" - # Defer import to reduce side effects from importing the help_channels package. - from bot.exts.help_channels._cog import HelpChannels - try: - validate_config() - except ValueError as e: - log.error(f"HelpChannels cog will not be loaded due to misconfiguration: {e}") - else: - await bot.add_cog(HelpChannels(bot)) + """Load the HelpForum cog.""" + await bot.add_cog(HelpForum(bot)) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index cfe774f4c1..38725ddfde 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,195 +1,144 @@ -import re -import typing as t -from datetime import timedelta -from enum import Enum +"""Contains all logic to handle changes to posts in the help forum.""" + +import textwrap -import arrow import discord -from arrow import Arrow +from botcore.utils import members import bot from bot import constants -from bot.exts.help_channels import _caches, _message +from bot.exts.help_channels import _stats from bot.log import get_logger -from bot.utils.channel import get_or_fetch_channel log = get_logger(__name__) -MAX_CHANNELS_PER_CATEGORY = 50 -EXCLUDED_CHANNELS = (constants.Channels.cooldown,) -CLAIMED_BY_RE = re.compile(r"Channel claimed by <@!?(?P\d{17,20})>\.$") +ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" + +POST_TITLE = "Python help channel" +NEW_POST_MSG = f""" +**Remember to:** +• **Ask** your Python question, not if you can ask or if there's an expert who can help. +• **Show** a code sample as text (rather than a screenshot) and the error message, if you got one. +• **Explain** what you expect to happen and what actually happens. +For more tips, check out our guide on [asking good questions]({ASKING_GUIDE_URL}). +""" +POST_FOOTER = f"Closes after a period of inactivity, or when you send {constants.Bot.prefix}close." -class ClosingReason(Enum): - """All possible closing reasons for help channels.""" +DORMANT_MSG = f""" +This help channel has been marked as **dormant** and locked. \ +It is no longer possible to send messages in this channel. - COMMAND = "command" - LATEST_MESSAGE = "auto.latest_message" - CLAIMANT_TIMEOUT = "auto.claimant_timeout" - OTHER_TIMEOUT = "auto.other_timeout" - DELETED = "auto.deleted" - CLEANUP = "auto.cleanup" +If your question wasn't answered yet, you can create a new post in <#{constants.Channels.help_system_forum}>. \ +Consider rephrasing the question to maximize your chance of getting a good answer. \ +If you're not sure how, have a look through our guide for **[asking a good question]({ASKING_GUIDE_URL})**. +""" -def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: - """Yield the text channels of the `category` in an unsorted manner.""" - log.trace(f"Getting text channels in the category '{category}' ({category.id}).") +def is_help_forum_post(channel: discord.abc.GuildChannel) -> bool: + """Return True if `channel` is a post in the help forum.""" + log.trace(f"Checking if #{channel} is a help channel.") + return getattr(channel, "parent_id", None) == constants.Channels.help_system_forum - # This is faster than using category.channels because the latter sorts them. - for channel in category.guild.channels: - if channel.category_id == category.id and not is_excluded_channel(channel): - yield channel +async def _close_help_thread(closed_thread: discord.Thread, closed_on: _stats.ClosingReason) -> None: + """Close the help thread and record stats.""" + embed = discord.Embed(description=DORMANT_MSG) + await closed_thread.send(embed=embed) + await closed_thread.edit(archived=True, locked=True, reason="Locked a dormant help channel") -async def get_closing_time(channel: discord.TextChannel, init_done: bool) -> t.Tuple[Arrow, ClosingReason]: - """ - Return the time at which the given help `channel` should be closed along with the reason. + _stats.report_post_count() + await _stats.report_complete_session(closed_thread, closed_on) - `init_done` is True if the cog has finished loading and False otherwise. + poster = closed_thread.owner + cooldown_role = closed_thread.guild.get_role(constants.Roles.help_cooldown) + await members.handle_role_change(poster, poster.remove_roles, cooldown_role) - The time is calculated as follows: - * If `init_done` is True or the cached time for the claimant's last message is unavailable, - add the configured `idle_minutes_claimant` to the time the most recent message was sent. - * If the help session is empty (see `is_empty`), do the above but with `deleted_idle_minutes`. - * If either of the above is attempted but the channel is completely empty, close the channel - immediately. - * Otherwise, retrieve the times of the claimant's and non-claimant's last messages from the - cache. Add the configured `idle_minutes_claimant` and idle_minutes_others`, respectively, and - choose the time which is furthest in the future. - """ - log.trace(f"Getting the closing time for #{channel} ({channel.id}).") - - is_empty = await _message.is_empty(channel) - if is_empty: - idle_minutes_claimant = constants.HelpChannels.deleted_idle_minutes - else: - idle_minutes_claimant = constants.HelpChannels.idle_minutes_claimant - - claimant_time = await _caches.claimant_last_message_times.get(channel.id) - - # The current session lacks messages, the cog is still starting, or the cache is empty. - if is_empty or not init_done or claimant_time is None: - msg = await _message.get_last_message(channel) - if not msg: - log.debug(f"No idle time available; #{channel} ({channel.id}) has no messages, closing now.") - return Arrow.min, ClosingReason.DELETED - - # Use the greatest offset to avoid the possibility of prematurely closing the channel. - time = Arrow.fromdatetime(msg.created_at) + timedelta(minutes=idle_minutes_claimant) - reason = ClosingReason.DELETED if is_empty else ClosingReason.LATEST_MESSAGE - return time, reason - - claimant_time = Arrow.utcfromtimestamp(claimant_time) - others_time = await _caches.non_claimant_last_message_times.get(channel.id) - - if others_time: - others_time = Arrow.utcfromtimestamp(others_time) - else: - # The help session hasn't received any answers (messages from non-claimants) yet. - # Set to min value so it isn't considered when calculating the closing time. - others_time = Arrow.min - - # Offset the cached times by the configured values. - others_time += timedelta(minutes=constants.HelpChannels.idle_minutes_others) - claimant_time += timedelta(minutes=idle_minutes_claimant) - - # Use the time which is the furthest into the future. - if claimant_time >= others_time: - closing_time = claimant_time - reason = ClosingReason.CLAIMANT_TIMEOUT - else: - closing_time = others_time - reason = ClosingReason.OTHER_TIMEOUT - - log.trace(f"#{channel} ({channel.id}) should be closed at {closing_time} due to {reason}.") - return closing_time, reason - - -async def get_in_use_time(channel_id: int) -> t.Optional[timedelta]: - """Return the duration `channel_id` has been in use. Return None if it's not in use.""" - log.trace(f"Calculating in use time for channel {channel_id}.") - - claimed_timestamp = await _caches.claim_times.get(channel_id) - if claimed_timestamp: - claimed = Arrow.utcfromtimestamp(claimed_timestamp) - return arrow.utcnow() - claimed - - -def is_excluded_channel(channel: discord.abc.GuildChannel) -> bool: - """Check if a channel should be excluded from the help channel system.""" - return not isinstance(channel, discord.TextChannel) or channel.id in EXCLUDED_CHANNELS - - -async def move_to_bottom(channel: discord.TextChannel, category_id: int, **options) -> None: - """ - Move the `channel` to the bottom position of `category` and edit channel attributes. - - To ensure "stable sorting", we use the `bulk_channel_update` endpoint and provide the current - positions of the other channels in the category as-is. This should make sure that the channel - really ends up at the bottom of the category. - - If `options` are provided, the channel will be edited after the move is completed. This is the - same order of operations that `discord.TextChannel.edit` uses. For information on available - options, see the documentation on `discord.TextChannel.edit`. While possible, position-related - options should be avoided, as it may interfere with the category move we perform. - """ - # Get a fresh copy of the category from the bot to avoid the cache mismatch issue we had. - category = await get_or_fetch_channel(category_id) - - payload = [{"id": c.id, "position": c.position} for c in category.channels] - - # Calculate the bottom position based on the current highest position in the category. If the - # category is currently empty, we simply use the current position of the channel to avoid making - # unnecessary changes to positions in the guild. - bottom_position = payload[-1]["position"] + 1 if payload else channel.position - - payload.append( - { - "id": channel.id, - "position": bottom_position, - "parent_id": category.id, - "lock_permissions": True, - } +async def send_opened_post_message(thread: discord.Thread) -> None: + """Send the opener message in the new help post.""" + embed = discord.Embed( + color=constants.Colours.bright_green, + description=NEW_POST_MSG, + ) + embed.set_author(name=POST_TITLE) + embed.set_footer(text=POST_FOOTER) + await thread.send(embed=embed) + + +async def send_opened_post_dm(thread: discord.Thread) -> None: + """Send the opener a DM message with a jump link to their new post.""" + embed = discord.Embed( + title="Help channel opened", + description=f"You opened {thread.mention}.", + colour=constants.Colours.bright_green, + timestamp=thread.created_at, + ) + embed.set_thumbnail(url=constants.Icons.green_questionmark) + message = thread.starter_message + if not message: + try: + message = await thread.fetch_message(thread.id) + except discord.HTTPException: + log.warning(f"Could not fetch message for thread {thread.name} ({thread.id})") + return + + formatted_message = textwrap.shorten(message.content, width=100, placeholder="...") + embed.add_field(name="Your message", value=formatted_message, inline=False) + embed.add_field( + name="Conversation", + value=f"[Jump to message!]({message.jump_url})", + inline=False, ) - # We use d.py's method to ensure our request is processed by d.py's rate limit manager - await bot.instance.http.bulk_channel_update(category.guild.id, payload) + try: + await thread.owner.send(embed=embed) + log.trace(f"Sent DM to {thread.owner} ({thread.owner_id}) after posting in help forum.") + except discord.errors.Forbidden: + log.trace( + f"Ignoring to send DM to {thread.owner} ({thread.owner_id}) after posting in help forum: DMs disabled.", + ) - # Now that the channel is moved, we can edit the other attributes - if options: - await channel.edit(**options) +async def help_thread_opened(opened_thread: discord.Thread, *, reopen: bool = False) -> None: + """Apply new post logic to a new help forum post.""" + _stats.report_post_count() -async def ensure_cached_claimant(channel: discord.TextChannel) -> None: - """ - Ensure there is a claimant cached for each help channel. + if not isinstance(opened_thread.owner, discord.Member): + log.debug(f"{opened_thread.owner_id} isn't a member. Closing post.") + await _close_help_thread(opened_thread, _stats.ClosingReason.CLEANUP) + return - Check the redis cache first, return early if there is already a claimant cached. - If there isn't an entry in redis, search for the "Claimed by X." embed in channel history. - Stopping early if we discover a dormant message first. + await send_opened_post_message(opened_thread) + await send_opened_post_dm(opened_thread) + + cooldown_role = opened_thread.guild.get_role(constants.Roles.help_cooldown) + await members.handle_role_change(opened_thread.owner, opened_thread.owner.add_roles, cooldown_role) - If a claimant could not be found, send a warning to #helpers and set the claimant to the bot. - """ - if await _caches.claimants.get(channel.id): - return - async for message in channel.history(limit=1000): - if message.author.id != bot.instance.user.id: - # We only care about bot messages +async def help_thread_closed(closed_thread: discord.Thread) -> None: + """Apply archive logic to a manually closed help forum post.""" + await _close_help_thread(closed_thread, _stats.ClosingReason.COMMAND) + + +async def help_thread_archived(archived_thread: discord.Thread) -> None: + """Apply archive logic to an archived help forum post.""" + async for thread_update in archived_thread.guild.audit_logs(limit=50, action=discord.AuditLogAction.thread_update): + if thread_update.target.id != archived_thread.id: continue - if message.embeds: - if _message._match_bot_embed(message, _message.DORMANT_MSG): - log.info("Hit the dormant message embed before finding a claimant in %s (%d).", channel, channel.id) - break - # Only set the claimant if the first embed matches the claimed channel embed regex - description = message.embeds[0].description - if (description is not None) and (match := CLAIMED_BY_RE.match(description)): - await _caches.claimants.set(channel.id, int(match.group("user_id"))) - return - - await bot.instance.get_channel(constants.Channels.helpers).send( - f"I couldn't find a claimant for {channel.mention} in that last 1000 messages. " - "Please use your helper powers to close the channel if/when appropriate." - ) - await _caches.claimants.set(channel.id, bot.instance.user.id) + + # Don't apply close logic if the post was archived by the bot, as it + # would have been done so via _close_help_thread. + if thread_update.user.id == bot.instance.user.id: + return + + await _close_help_thread(archived_thread, _stats.ClosingReason.INACTIVE) + + +async def help_thread_deleted(deleted_thread_event: discord.RawThreadDeleteEvent) -> None: + """Record appropriate stats when a help thread is deleted.""" + _stats.report_post_count() + cached_thread = deleted_thread_event.thread + if cached_thread and not cached_thread.archived: + # If the thread is in the bot's cache, and it was not archived before deleting, report a complete session. + await _stats.report_complete_session(cached_thread, _stats.ClosingReason.DELETED) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 31a33f8afc..6423f6f2f2 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -1,656 +1,74 @@ -import asyncio -import random -import typing as t -from datetime import timedelta -from operator import attrgetter +"""Contains the Cog that receives discord.py events and defers most actions to other files in the module.""" -import arrow import discord -import discord.abc -from botcore.utils import members, scheduling from discord.ext import commands from bot import constants from bot.bot import Bot -from bot.constants import Channels, RedirectOutput -from bot.exts.help_channels import _caches, _channel, _message, _name, _stats +from bot.exts.help_channels import _caches, _channel, _message from bot.log import get_logger -from bot.utils import channel as channel_utils, lock log = get_logger(__name__) -NAMESPACE = "help" -HELP_CHANNEL_TOPIC = """ -This is a Python help channel. You can claim your own help channel in the Python Help: Available category. -""" -AVAILABLE_HELP_CHANNELS = "**Currently available help channel(s):** {available}" - -class HelpChannels(commands.Cog): +class HelpForum(commands.Cog): """ - Manage the help channel system of the guild. - - The system is based on a 3-category system: - - Available Category - - * Contains channels which are ready to be occupied by someone who needs help - * Will always contain `constants.HelpChannels.max_available` channels; refilled automatically - from the pool of dormant channels - * Prioritise using the channels which have been dormant for the longest amount of time - * If there are no more dormant channels, the bot will automatically create a new one - * If there are no dormant channels to move, helpers will be notified (see `notify()`) - * When a channel becomes available, the dormant embed will be edited to show `AVAILABLE_MSG` - * User can only claim a channel at an interval `constants.HelpChannels.claim_minutes` - * To keep track of cooldowns, user which claimed a channel will have a temporary role - - In Use Category - - * Contains all channels which are occupied by someone needing help - * Channel moves to dormant category after - - `constants.HelpChannels.idle_minutes_other` minutes since the last user message, or - - `constants.HelpChannels.idle_minutes_claimant` minutes since the last claimant message. - * Command can prematurely mark a channel as dormant - * Channel claimant is allowed to use the command - * Allowed roles for the command are configurable with `constants.HelpChannels.cmd_whitelist` - * When a channel becomes dormant, an embed with `DORMANT_MSG` will be sent + Manage the help channel forum of the guild. - Dormant Category + This system uses Discord's native forum channel feature to handle most of the logic. - * Contains channels which aren't in use - * Channels are used to refill the Available category - - Help channels are named after the foods in `bot/resources/foods.json`. + The purpose of this cog is to add additional features, such as stats collection, old post locking + and helpful automated messages. """ def __init__(self, bot: Bot): self.bot = bot - self.scheduler = scheduling.Scheduler(self.__class__.__name__) - - self.guild: discord.Guild = None - self.cooldown_role: discord.Role = None - - # Categories - self.available_category: discord.CategoryChannel = None - self.in_use_category: discord.CategoryChannel = None - self.dormant_category: discord.CategoryChannel = None - - # Queues - self.channel_queue: asyncio.Queue[discord.TextChannel] = None - self.name_queue: t.Deque[str] = None - - # Notifications - # Using a very old date so that we don't have to use Optional typing. - self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') - self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') - - self.dynamic_message: t.Optional[int] = None - self.available_help_channels: t.Set[discord.TextChannel] = set() - - # Asyncio stuff - self.queue_tasks: t.List[asyncio.Task] = [] - self.init_done = False - - async def cog_unload(self) -> None: - """Cancel the init task and scheduled tasks when the cog unloads.""" - log.trace("Cog unload: cancelling the init_cog task") - - log.trace("Cog unload: cancelling the channel queue tasks") - for task in self.queue_tasks: - task.cancel() - - self.scheduler.cancel_all() - - @lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id")) - @lock.lock_arg(NAMESPACE, "message", attrgetter("author.id")) - @lock.lock_arg(f"{NAMESPACE}.unclaim", "message", attrgetter("author.id"), wait=True) - async def claim_channel(self, message: discord.Message) -> None: - """ - Claim the channel in which the question `message` was sent. - - Send an embed stating the claimant, move the channel to the In Use category, and pin the `message`. - Add a cooldown to the claimant to prevent them from asking another question. - Lastly, make a new channel available. - """ - log.info(f"Channel #{message.channel} was claimed by `{message.author.id}`.") - - try: - await self.move_to_in_use(message.channel) - except discord.DiscordServerError: - try: - await message.channel.send( - "The bot encountered a Discord API error while trying to move this channel, please try again later." - ) - except Exception as e: - log.warning("Error occurred while sending fail claim message:", exc_info=e) - log.info( - "500 error from Discord when moving #%s (%d) to in-use for %s (%d). Cancelling claim.", - message.channel.name, - message.channel.id, - message.author.name, - message.author.id, - ) - self.bot.stats.incr("help.failed_claims.500_on_move") - return - - embed = discord.Embed( - description=f"Channel claimed by {message.author.mention}.", - color=constants.Colours.bright_green, - ) - await message.channel.send(embed=embed) - - # Handle odd edge case of `message.author` not being a `discord.Member` (see bot#1839) - if not isinstance(message.author, discord.Member): - log.debug(f"{message.author} ({message.author.id}) isn't a member. Not giving cooldown role or sending DM.") - else: - await members.handle_role_change(message.author, message.author.add_roles, self.cooldown_role) - - try: - await _message.dm_on_open(message) - except Exception as e: - log.warning("Error occurred while sending DM:", exc_info=e) - - await _message.pin(message) - - # Add user with channel for dormant check. - await _caches.claimants.set(message.channel.id, message.author.id) - - self.bot.stats.incr("help.claimed") - - # datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time. - timestamp = arrow.Arrow.fromdatetime(message.created_at).timestamp() - - await _caches.claim_times.set(message.channel.id, timestamp) - await _caches.claimant_last_message_times.set(message.channel.id, timestamp) - # Delete to indicate that the help session has yet to receive an answer. - await _caches.non_claimant_last_message_times.delete(message.channel.id) - - # Removing the help channel from the dynamic message, and editing/sending that message. - self.available_help_channels.remove(message.channel) - - # Not awaited because it may indefinitely hold the lock while waiting for a channel. - scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}") - - def create_channel_queue(self) -> asyncio.Queue: - """ - Return a queue of dormant channels to use for getting the next available channel. - - The channels are added to the queue in a random order. - """ - log.trace("Creating the channel queue.") - - channels = list(_channel.get_category_channels(self.dormant_category)) - random.shuffle(channels) - - log.trace("Populating the channel queue with channels.") - queue = asyncio.Queue() - for channel in channels: - queue.put_nowait(channel) - - return queue - - async def create_dormant(self) -> t.Optional[discord.TextChannel]: - """ - Create and return a new channel in the Dormant category. - - The new channel will sync its permission overwrites with the category. - - Return None if no more channel names are available. - """ - log.trace("Getting a name for a new dormant channel.") - - try: - name = self.name_queue.popleft() - except IndexError: - log.debug("No more names available for new dormant channels.") - return None - - log.debug(f"Creating a new dormant channel named {name}.") - return await self.dormant_category.create_text_channel(name, topic=HELP_CHANNEL_TOPIC) + self.help_forum_channel_id = constants.Channels.help_system_forum async def close_check(self, ctx: commands.Context) -> bool: - """Return True if the channel is in use and the user is the claimant or has a whitelisted role.""" - if ctx.channel.category != self.in_use_category: - log.debug(f"{ctx.author} invoked command 'close' outside an in-use help channel") + """Return True if the channel is a help post, and the user is the claimant or has a whitelisted role.""" + if not _channel.is_help_forum_post(ctx.channel): return False - if await _caches.claimants.get(ctx.channel.id) == ctx.author.id: + if ctx.author.id == ctx.channel.owner_id: log.trace(f"{ctx.author} is the help channel claimant, passing the check for dormant.") self.bot.stats.incr("help.dormant_invoke.claimant") return True log.trace(f"{ctx.author} is not the help channel claimant, checking roles.") has_role = await commands.has_any_role(*constants.HelpChannels.cmd_whitelist).predicate(ctx) - if has_role: self.bot.stats.incr("help.dormant_invoke.staff") - return has_role - @commands.command(name="close", aliases=["dormant", "solved"], enabled=False) + @commands.group(name="help-forum") + async def help_forum_group(self, ctx: commands.Context) -> None: + """A group of commands that help manage our help forum system.""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @help_forum_group.command(name="close", root_aliases=("close", "dormant", "solved")) async def close_command(self, ctx: commands.Context) -> None: """ - Make the current in-use help channel dormant. + Make the help post this command was called in dormant. May only be invoked by the channel's claimant or by staff. """ # Don't use a discord.py check because the check needs to fail silently. if await self.close_check(ctx): log.info(f"Close command invoked by {ctx.author} in #{ctx.channel}.") - await self.unclaim_channel(ctx.channel, closed_on=_channel.ClosingReason.COMMAND) - - async def get_available_candidate(self) -> discord.TextChannel: - """ - Return a dormant channel to turn into an available channel. - - If no channel is available, wait indefinitely until one becomes available. - """ - log.trace("Getting an available channel candidate.") - - try: - channel = self.channel_queue.get_nowait() - except asyncio.QueueEmpty: - log.info("No candidate channels in the queue; creating a new channel.") - channel = await self.create_dormant() - - if not channel: - log.info("Couldn't create a candidate channel; waiting to get one from the queue.") - last_notification = await _message.notify_none_remaining(self.last_none_remaining_notification) - - if last_notification: - self.last_none_remaining_notification = last_notification - - channel = await self.wait_for_dormant_channel() # Blocks until a new channel is available - - else: - last_notification = await _message.notify_running_low( - self.channel_queue.qsize(), - self.last_running_low_notification - ) - - if last_notification: - self.last_running_low_notification = last_notification - - return channel - - async def init_available(self) -> None: - """Initialise the Available category with channels.""" - log.trace("Initialising the Available category with channels.") - - channels = list(_channel.get_category_channels(self.available_category)) - missing = constants.HelpChannels.max_available - len(channels) - - # If we've got less than `max_available` channel available, we should add some. - if missing > 0: - log.trace(f"Moving {missing} missing channels to the Available category.") - for _ in range(missing): - await self.move_to_available() - - # If for some reason we have more than `max_available` channels available, - # we should move the superfluous ones over to dormant. - elif missing < 0: - log.trace(f"Moving {abs(missing)} superfluous available channels over to the Dormant category.") - for channel in channels[:abs(missing)]: - await self.unclaim_channel(channel, closed_on=_channel.ClosingReason.CLEANUP) - - self.available_help_channels = set(_channel.get_category_channels(self.available_category)) - - # Getting channels that need to be included in the dynamic message. - await self.update_available_help_channels() - log.trace("Dynamic available help message updated.") - - async def init_categories(self) -> None: - """Get the help category objects. Remove the cog if retrieval fails.""" - log.trace("Getting the CategoryChannel objects for the help categories.") - - try: - self.available_category = await channel_utils.get_or_fetch_channel( - constants.Categories.help_available - ) - self.in_use_category = await channel_utils.get_or_fetch_channel( - constants.Categories.help_in_use - ) - self.dormant_category = await channel_utils.get_or_fetch_channel( - constants.Categories.help_dormant - ) - except discord.HTTPException: - log.exception("Failed to get a category; cog will be removed") - await self.bot.remove_cog(self.qualified_name) - - async def cog_load(self) -> None: - """Initialise the help channel system.""" - log.trace("Waiting for the guild to be available before initialisation.") - await self.bot.wait_until_guild_available() - - log.trace("Initialising the cog.") - self.guild = self.bot.get_guild(constants.Guild.id) - self.cooldown_role = self.guild.get_role(constants.Roles.help_cooldown) - - await self.init_categories() - - self.channel_queue = self.create_channel_queue() - self.name_queue = _name.create_name_queue( - self.available_category, - self.in_use_category, - self.dormant_category, - ) - - log.trace("Moving or rescheduling in-use channels.") - for channel in _channel.get_category_channels(self.in_use_category): - await _channel.ensure_cached_claimant(channel) - await self.move_idle_channel(channel, has_task=False) - - # Prevent the command from being used until ready. - # The ready event wasn't used because channels could change categories between the time - # the command is invoked and the cog is ready (e.g. if move_idle_channel wasn't called yet). - # This may confuse users. So would potentially long delays for the cog to become ready. - self.close_command.enabled = True - - # Acquiring the dynamic message ID, if it exists within the cache. - log.trace("Attempting to fetch How-to-get-help dynamic message ID.") - self.dynamic_message = await _caches.dynamic_message.get("message_id") - - await self.init_available() - _stats.report_counts() - - self.init_done = True - log.info("Cog is ready!") - - async def move_idle_channel(self, channel: discord.TextChannel, has_task: bool = True) -> None: - """ - Make the `channel` dormant if idle or schedule the move if still active. - - If `has_task` is True and rescheduling is required, the extant task to make the channel - dormant will first be cancelled. - """ - log.trace(f"Handling in-use channel #{channel} ({channel.id}).") - - closing_time, closed_on = await _channel.get_closing_time(channel, self.init_done) - - # Closing time is in the past. - # Add 1 second due to POSIX timestamps being lower resolution than datetime objects. - if closing_time < (arrow.utcnow() + timedelta(seconds=1)): - log.info( - f"#{channel} ({channel.id}) is idle past {closing_time} " - f"and will be made dormant. Reason: {closed_on.value}" - ) - - await self.unclaim_channel(channel, closed_on=closed_on) - else: - # Cancel the existing task, if any. - if has_task: - self.scheduler.cancel(channel.id) + await _channel.help_thread_closed(ctx.channel) - delay = (closing_time - arrow.utcnow()).seconds - log.info( - f"#{channel} ({channel.id}) is still active; " - f"scheduling it to be moved after {delay} seconds." - ) - - self.scheduler.schedule_later(delay, channel.id, self.move_idle_channel(channel)) - - async def move_to_available(self) -> None: - """Make a channel available.""" - log.trace("Making a channel available.") - - channel = await self.get_available_candidate() - channel_str = f"#{channel} ({channel.id})" - log.info(f"Making {channel_str} available.") - - await _message.send_available_message(channel) - - log.trace(f"Moving {channel_str} to the Available category.") - - # Unpin any previously stuck pins - log.trace(f"Looking for pins stuck in {channel_str}.") - if stuck_pins := await _message.unpin_all(channel): - log.debug(f"Removed {stuck_pins} stuck pins from {channel_str}.") - - await _channel.move_to_bottom( - channel=channel, - category_id=constants.Categories.help_available, - ) - - # Adding the help channel to the dynamic message, and editing/sending that message. - self.available_help_channels.add(channel) - await self.update_available_help_channels() - - _stats.report_counts() - - async def move_to_dormant(self, channel: discord.TextChannel) -> None: - """Make the `channel` dormant.""" - log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") - await _channel.move_to_bottom( - channel=channel, - category_id=constants.Categories.help_dormant, - ) - - log.trace(f"Sending dormant message for #{channel} ({channel.id}).") - embed = discord.Embed( - description=_message.DORMANT_MSG.format( - dormant=self.dormant_category.name, - available=self.available_category.name, - ) - ) - await channel.send(embed=embed) - - log.trace(f"Pushing #{channel} ({channel.id}) into the channel queue.") - self.channel_queue.put_nowait(channel) - - _stats.report_counts() - - @lock.lock_arg(f"{NAMESPACE}.unclaim", "channel") - async def unclaim_channel(self, channel: discord.TextChannel, *, closed_on: _channel.ClosingReason) -> None: - """ - Unclaim an in-use help `channel` to make it dormant. - - Unpin the claimant's question message and move the channel to the Dormant category. - Remove the cooldown role from the channel claimant if they have no other channels claimed. - Cancel the scheduled cooldown role removal task. - - `closed_on` is the reason that the channel was closed. See _channel.ClosingReason for possible values. - """ - claimant_id = await _caches.claimants.get(channel.id) - _unclaim_channel = self._unclaim_channel - - # It could be possible that there is no claimant cached. In such case, it'd be useless and - # possibly incorrect to lock on None. Therefore, the lock is applied conditionally. - if claimant_id is not None: - decorator = lock.lock_arg(f"{NAMESPACE}.unclaim", "claimant_id", wait=True) - _unclaim_channel = decorator(_unclaim_channel) - - return await _unclaim_channel(channel, claimant_id, closed_on) - - async def _unclaim_channel( - self, - channel: discord.TextChannel, - claimant_id: t.Optional[int], - closed_on: _channel.ClosingReason - ) -> None: - """Actual implementation of `unclaim_channel`. See that for full documentation.""" - await _caches.claimants.delete(channel.id) - await _caches.session_participants.delete(channel.id) - - if not claimant_id: - log.info("No claimant given when un-claiming %s (%d). Skipping role removal.", channel, channel.id) - else: - claimant = await members.get_or_fetch_member(self.guild, claimant_id) - if claimant is None: - log.info(f"{claimant_id} left the guild during their help session; the cooldown role won't be removed") - else: - await members.handle_role_change(claimant, claimant.remove_roles, self.cooldown_role) - - await _message.unpin_all(channel) - await _stats.report_complete_session(channel.id, closed_on) - await self.move_to_dormant(channel) - - # Cancel the task that makes the channel dormant only if called by the close command. - # In other cases, the task is either already done or not-existent. - if closed_on == _channel.ClosingReason.COMMAND: - self.scheduler.cancel(channel.id) - - async def move_to_in_use(self, channel: discord.TextChannel) -> None: - """Make a channel in-use and schedule it to be made dormant.""" - log.info(f"Moving #{channel} ({channel.id}) to the In Use category.") - - await _channel.move_to_bottom( - channel=channel, - category_id=constants.Categories.help_in_use, - ) - - timeout = constants.HelpChannels.idle_minutes_claimant * 60 - - log.trace(f"Scheduling #{channel} ({channel.id}) to become dormant in {timeout} sec.") - self.scheduler.schedule_later(timeout, channel.id, self.move_idle_channel(channel)) - _stats.report_counts() - - @commands.Cog.listener() - async def on_message(self, message: discord.Message) -> None: - """Move an available channel to the In Use category and replace it with a dormant one.""" - if message.author.bot: - return # Ignore messages sent by bots. - - if channel_utils.is_in_category(message.channel, constants.Categories.help_available): - if not _channel.is_excluded_channel(message.channel): - await self.claim_channel(message) - - elif channel_utils.is_in_category(message.channel, constants.Categories.help_in_use): - await self.notify_session_participants(message) - await _message.update_message_caches(message) - - @commands.Cog.listener() - async def on_message_delete(self, msg: discord.Message) -> None: - """ - Reschedule an in-use channel to become dormant sooner if the channel is empty. - - The new time for the dormant task is configured with `HelpChannels.deleted_idle_minutes`. - """ - if not channel_utils.is_in_category(msg.channel, constants.Categories.help_in_use): - return - - if not await _message.is_empty(msg.channel): - return - - log.info(f"Claimant of #{msg.channel} ({msg.author}) deleted message, channel is empty now. Rescheduling task.") - - # Cancel existing dormant task before scheduling new. - self.scheduler.cancel(msg.channel.id) - - delay = constants.HelpChannels.deleted_idle_minutes * 60 - self.scheduler.schedule_later(delay, msg.channel.id, self.move_idle_channel(msg.channel)) - - async def wait_for_dormant_channel(self) -> discord.TextChannel: - """Wait for a dormant channel to become available in the queue and return it.""" - log.trace("Waiting for a dormant channel.") - - task = scheduling.create_task(self.channel_queue.get()) - self.queue_tasks.append(task) - channel = await task - - log.trace(f"Channel #{channel} ({channel.id}) finally retrieved from the queue.") - self.queue_tasks.remove(task) - - return channel - - async def update_available_help_channels(self) -> None: - """Updates the dynamic message within #how-to-get-help for available help channels.""" - available_channels = AVAILABLE_HELP_CHANNELS.format( - available=", ".join( - c.mention for c in sorted(self.available_help_channels, key=attrgetter("position")) - ) or None - ) - - if self.dynamic_message is not None: - try: - log.trace("Help channels have changed, dynamic message has been edited.") - await discord.PartialMessage( - channel=self.bot.get_channel(constants.Channels.how_to_get_help), - id=self.dynamic_message, - ).edit(content=available_channels) - except discord.NotFound: - pass - else: - return - - log.trace("Dynamic message could not be edited or found. Creating a new one.") - new_dynamic_message = await self.bot.get_channel(constants.Channels.how_to_get_help).send(available_channels) - self.dynamic_message = new_dynamic_message.id - await _caches.dynamic_message.set("message_id", self.dynamic_message) - - @staticmethod - def _serialise_session_participants(participants: set[int]) -> str: - """Convert a set to a comma separated string.""" - return ','.join(str(p) for p in participants) - - @staticmethod - def _deserialise_session_participants(s: str) -> set[int]: - """Convert a comma separated string into a set.""" - return set(int(user_id) for user_id in s.split(",") if user_id != "") - - @lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id")) - @lock.lock_arg(NAMESPACE, "message", attrgetter("author.id")) - async def notify_session_participants(self, message: discord.Message) -> None: - """ - Check if the message author meets the requirements to be notified. - - If they meet the requirements they are notified. - """ - if await _caches.claimants.get(message.channel.id) == message.author.id: - return # Ignore messages sent by claimants - - if not await _caches.help_dm.get(message.author.id): - return # Ignore message if user is opted out of help dms - - if (await self.bot.get_context(message)).command == self.close_command: - return # Ignore messages that are closing the channel - - session_participants = self._deserialise_session_participants( - await _caches.session_participants.get(message.channel.id) or "" - ) - - if message.author.id not in session_participants: - session_participants.add(message.author.id) - - embed = discord.Embed( - title="Currently Helping", - description=f"You're currently helping in {message.channel.mention}", - color=constants.Colours.bright_green, - timestamp=message.created_at - ) - embed.add_field(name="Conversation", value=f"[Jump to message]({message.jump_url})") - - try: - await message.author.send(embed=embed) - except discord.Forbidden: - log.trace( - f"Failed to send helpdm message to {message.author.id}. DMs Closed/Blocked. " - "Removing user from helpdm." - ) - bot_commands_channel = self.bot.get_channel(Channels.bot_commands) - await _caches.help_dm.delete(message.author.id) - await bot_commands_channel.send( - f"{message.author.mention} {constants.Emojis.cross_mark} " - "To receive updates on help channels you're active in, enable your DMs.", - delete_after=RedirectOutput.delete_delay - ) - return - - await _caches.session_participants.set( - message.channel.id, - self._serialise_session_participants(session_participants) - ) - - @commands.command(name="helpdm") - async def helpdm_command( + @help_forum_group.command(name="dm", root_aliases=("helpdm",)) + async def help_dm_command( self, ctx: commands.Context, - state_bool: bool + state_bool: bool, ) -> None: """ - Allows user to toggle "Helping" dms. + Allows user to toggle "Helping" DMs. If this is set to on the user will receive a dm for the channel they are participating in. - If this is set to off the user will not receive a dm for channel that they are participating in. """ state_str = "ON" if state_bool else "OFF" @@ -664,3 +82,47 @@ async def helpdm_command( else: await _caches.help_dm.delete(ctx.author.id) await ctx.send(f"{constants.Emojis.ok_hand} {ctx.author.mention} Help DMs {state_str}!") + + @help_forum_group.command(name="title", root_aliases=("title",)) + async def rename_help_post(self, ctx: commands.Context, *, title: str) -> None: + """Rename the help post to the provided title.""" + if not _channel.is_help_forum_post(ctx.channel): + # Silently fail in channels other than help posts + return + + if not await commands.has_any_role(constants.Roles.helpers).predicate(ctx): + # Silently fail for non-helpers + return + + await ctx.channel.edit(name=title) + + @commands.Cog.listener() + async def on_thread_create(self, thread: discord.Thread) -> None: + """Defer application of new post logic for posts the help forum to the _channel helper.""" + if thread.parent_id == self.help_forum_channel_id: + await _channel.help_thread_opened(thread) + + @commands.Cog.listener() + async def on_thread_update(self, before: discord.Thread, after: discord.Thread) -> None: + """Defer application archive logic for posts in the help forum to the _channel helper.""" + if after.parent_id != self.help_forum_channel_id: + return + if not before.archived and after.archived: + await _channel.help_thread_archived(after) + + @commands.Cog.listener() + async def on_raw_thread_delete(self, deleted_thread_event: discord.RawThreadDeleteEvent) -> None: + """Defer application of new post logic for posts the help forum to the _channel helper.""" + if deleted_thread_event.parent_id == self.help_forum_channel_id: + await _channel.help_thread_deleted(deleted_thread_event) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + """Defer application of new message logic for messages in the help forum to the _message helper.""" + if not _channel.is_help_forum_post(message.channel): + return None + + await _message.notify_session_participants(message) + + if message.author.id != message.channel.owner_id: + await _caches.posts_with_non_claimant_messages.set(message.channel.id, "sentinel") diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 00d57ea40b..98bfe59b8d 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -1,286 +1,73 @@ -import textwrap -import typing as t +from operator import attrgetter -import arrow import discord -from arrow import Arrow import bot from bot import constants from bot.exts.help_channels import _caches from bot.log import get_logger +from bot.utils import lock log = get_logger(__name__) +NAMESPACE = "help" -ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" -AVAILABLE_MSG = f""" -Send your question here to claim the channel. +def _serialise_session_participants(participants: set[int]) -> str: + """Convert a set to a comma separated string.""" + return ','.join(str(p) for p in participants) -**Remember to:** -• **Ask** your Python question, not if you can ask or if there's an expert who can help. -• **Show** a code sample as text (rather than a screenshot) and the error message, if you got one. -• **Explain** what you expect to happen and what actually happens. -For more tips, check out our guide on [asking good questions]({ASKING_GUIDE_URL}). -""" +def _deserialise_session_participants(s: str) -> set[int]: + """Convert a comma separated string into a set.""" + return set(int(user_id) for user_id in s.split(",") if user_id != "") -AVAILABLE_TITLE = "Available help channel" -AVAILABLE_FOOTER = f"Closes after a period of inactivity, or when you send {constants.Bot.prefix}close." - -DORMANT_MSG = f""" -This help channel has been marked as **dormant**, and has been moved into the **{{dormant}}** \ -category at the bottom of the channel list. It is no longer possible to send messages in this \ -channel until it becomes available again. - -If your question wasn't answered yet, you can claim a new help channel from the \ -**{{available}}** category by simply asking your question again. Consider rephrasing the \ -question to maximize your chance of getting a good answer. If you're not sure how, have a look \ -through our guide for **[asking a good question]({ASKING_GUIDE_URL})**. -""" - - -async def update_message_caches(message: discord.Message) -> None: - """Checks the source of new content in a help channel and updates the appropriate cache.""" - channel = message.channel - - log.trace(f"Checking if #{channel} ({channel.id}) has had a reply.") - - claimant_id = await _caches.claimants.get(channel.id) - if not claimant_id: - # The mapping for this channel doesn't exist, we can't do anything. - return - - # datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time. - timestamp = Arrow.fromdatetime(message.created_at).timestamp() - - # Overwrite the appropriate last message cache depending on the author of the message - if message.author.id == claimant_id: - await _caches.claimant_last_message_times.set(channel.id, timestamp) - else: - await _caches.non_claimant_last_message_times.set(channel.id, timestamp) - - -async def get_last_message(channel: discord.TextChannel) -> t.Optional[discord.Message]: - """Return the last message sent in the channel or None if no messages exist.""" - log.trace(f"Getting the last message in #{channel} ({channel.id}).") - - async for message in channel.history(limit=1): - return message - - log.debug(f"No last message available; #{channel} ({channel.id}) has no messages.") - return None - - -async def is_empty(channel: discord.TextChannel) -> bool: - """Return True if there's an AVAILABLE_MSG and the messages leading up are bot messages.""" - log.trace(f"Checking if #{channel} ({channel.id}) is empty.") - - # A limit of 100 results in a single API call. - # If AVAILABLE_MSG isn't found within 100 messages, then assume the channel is not empty. - # Not gonna do an extensive search for it cause it's too expensive. - async for msg in channel.history(limit=100): - if not msg.author.bot: - log.trace(f"#{channel} ({channel.id}) has a non-bot message.") - return False - - if _match_bot_embed(msg, AVAILABLE_MSG): - log.trace(f"#{channel} ({channel.id}) has the available message embed.") - return True - - return False - - -async def dm_on_open(message: discord.Message) -> None: - """ - DM claimant with a link to the claimed channel's first message, with a 100 letter preview of the message. - - Does nothing if the user has DMs disabled. - """ - embed = discord.Embed( - title="Help channel opened", - description=f"You claimed {message.channel.mention}.", - colour=bot.constants.Colours.bright_green, - timestamp=message.created_at, - ) - - embed.set_thumbnail(url=constants.Icons.green_questionmark) - formatted_message = textwrap.shorten(message.content, width=100, placeholder="...") - if formatted_message: - embed.add_field(name="Your message", value=formatted_message, inline=False) - embed.add_field( - name="Conversation", - value=f"[Jump to message!]({message.jump_url})", - inline=False, - ) - - try: - await message.author.send(embed=embed) - log.trace(f"Sent DM to {message.author.id} after claiming help channel.") - except discord.errors.Forbidden: - log.trace( - f"Ignoring to send DM to {message.author.id} after claiming help channel: DMs disabled." - ) - - -async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: - """ - Send a pinging message in `channel` notifying about there being no dormant channels remaining. - - If a notification was sent, return the time at which the message was sent. - Otherwise, return None. - - Configuration: - * `HelpChannels.notify_minutes` - minimum interval between notifications - * `HelpChannels.notify_none_remaining` - toggle none_remaining notifications - * `HelpChannels.notify_none_remaining_roles` - roles mentioned in notifications - """ - if not constants.HelpChannels.notify_none_remaining: - return None - - if (arrow.utcnow() - last_notification).total_seconds() < (constants.HelpChannels.notify_minutes * 60): - log.trace("Did not send none_remaining notification as it hasn't been enough time since the last one.") - return None - - log.trace("Notifying about lack of channels.") - - mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) - allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] - - channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) - if channel is None: - log.trace("Did not send none_remaining notification as the notification channel couldn't be gathered.") - - try: - await channel.send( - f"{mentions} A new available help channel is needed but there " - "are no more dormant ones. Consider freeing up some in-use channels manually by " - f"using the `{constants.Bot.prefix}dormant` command within the channels.", - allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles) - ) - except Exception: - # Handle it here cause this feature isn't critical for the functionality of the system. - log.exception("Failed to send notification about lack of dormant channels!") - else: - bot.instance.stats.incr("help.out_of_channel_alerts") - return arrow.utcnow() - - -async def notify_running_low(number_of_channels_left: int, last_notification: Arrow) -> t.Optional[Arrow]: +@lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id")) +@lock.lock_arg(NAMESPACE, "message", attrgetter("author.id")) +async def notify_session_participants(message: discord.Message) -> None: """ - Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. - - This will include the number of dormant channels left `number_of_channels_left` + Check if the message author meets the requirements to be notified. - If a notification was sent, return the time at which the message was sent. - Otherwise, return None. - - Configuration: - * `HelpChannels.notify_minutes` - minimum interval between notifications - * `HelpChannels.notify_running_low` - toggle running_low notifications - * `HelpChannels.notify_running_low_threshold` - minimum amount of channels to trigger running_low notifications + If they meet the requirements they are notified. """ - if not constants.HelpChannels.notify_running_low: - return None - - if number_of_channels_left > constants.HelpChannels.notify_running_low_threshold: - log.trace("Did not send notify_running_low notification as the threshold was not met.") - return None - - if (arrow.utcnow() - last_notification).total_seconds() < (constants.HelpChannels.notify_minutes * 60): - log.trace("Did not send notify_running_low notification as it hasn't been enough time since the last one.") - return None - - log.trace("Notifying about getting close to no dormant channels.") - - channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) - if channel is None: - log.trace("Did not send notify_running notification as the notification channel couldn't be gathered.") - - try: - if number_of_channels_left == 1: - message = f"There is only {number_of_channels_left} dormant channel left. " - else: - message = f"There are only {number_of_channels_left} dormant channels left. " - message += "Consider participating in some help channels so that we don't run out." - await channel.send(message) - except Exception: - # Handle it here cause this feature isn't critical for the functionality of the system. - log.exception("Failed to send notification about running low of dormant channels!") - else: - bot.instance.stats.incr("help.running_low_alerts") - return arrow.utcnow() - - -async def pin(message: discord.Message) -> None: - """Pin an initial question `message`.""" - await _pin_wrapper(message, pin=True) + if message.channel.owner_id == message.author.id: + return # Ignore messages sent by claimants + if not await _caches.help_dm.get(message.author.id): + return # Ignore message if user is opted out of help dms -async def send_available_message(channel: discord.TextChannel) -> None: - """Send the available message by editing a dormant message or sending a new message.""" - channel_info = f"#{channel} ({channel.id})" - log.trace(f"Sending available message in {channel_info}.") - - embed = discord.Embed( - color=constants.Colours.bright_green, - description=AVAILABLE_MSG, + session_participants = _deserialise_session_participants( + await _caches.session_participants.get(message.channel.id) or "", ) - embed.set_author(name=AVAILABLE_TITLE, icon_url=constants.Icons.green_checkmark) - embed.set_footer(text=AVAILABLE_FOOTER) - - msg = await get_last_message(channel) - if _match_bot_embed(msg, DORMANT_MSG): - log.trace(f"Found dormant message {msg.id} in {channel_info}; editing it.") - await msg.edit(embed=embed) - else: - log.trace(f"Dormant message not found in {channel_info}; sending a new message.") - await channel.send(embed=embed) - - -async def unpin_all(channel: discord.TextChannel) -> int: - """Unpin all pinned messages in `channel` and return the amount of unpinned messages.""" - count = 0 - for message in await channel.pins(): - if await _pin_wrapper(message, pin=False): - count += 1 - - return count - - -def _match_bot_embed(message: t.Optional[discord.Message], description: str) -> bool: - """Return `True` if the bot's `message`'s embed description matches `description`.""" - if not message or not message.embeds: - return False - - bot_msg_desc = message.embeds[0].description - if bot_msg_desc is None: - log.trace("Last message was a bot embed but it was empty.") - return False - return message.author == bot.instance.user and bot_msg_desc.strip() == description.strip() + if message.author.id not in session_participants: + session_participants.add(message.author.id) -async def _pin_wrapper(message: discord.Message, *, pin: bool) -> bool: - """ - Pin `message` if `pin` is True or unpin if it's False. - - Return True if successful and False otherwise. - """ - channel_str = f"#{message.channel} ({message.channel.id})" - func = message.pin if pin else message.unpin - - try: - await func() - except discord.HTTPException as e: - if e.code == 10008: - log.debug(f"Message {message.id} in {channel_str} doesn't exist; can't {func.__name__}.") - else: - log.exception( - f"Error {func.__name__}ning message {message.id} in {channel_str}: " - f"{e.status} ({e.code})" + embed = discord.Embed( + title="Currently Helping", + description=f"You're currently helping in {message.channel.mention}", + color=constants.Colours.bright_green, + timestamp=message.created_at, + ) + embed.add_field(name="Conversation", value=f"[Jump to message]({message.jump_url})") + + try: + await message.author.send(embed=embed) + except discord.Forbidden: + log.trace( + f"Failed to send help dm message to {message.author.id}. DMs Closed/Blocked. " + "Removing user from help dm." + ) + await _caches.help_dm.delete(message.author.id) + bot_commands_channel = bot.instance.get_channel(constants.Channels.bot_commands) + await bot_commands_channel.send( + f"{message.author.mention} {constants.Emojis.cross_mark} " + "To receive updates on help channels you're active in, enable your DMs.", + delete_after=constants.RedirectOutput.delete_delay, ) - return False - else: - log.trace(f"{func.__name__.capitalize()}ned message {message.id} in {channel_str}.") - return True + return + + await _caches.session_participants.set( + message.channel.id, + _serialise_session_participants(session_participants), + ) diff --git a/bot/exts/help_channels/_name.py b/bot/exts/help_channels/_name.py deleted file mode 100644 index a9d9b2df14..0000000000 --- a/bot/exts/help_channels/_name.py +++ /dev/null @@ -1,69 +0,0 @@ -import json -import typing as t -from collections import deque -from pathlib import Path - -import discord - -from bot import constants -from bot.exts.help_channels._channel import MAX_CHANNELS_PER_CATEGORY, get_category_channels -from bot.log import get_logger - -log = get_logger(__name__) - - -def create_name_queue(*categories: discord.CategoryChannel) -> deque: - """ - Return a queue of food names to use for creating new channels. - - Skip names that are already in use by channels in `categories`. - """ - log.trace("Creating the food name queue.") - - used_names = _get_used_names(*categories) - - log.trace("Determining the available names.") - available_names = (name for name in _get_names() if name not in used_names) - - log.trace("Populating the name queue with names.") - return deque(available_names) - - -def _get_names() -> t.List[str]: - """ - Return a truncated list of prefixed food names. - - The amount of names is configured with `HelpChannels.max_total_channels`. - The prefix is configured with `HelpChannels.name_prefix`. - """ - count = constants.HelpChannels.max_total_channels - prefix = constants.HelpChannels.name_prefix - - log.trace(f"Getting the first {count} food names from JSON.") - - with Path("bot/resources/foods.json").open(encoding="utf-8") as foods_file: - all_names = json.load(foods_file) - - if prefix: - return [prefix + name for name in all_names[:count]] - else: - return all_names[:count] - - -def _get_used_names(*categories: discord.CategoryChannel) -> t.Set[str]: - """Return names which are already being used by channels in `categories`.""" - log.trace("Getting channel names which are already being used.") - - names = set() - for cat in categories: - for channel in get_category_channels(cat): - names.add(channel.name) - - if len(names) > MAX_CHANNELS_PER_CATEGORY: - log.warning( - f"Too many help channels ({len(names)}) already exist! " - f"Discord only supports {MAX_CHANNELS_PER_CATEGORY} in a category." - ) - - log.trace(f"Got {len(names)} used names: {names}") - return names diff --git a/bot/exts/help_channels/_stats.py b/bot/exts/help_channels/_stats.py index 4698c26de3..6c05b47018 100644 --- a/bot/exts/help_channels/_stats.py +++ b/bot/exts/help_channels/_stats.py @@ -1,40 +1,44 @@ -from more_itertools import ilen +from enum import Enum + +import arrow +import discord import bot from bot import constants -from bot.exts.help_channels import _caches, _channel from bot.log import get_logger log = get_logger(__name__) -def report_counts() -> None: - """Report channel count stats of each help category.""" - for name in ("in_use", "available", "dormant"): - id_ = getattr(constants.Categories, f"help_{name}") - category = bot.instance.get_channel(id_) +class ClosingReason(Enum): + """All possible closing reasons for help channels.""" + + COMMAND = "command" + INACTIVE = "auto.inactive" + DELETED = "auto.deleted" + CLEANUP = "auto.cleanup" + - if category: - total = ilen(_channel.get_category_channels(category)) - bot.instance.stats.gauge(f"help.total.{name}", total) - else: - log.warning(f"Couldn't find category {name!r} to track channel count stats.") +def report_post_count() -> None: + """Report post count stats of the help forum.""" + help_forum = bot.instance.get_channel(constants.Channels.help_system_forum) + bot.instance.stats.gauge("help_forum.total.in_use", len(help_forum.threads)) -async def report_complete_session(channel_id: int, closed_on: _channel.ClosingReason) -> None: +async def report_complete_session(help_session_post: discord.Thread, closed_on: ClosingReason) -> None: """ - Report stats for a completed help session channel `channel_id`. + Report stats for a completed help session post `help_session_post`. - `closed_on` is the reason why the channel was closed. See `_channel.ClosingReason` for possible reasons. + `closed_on` is the reason why the post was closed. See `ClosingReason` for possible reasons. """ - bot.instance.stats.incr(f"help.dormant_calls.{closed_on.value}") + bot.instance.stats.incr(f"help_forum.dormant_calls.{closed_on.value}") - in_use_time = await _channel.get_in_use_time(channel_id) - if in_use_time: - bot.instance.stats.timing("help.in_use_time", in_use_time) + open_time = discord.utils.snowflake_time(help_session_post.id) + in_use_time = arrow.utcnow() - open_time + bot.instance.stats.timing("help_forum.in_use_time", in_use_time) - non_claimant_last_message_time = await _caches.non_claimant_last_message_times.get(channel_id) - if non_claimant_last_message_time is None: - bot.instance.stats.incr("help.sessions.unanswered") + if set(help_session_post.members)-{help_session_post.owner_id} == set(): + # Can't use len(help_session_post.members) as the claimant (owner) may have left the thread. + bot.instance.stats.incr("help_forum.sessions.unanswered") else: - bot.instance.stats.incr("help.sessions.answered") + bot.instance.stats.incr("help_forum.sessions.answered") From a2b3c06b467a80f610488f95aca55e0abf1ca7e8 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 1 Nov 2022 18:53:40 +0000 Subject: [PATCH 08/50] Use help channel util from help channels cog This removes the need for the old helper entirely --- bot/exts/info/codeblock/_cog.py | 4 ++-- bot/utils/channel.py | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/bot/exts/info/codeblock/_cog.py b/bot/exts/info/codeblock/_cog.py index 9027105d9d..0605a26e7f 100644 --- a/bot/exts/info/codeblock/_cog.py +++ b/bot/exts/info/codeblock/_cog.py @@ -10,10 +10,10 @@ from bot.bot import Bot from bot.exts.filters.token_remover import TokenRemover from bot.exts.filters.webhook_remover import WEBHOOK_URL_RE +from bot.exts.help_channels._channel import is_help_forum_post from bot.exts.info.codeblock._instructions import get_instructions from bot.log import get_logger from bot.utils import has_lines -from bot.utils.channel import is_help_channel from bot.utils.messages import wait_for_deletion log = get_logger(__name__) @@ -98,7 +98,7 @@ def is_valid_channel(self, channel: discord.TextChannel) -> bool: """Return True if `channel` is a help channel, may be on a cooldown, or is whitelisted.""" log.trace(f"Checking if #{channel} qualifies for code block detection.") return ( - is_help_channel(channel) + is_help_forum_post(channel) or channel.id in self.channel_cooldowns or channel.id in constants.CodeBlock.channel_whitelist ) diff --git a/bot/utils/channel.py b/bot/utils/channel.py index 954a10e562..821a3732ab 100644 --- a/bot/utils/channel.py +++ b/bot/utils/channel.py @@ -4,20 +4,11 @@ import bot from bot import constants -from bot.constants import Categories from bot.log import get_logger log = get_logger(__name__) -def is_help_channel(channel: discord.TextChannel) -> bool: - """Return True if `channel` is in one of the help categories (excluding dormant).""" - log.trace(f"Checking if #{channel} is a help channel.") - categories = (Categories.help_available, Categories.help_in_use) - - return any(is_in_category(channel, category) for category in categories) - - def is_mod_channel(channel: Union[discord.TextChannel, discord.Thread]) -> bool: """True if channel, or channel.parent for threads, is considered a mod channel.""" if isinstance(channel, discord.Thread): From a152396ad2d449b0338a89cd40d61bee498806fd Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 13 Nov 2022 13:05:55 +0000 Subject: [PATCH 09/50] Use helper util to determine if snekbox is being invoked in a help post --- bot/exts/utils/snekbox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 5e217a2885..1909569595 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -13,8 +13,9 @@ from discord.ext.commands import Cog, Command, Context, Converter, command, guild_only from bot.bot import Bot -from bot.constants import Categories, Channels, MODERATION_ROLES, Roles, URLs +from bot.constants import Channels, MODERATION_ROLES, Roles, URLs from bot.decorators import redirect_output +from bot.exts.help_channels._channel import is_help_forum_post from bot.log import get_logger from bot.utils import send_to_paste_service from bot.utils.lock import LockedResourceError, lock_arg @@ -456,7 +457,7 @@ async def run_job( else: self.bot.stats.incr("snekbox_usages.roles.developers") - if ctx.channel.category_id == Categories.help_in_use: + if is_help_forum_post(ctx.channel): self.bot.stats.incr("snekbox_usages.channels.help") elif ctx.channel.id == Channels.bot_commands: self.bot.stats.incr("snekbox_usages.channels.bot_commands") From 2c2924c46cd6db8aa1fc9024c850287c92c5b8bc Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 13 Nov 2022 13:06:50 +0000 Subject: [PATCH 10/50] Remove modlog ignore for help chanels This was due to the hlep channels causing many events top be pushed to modlog due to how the old system worked. Now that we use a forum chanel, this is no longer the case. --- bot/exts/moderation/modlog.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index efa87ce251..a1ed714be5 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -16,7 +16,7 @@ from sentry_sdk import add_breadcrumb from bot.bot import Bot -from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs +from bot.constants import Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs from bot.log import get_logger from bot.utils import time from bot.utils.messages import format_user @@ -209,12 +209,6 @@ async def on_guild_channel_update(self, before: GUILD_CHANNEL, after: GuildChann self._ignored[Event.guild_channel_update].remove(before.id) return - # Two channel updates are sent for a single edit: 1 for topic and 1 for category change. - # TODO: remove once support is added for ignoring multiple occurrences for the same channel. - help_categories = (Categories.help_available, Categories.help_dormant, Categories.help_in_use) - if after.category and after.category.id in help_categories: - return - diff = DeepDiff(before, after) changes = [] done = [] From 13b9e28848d03d1750b96bcbde3a6988ee429aa6 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 13 Nov 2022 15:03:51 +0000 Subject: [PATCH 11/50] Use a redis cache to determine if a help session was answered --- bot/exts/help_channels/_caches.py | 4 ++++ bot/exts/help_channels/_stats.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bot/exts/help_channels/_caches.py b/bot/exts/help_channels/_caches.py index eb15b85606..5d98f99d3b 100644 --- a/bot/exts/help_channels/_caches.py +++ b/bot/exts/help_channels/_caches.py @@ -8,3 +8,7 @@ # serialise the set as a comma separated string to allow usage with redis # RedisCache[discord.TextChannel.id, str[set[discord.User.id]]] session_participants = RedisCache(namespace="HelpChannels.session_participants") + +# Stores posts that have had a non-claimant reply. +# Currently only used to determine whether the post was answered or not when collecting stats. +posts_with_non_claimant_messages = RedisCache(namespace="HelpChannels.posts_with_non_claimant_messages") diff --git a/bot/exts/help_channels/_stats.py b/bot/exts/help_channels/_stats.py index 6c05b47018..8ab93f19da 100644 --- a/bot/exts/help_channels/_stats.py +++ b/bot/exts/help_channels/_stats.py @@ -5,6 +5,7 @@ import bot from bot import constants +from bot.exts.help_channels import _caches from bot.log import get_logger log = get_logger(__name__) @@ -37,8 +38,7 @@ async def report_complete_session(help_session_post: discord.Thread, closed_on: in_use_time = arrow.utcnow() - open_time bot.instance.stats.timing("help_forum.in_use_time", in_use_time) - if set(help_session_post.members)-{help_session_post.owner_id} == set(): - # Can't use len(help_session_post.members) as the claimant (owner) may have left the thread. - bot.instance.stats.incr("help_forum.sessions.unanswered") - else: + if await _caches.posts_with_non_claimant_messages.get(help_session_post.id): bot.instance.stats.incr("help_forum.sessions.answered") + else: + bot.instance.stats.incr("help_forum.sessions.unanswered") From f1d433e626701a56b99e07a68cebbe35bdb168c8 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 14 Nov 2022 12:33:21 +0000 Subject: [PATCH 12/50] Add a filter for help chanel post names It is expected that this code will be delete whent he new fitler cog is added, and we start filtering on thread names genericly. --- bot/exts/help_channels/_cog.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 6423f6f2f2..c119a44ef8 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -1,5 +1,7 @@ """Contains the Cog that receives discord.py events and defers most actions to other files in the module.""" +import typing as t + import discord from discord.ext import commands @@ -10,6 +12,9 @@ log = get_logger(__name__) +if t.TYPE_CHECKING: + from bot.exts.filters.filtering import Filtering + class HelpForum(commands.Cog): """ @@ -41,6 +46,18 @@ async def close_check(self, ctx: commands.Context) -> bool: self.bot.stats.incr("help.dormant_invoke.staff") return has_role + async def post_with_disallowed_title_check(self, post: discord.Thread) -> None: + """Check if the given post has a bad word, alerting moderators if it does.""" + filter_cog: Filtering | None = self.bot.get_cog("Filtering") + if filter_cog and (match := filter_cog.get_name_match(post.name)): + mod_alerts = self.bot.get_channel(constants.Channels.mod_alerts) + await mod_alerts.send( + f"<@&{constants.Roles.moderators}>\n" + f"<@{post.owner_id}> ({post.owner_id}) opened the thread {post.mention} ({post.id}), " + "which triggered the token filter with its name!\n" + f"**Match:** {match.group()}" + ) + @commands.group(name="help-forum") async def help_forum_group(self, ctx: commands.Context) -> None: """A group of commands that help manage our help forum system.""" @@ -99,8 +116,12 @@ async def rename_help_post(self, ctx: commands.Context, *, title: str) -> None: @commands.Cog.listener() async def on_thread_create(self, thread: discord.Thread) -> None: """Defer application of new post logic for posts the help forum to the _channel helper.""" - if thread.parent_id == self.help_forum_channel_id: - await _channel.help_thread_opened(thread) + if thread.parent_id != self.help_forum_channel_id: + return + + await _channel.help_thread_opened(thread) + + await self.post_with_disallowed_title_check(thread) @commands.Cog.listener() async def on_thread_update(self, before: discord.Thread, after: discord.Thread) -> None: @@ -109,6 +130,8 @@ async def on_thread_update(self, before: discord.Thread, after: discord.Thread) return if not before.archived and after.archived: await _channel.help_thread_archived(after) + if before.name != after.name: + await self.post_with_disallowed_title_check(after) @commands.Cog.listener() async def on_raw_thread_delete(self, deleted_thread_event: discord.RawThreadDeleteEvent) -> None: From 9e95d459e5663ba3a3eacecf4c3073fce4817951 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 15 Nov 2022 10:25:35 +0000 Subject: [PATCH 13/50] Add hf as an alias for the help-forum command group --- bot/exts/help_channels/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index c119a44ef8..50f8416fc4 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -58,7 +58,7 @@ async def post_with_disallowed_title_check(self, post: discord.Thread) -> None: f"**Match:** {match.group()}" ) - @commands.group(name="help-forum") + @commands.group(name="help-forum", aliases=("hf",)) async def help_forum_group(self, ctx: commands.Context) -> None: """A group of commands that help manage our help forum system.""" if not ctx.invoked_subcommand: From 12f6339ccff0282bd8069398aedccecde61660e5 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 15 Nov 2022 10:26:14 +0000 Subject: [PATCH 14/50] Pin the user's starter message on help post creation --- bot/exts/help_channels/_channel.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 38725ddfde..a191e1ed76 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -109,6 +109,10 @@ async def help_thread_opened(opened_thread: discord.Thread, *, reopen: bool = Fa await _close_help_thread(opened_thread, _stats.ClosingReason.CLEANUP) return + if opened_thread.starter_message: + # To cover the case where the user deletes their starter message before code execution reaches this line. + await opened_thread.starter_message.pin() + await send_opened_post_message(opened_thread) await send_opened_post_dm(opened_thread) From e50896badaf8308df1e77c9d1df53073f7995ad0 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 25 Nov 2022 23:21:44 +0000 Subject: [PATCH 15/50] Send DM message to help post opener sooner in the process This is to give Discord time to make the thread actually avaiulable to be posted in for the send_opened_post_message call Closes #2334 Closes BOT-3AW --- bot/exts/help_channels/_channel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index a191e1ed76..cc3d831b0c 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -109,12 +109,13 @@ async def help_thread_opened(opened_thread: discord.Thread, *, reopen: bool = Fa await _close_help_thread(opened_thread, _stats.ClosingReason.CLEANUP) return + await send_opened_post_dm(opened_thread) + if opened_thread.starter_message: # To cover the case where the user deletes their starter message before code execution reaches this line. await opened_thread.starter_message.pin() await send_opened_post_message(opened_thread) - await send_opened_post_dm(opened_thread) cooldown_role = opened_thread.guild.get_role(constants.Roles.help_cooldown) await members.handle_role_change(opened_thread.owner, opened_thread.owner.add_roles, cooldown_role) From ed815a1cabde59edbea734ed7e10a15bf9a845ff Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 26 Nov 2022 14:09:02 +0400 Subject: [PATCH 16/50] Simplify Help Thread Warning Log The current warning log includes the thread name, which means the log message varies wildly between threads. This causes issues with sentry since the actual error message gets trimmed, and sentry fails to group issues from this log as they appear as different messages. Signed-off-by: Hassan Abouelela --- bot/exts/help_channels/_channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index cc3d831b0c..d4a7420002 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -80,7 +80,7 @@ async def send_opened_post_dm(thread: discord.Thread) -> None: try: message = await thread.fetch_message(thread.id) except discord.HTTPException: - log.warning(f"Could not fetch message for thread {thread.name} ({thread.id})") + log.warning(f"Could not fetch message for thread {thread.id}") return formatted_message = textwrap.shorten(message.content, width=100, placeholder="...") From fcf7e1cc2cdef58b27cc7d418f22b7bc22b78f07 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 26 Nov 2022 14:11:27 +0400 Subject: [PATCH 17/50] Handle Discord API Not Being Ready When discord sends us the thread create event in help channels, it is not ready to perform other operations on the thread such as getting or pinning messages. This causes it to error out when we try to do these actions and claim that those channels don't exist. Instead, we sleep for a short time to try and wait for it to be ready. Signed-off-by: Hassan Abouelela --- bot/exts/help_channels/_channel.py | 7 ++++++- bot/exts/help_channels/_cog.py | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index d4a7420002..3dc9e81ef7 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,5 +1,5 @@ """Contains all logic to handle changes to posts in the help forum.""" - +import asyncio import textwrap import discord @@ -109,6 +109,11 @@ async def help_thread_opened(opened_thread: discord.Thread, *, reopen: bool = Fa await _close_help_thread(opened_thread, _stats.ClosingReason.CLEANUP) return + # Discord sends the open event long before the thread is ready for actions in the API. + # This causes actions such as fetching the message, pinning message, etc to fail. + # We sleep here to try and delay our code enough so the thread is ready in the API. + await asyncio.sleep(2) + await send_opened_post_dm(opened_thread) if opened_thread.starter_message: diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 50f8416fc4..bb2f43c5a9 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -119,9 +119,8 @@ async def on_thread_create(self, thread: discord.Thread) -> None: if thread.parent_id != self.help_forum_channel_id: return - await _channel.help_thread_opened(thread) - await self.post_with_disallowed_title_check(thread) + await _channel.help_thread_opened(thread) @commands.Cog.listener() async def on_thread_update(self, before: discord.Thread, after: discord.Thread) -> None: From 4779d4240ae43fd48699ac80c0779665604abebb Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 26 Nov 2022 14:21:58 +0400 Subject: [PATCH 18/50] Don't Remove Cooldown Role From Non-Existing Users We try to remove the cooldown role from users before checking if the user is still in the server, which can cause an error since the thread object will just contain `None` as the user. Signed-off-by: Hassan Abouelela --- bot/exts/help_channels/_channel.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 3dc9e81ef7..a41fcd63f9 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -52,6 +52,15 @@ async def _close_help_thread(closed_thread: discord.Thread, closed_on: _stats.Cl poster = closed_thread.owner cooldown_role = closed_thread.guild.get_role(constants.Roles.help_cooldown) + + if poster is None: + # We can't include the owner ID/name here since the thread only contains None + log.info( + f"Failed to remove cooldown role for owner of thread ({closed_thread.id}). " + f"The user is likely no longer on the server." + ) + return + await members.handle_role_change(poster, poster.remove_roles, cooldown_role) From 81a68784559d231afcc055c2d34cb71314d5bd61 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 26 Nov 2022 14:28:18 +0400 Subject: [PATCH 19/50] Catch Failure In Pining Help Starter Message The old method for detecting deleted opener messages does not seem to work, probably because the message is fetched from a cache or similar. Instead we simply try/except pinning the message and pass if the pinning failed. Signed-off-by: Hassan Abouelela --- bot/exts/help_channels/_channel.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index a41fcd63f9..04c2cc454f 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -125,9 +125,14 @@ async def help_thread_opened(opened_thread: discord.Thread, *, reopen: bool = Fa await send_opened_post_dm(opened_thread) - if opened_thread.starter_message: - # To cover the case where the user deletes their starter message before code execution reaches this line. + try: await opened_thread.starter_message.pin() + except discord.HTTPException as e: + if e.code == 10008: + # The message was not found, most likely deleted + pass + else: + raise e await send_opened_post_message(opened_thread) From 555ed4e9195aae7b5af742c59125833ce8b01b4c Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 26 Nov 2022 14:40:04 +0400 Subject: [PATCH 20/50] Check If Thread Is Closed In wait_for_deletion The wait_for_deletion utility would try to remove reactions from a message after the timeout expires, which would normally be fine. In threads however, they can be closed while waiting for the timeout to expire. In such a case, the bot will try to remove the reactions after the channel has been closed and fail. A special exception was added for this case to do nothing, since this is only a QoL feature. Signed-off-by: Hassan Abouelela --- bot/utils/messages.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index a5ed84351d..cc7e6dccbe 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -98,7 +98,14 @@ async def wait_for_deletion( try: await bot.instance.wait_for('reaction_add', check=check, timeout=timeout) except asyncio.TimeoutError: - await message.clear_reactions() + try: + await message.clear_reactions() + except discord.HTTPException as e: + if isinstance(message.channel, discord.Thread): + # Threads might not be accessible by the time we try to remove the reaction. + pass + else: + raise e else: await message.delete() except discord.NotFound: From dddf9734f898474ebd841537a49fb8d6905da346 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 26 Nov 2022 15:16:54 +0400 Subject: [PATCH 21/50] Check If Thread Is Closed In Pagination Similar to 555ed4e9, the pagination utility needs to catch when it's trying to act on an archived thread. Signed-off-by: Hassan Abouelela --- bot/pagination.py | 93 +++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 55 deletions(-) diff --git a/bot/pagination.py b/bot/pagination.py index 10bef1c9f9..5b96d8fbb2 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -301,42 +301,30 @@ async def paginate( if str(reaction.emoji) == DELETE_EMOJI: log.debug("Got delete reaction") return await message.delete() - - if reaction.emoji == FIRST_EMOJI: - await message.remove_reaction(reaction.emoji, user) - current_page = 0 - - log.debug(f"Got first page reaction - changing to page 1/{len(paginator.pages)}") - - embed.description = paginator.pages[current_page] - if footer_text: - embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") - else: - embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - await message.edit(embed=embed) - - if reaction.emoji == LAST_EMOJI: - await message.remove_reaction(reaction.emoji, user) - current_page = len(paginator.pages) - 1 - - log.debug(f"Got last page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") - - embed.description = paginator.pages[current_page] - if footer_text: - embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") - else: - embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - await message.edit(embed=embed) - - if reaction.emoji == LEFT_EMOJI: + elif reaction.emoji in PAGINATION_EMOJI: + total_pages = len(paginator.pages) await message.remove_reaction(reaction.emoji, user) - if current_page <= 0: - log.debug("Got previous page reaction, but we're on the first page - ignoring") - continue - - current_page -= 1 - log.debug(f"Got previous page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") + if reaction.emoji == FIRST_EMOJI: + current_page = 0 + log.debug(f"Got first page reaction - changing to page 1/{total_pages}") + elif reaction.emoji == LAST_EMOJI: + current_page = len(paginator.pages) - 1 + log.debug(f"Got last page reaction - changing to page {current_page + 1}/{total_pages}") + elif reaction.emoji == LEFT_EMOJI: + if current_page <= 0: + log.debug("Got previous page reaction, but we're on the first page - ignoring") + continue + + current_page -= 1 + log.debug(f"Got previous page reaction - changing to page {current_page + 1}/{total_pages}") + elif reaction.emoji == RIGHT_EMOJI: + if current_page >= len(paginator.pages) - 1: + log.debug("Got next page reaction, but we're on the last page - ignoring") + continue + + current_page += 1 + log.debug(f"Got next page reaction - changing to page {current_page + 1}/{total_pages}") embed.description = paginator.pages[current_page] @@ -345,27 +333,22 @@ async def paginate( else: embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - await message.edit(embed=embed) - - if reaction.emoji == RIGHT_EMOJI: - await message.remove_reaction(reaction.emoji, user) - - if current_page >= len(paginator.pages) - 1: - log.debug("Got next page reaction, but we're on the last page - ignoring") - continue - - current_page += 1 - log.debug(f"Got next page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") - - embed.description = paginator.pages[current_page] - - if footer_text: - embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") - else: - embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - - await message.edit(embed=embed) + try: + await message.edit(embed=embed) + except discord.HTTPException as e: + if e.code == 50083: + # Trying to act on an archived thread, just ignore and abort + break + else: + raise e log.debug("Ending pagination and clearing reactions.") with suppress(discord.NotFound): - await message.clear_reactions() + try: + await message.clear_reactions() + except discord.HTTPException as e: + if e.code == 50083: + # Trying to act on an archived thread, just ignore + pass + else: + raise e From 4ca04f60c122f534700e2a350bcbafd2471b96e4 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 26 Nov 2022 15:20:14 +0400 Subject: [PATCH 22/50] Handle Images As Starter Messages In the case of an image or other media as the starter message, the formatted message in the help DM will be empty, which is invalid for the embed. We populate the field with some more useful text in this case. Signed-off-by: Hassan Abouelela --- bot/exts/help_channels/_channel.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 04c2cc454f..b9a79a4768 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -92,7 +92,11 @@ async def send_opened_post_dm(thread: discord.Thread) -> None: log.warning(f"Could not fetch message for thread {thread.id}") return - formatted_message = textwrap.shorten(message.content, width=100, placeholder="...") + formatted_message = textwrap.shorten(message.content, width=100, placeholder="...").strip() + if formatted_message is None: + # This most likely means the initial message is only an image or similar + formatted_message = "No text content." + embed.add_field(name="Your message", value=formatted_message, inline=False) embed.add_field( name="Conversation", From 8cfa7ed5b39b85e432420c5749c0fe1569e2d2cb Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 26 Nov 2022 16:12:42 +0400 Subject: [PATCH 23/50] Refactor Thread Error Catching Co-authored-by: Boris Muratov <8bee278@gmail.com> Co-authored-by: Amrou Bellalouna <48383734+shtlrs@users.noreply.github.com> Signed-off-by: Hassan Abouelela --- bot/exts/help_channels/_channel.py | 6 ++---- bot/pagination.py | 13 ++++++++----- bot/utils/messages.py | 15 +++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index b9a79a4768..5fc39b6236 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -132,10 +132,8 @@ async def help_thread_opened(opened_thread: discord.Thread, *, reopen: bool = Fa try: await opened_thread.starter_message.pin() except discord.HTTPException as e: - if e.code == 10008: - # The message was not found, most likely deleted - pass - else: + # Suppress if the message was not found, most likely deleted + if e.code != 10008: raise e await send_opened_post_message(opened_thread) diff --git a/bot/pagination.py b/bot/pagination.py index 5b96d8fbb2..0ef5808ccc 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -303,7 +303,12 @@ async def paginate( return await message.delete() elif reaction.emoji in PAGINATION_EMOJI: total_pages = len(paginator.pages) - await message.remove_reaction(reaction.emoji, user) + try: + await message.remove_reaction(reaction.emoji, user) + except discord.HTTPException as e: + # Suppress if trying to act on an archived thread. + if e.code != 50083: + raise e if reaction.emoji == FIRST_EMOJI: current_page = 0 @@ -347,8 +352,6 @@ async def paginate( try: await message.clear_reactions() except discord.HTTPException as e: - if e.code == 50083: - # Trying to act on an archived thread, just ignore - pass - else: + # Suppress if trying to act on an archived thread. + if e.code != 50083: raise e diff --git a/bot/utils/messages.py b/bot/utils/messages.py index cc7e6dccbe..8a968f6598 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -98,19 +98,18 @@ async def wait_for_deletion( try: await bot.instance.wait_for('reaction_add', check=check, timeout=timeout) except asyncio.TimeoutError: - try: - await message.clear_reactions() - except discord.HTTPException as e: - if isinstance(message.channel, discord.Thread): - # Threads might not be accessible by the time we try to remove the reaction. - pass - else: - raise e + await message.clear_reactions() else: await message.delete() + except discord.NotFound: log.trace(f"wait_for_deletion: message {message.id} deleted prematurely.") + except discord.HTTPException: + if not isinstance(message.channel, discord.Thread): + # Threads might not be accessible by the time the timeout expires + raise + async def send_attachments( message: discord.Message, From fd5af2ea3eaa87d6ee83e6e3af881dc9faa617a3 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 11:00:57 +0000 Subject: [PATCH 24/50] Bump bot-core version --- poetry.lock | 465 +++++++++++++++++++++++++------------------------ pyproject.toml | 3 +- 2 files changed, 236 insertions(+), 232 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5df497649d..b273319597 100644 --- a/poetry.lock +++ b/poetry.lock @@ -31,11 +31,11 @@ speedups = ["Brotli", "aiodns", "cchardet"] [[package]] name = "aiosignal" -version = "1.2.0" +version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] frozenlist = ">=1.1.0" @@ -86,7 +86,7 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "beautifulsoup4" @@ -103,27 +103,6 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "bot-core" -version = "8.2.1" -description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." -category = "main" -optional = false -python-versions = "3.10.*" - -[package.dependencies] -aiodns = "3.0.0" -async-rediscache = {version = "1.0.0rc2", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""} -"discord.py" = "2.0.1" -statsd = "3.3.0" - -[package.extras] -async-rediscache = ["async-rediscache[fakeredis] (==1.0.0rc2)"] - -[package.source] -type = "url" -url = "https://github.com/python-discord/bot-core/archive/refs/tags/v8.2.1.zip" - [[package]] name = "certifi" version = "2022.9.24" @@ -160,7 +139,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "colorama" @@ -213,7 +192,7 @@ ordered-set = ">=4.1.0,<4.2.0" cli = ["clevercsv (==0.7.1)", "click (==8.0.3)", "pyyaml (==5.4.1)", "toml (==0.10.2)"] [[package]] -name = "Deprecated" +name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." category = "main" @@ -227,8 +206,8 @@ wrapt = ">=1.10,<2" dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] [[package]] -name = "discord.py" -version = "2.0.1" +name = "discord-py" +version = "2.1.0" description = "A Python wrapper for the Discord API" category = "main" optional = false @@ -415,7 +394,7 @@ pycodestyle = ">=2.0.0,<3.0.0" [[package]] name = "frozenlist" -version = "1.3.1" +version = "1.3.3" description = "A list-like structure which implements collections.abc.MutableSequence" category = "main" optional = false @@ -434,7 +413,7 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.5.6" +version = "2.5.9" description = "File identification library for Python" category = "dev" optional = false @@ -469,13 +448,13 @@ python-versions = ">=3.6.1,<4.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "lupa" -version = "1.13" +version = "1.14.1" description = "Python wrapper around Lua and LuaJIT" category = "main" optional = false @@ -599,15 +578,15 @@ test = ["docutils", "pytest-cov", "pytest-pycodestyle", "pytest-runner"] [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.5.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] +test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -639,7 +618,7 @@ virtualenv = ">=20.0.8" [[package]] name = "psutil" -version = "5.9.2" +version = "5.9.4" description = "Cross-platform lib for process and system monitoring in Python." category = "dev" optional = false @@ -649,7 +628,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] -name = "PTable" +name = "ptable" version = "0.9.2" description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" category = "dev" @@ -709,6 +688,23 @@ typing-extensions = ">=4.1.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pydis-core" +version = "9.1.1" +description = "PyDis core provides core functionality and utility to the bots of the Python Discord community." +category = "main" +optional = false +python-versions = ">=3.10.0,<3.11.0" + +[package.dependencies] +aiodns = "3.0.0" +async-rediscache = {version = "1.0.0rc2", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""} +"discord.py" = "2.1.0" +statsd = "4.0.1" + +[package.extras] +async-rediscache = ["async-rediscache[fakeredis] (==1.0.0rc2)"] + [[package]] name = "pydocstyle" version = "6.1.1" @@ -864,7 +860,7 @@ docs = ["sphinx"] test = ["pyaml", "pytest", "toml"] [[package]] -name = "PyYAML" +name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "main" @@ -923,7 +919,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-file" @@ -960,7 +956,7 @@ falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)"] httpx = ["httpx (>=0.16.0)"] -pure_eval = ["asttokens", "executing", "pure-eval"] +pure-eval = ["asttokens", "executing", "pure-eval"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] rq = ["rq (>=0.6)"] @@ -971,7 +967,7 @@ tornado = ["tornado (>=5)"] [[package]] name = "setuptools" -version = "65.4.1" +version = "65.6.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false @@ -979,7 +975,7 @@ python-versions = ">=3.7" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1024,7 +1020,7 @@ python-versions = ">=3.6" [[package]] name = "statsd" -version = "3.3.0" +version = "4.0.1" description = "A simple statsd client." category = "main" optional = false @@ -1084,11 +1080,11 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -1097,19 +1093,19 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.5" +version = "20.16.7" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] @@ -1135,7 +1131,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.10.*" -content-hash = "f7c3aa7385e92837d5f8401f9903375a20881ad2d21aeeda56c9e374e3c46918" +content-hash = "b35ea1d531f76e14f06bbed58d6797cc139064fe146f4142a622876f6f2fbe86" [metadata.files] aiodns = [ @@ -1232,8 +1228,8 @@ aiohttp = [ {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, ] aiosignal = [ - {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, - {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] arrow = [ {file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"}, @@ -1255,7 +1251,6 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, ] -bot-core = [] certifi = [ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, @@ -1398,13 +1393,13 @@ deepdiff = [ {file = "deepdiff-5.8.1-py3-none-any.whl", hash = "sha256:e9aea49733f34fab9a0897038d8f26f9d94a97db1790f1b814cced89e9e0d2b7"}, {file = "deepdiff-5.8.1.tar.gz", hash = "sha256:8d4eb2c4e6cbc80b811266419cb71dd95a157094a3947ccf937a94d44943c7b8"}, ] -Deprecated = [ +deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] -"discord.py" = [ - {file = "discord.py-2.0.1-py3-none-any.whl", hash = "sha256:aeb186348bf011708b085b2715cf92bbb72c692eb4f59c4c0b488130cc4c4b7e"}, - {file = "discord.py-2.0.1.tar.gz", hash = "sha256:309146476e986cb8faf038cd5d604d4b3834ef15c2d34df697ce5064bf5cd779"}, +discord-py = [ + {file = "discord.py-2.1.0-py3-none-any.whl", hash = "sha256:a2cfa9f09e3013aaaa43600cc8dfaf67c532dd34afcb71e550f5a0dc9133a5e0"}, + {file = "discord.py-2.1.0.tar.gz", hash = "sha256:027ccdd22b5bb66a9e19cbd8daa1bc74b49271a16a074d57e52f288fcfa208e8"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, @@ -1461,73 +1456,88 @@ flake8-todo = [ {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"}, ] frozenlist = [ - {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"}, - {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"}, - {file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"}, - {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"}, - {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"}, - {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"}, - {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"}, - {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"}, - {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"}, - {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"}, - {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"}, - {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"}, - {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"}, - {file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"}, - {file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"}, - {file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"}, - {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"}, - {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"}, - {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"}, - {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"}, - {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"}, - {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"}, - {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"}, - {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"}, - {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"}, - {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"}, - {file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"}, - {file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"}, - {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"}, - {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"}, - {file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"}, - {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"}, - {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"}, - {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"}, - {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"}, - {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"}, - {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"}, - {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"}, - {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"}, - {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"}, - {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"}, - {file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"}, - {file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"}, - {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"}, - {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"}, - {file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"}, - {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"}, - {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"}, - {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"}, - {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"}, - {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"}, - {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"}, - {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"}, - {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"}, - {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"}, - {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"}, - {file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"}, - {file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"}, - {file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, + {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, + {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, + {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, + {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, + {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, + {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, + {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, + {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, + {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, + {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, ] humanfriendly = [ {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, ] identify = [ - {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, - {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, + {file = "identify-2.5.9-py2.py3-none-any.whl", hash = "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d"}, + {file = "identify-2.5.9.tar.gz", hash = "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1542,72 +1552,81 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] lupa = [ - {file = "lupa-1.13-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:da1885faca29091f9e408c0cc6b43a0b29a2128acf8d08c188febc5d9f99129d"}, - {file = "lupa-1.13-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4525e954e951562eb5609eca6ac694d0158a5351649656e50d524f87f71e2a35"}, - {file = "lupa-1.13-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5a04febcd3016cb992e6c5b2f97834ad53a2fd4b37767d9afdce116021c2463a"}, - {file = "lupa-1.13-cp27-cp27m-win32.whl", hash = "sha256:98f6d3debc4d3668e5e19d70e288dbdbbedef021a75ac2e42c450c7679b4bf52"}, - {file = "lupa-1.13-cp27-cp27m-win_amd64.whl", hash = "sha256:7009719bf65549c018a2f925ff06b9d862a5a1e22f8a7aeeef807eb1e99b56bc"}, - {file = "lupa-1.13-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bde9e73b06d147d31b970123a013cc6d28a4bea7b3d6b64fe115650cbc62b1a3"}, - {file = "lupa-1.13-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a122baad6c6f9aaae496a59318217c068ae73654f618526e404a28775b46da38"}, - {file = "lupa-1.13-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:4d1588486ed16d6b53f41b080047d44db3aa9991cf8a30da844cb97486a63c8b"}, - {file = "lupa-1.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:a79be3ca652c8392d612bdc2234074325a68ec572c4175a35347cd650ef4a4b9"}, - {file = "lupa-1.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d9105f3b098cd4c276d6258f8254224243066f51c5d3c923b8f460efac9de37b"}, - {file = "lupa-1.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2d1fbddfa2914c405004f805afb13f5fc385793f3ba28e86a6f0c85b4059b86c"}, - {file = "lupa-1.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a3c84994399887a8befc82aef4d837582db45a301413025c510e20fef9e9148"}, - {file = "lupa-1.13-cp310-cp310-win32.whl", hash = "sha256:c665af2a92e79106045f973174e0849f92b44395f5247505d321bc1173d9f3fd"}, - {file = "lupa-1.13-cp310-cp310-win_amd64.whl", hash = "sha256:c9b47a9e93cb8e8f342343f4e0963eb1966d36baeced482575141925eafc17dc"}, - {file = "lupa-1.13-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:b3003d723faabb9502259662722462cbff368f26ed83a6311f65949d298593bf"}, - {file = "lupa-1.13-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b341b8a4711558af771bd4a954a6ffe531bfe097c1f1cdce84b9ad56070dfe90"}, - {file = "lupa-1.13-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ea049ee507a549eec553a9d27e3e6c034eae8c145e7bad5947e85c4b9e23757b"}, - {file = "lupa-1.13-cp35-cp35m-win32.whl", hash = "sha256:ba6c49646ad42c836f18ff8f1b6b8db4ca32fc02e786e1bf401b0fa34fe82cca"}, - {file = "lupa-1.13-cp35-cp35m-win_amd64.whl", hash = "sha256:de51177d1374fd9cce27b9cdb20771142d91a509e42337b3e7c6cffbba818d6f"}, - {file = "lupa-1.13-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:dddfeb031ab67c8bdbeefd2de237a98bee58e2166d5ed629c3a0c3842bb91738"}, - {file = "lupa-1.13-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57f00004c185bd60459586a9d08961541f5da1cfec5925a3fc1ab68deaa2e038"}, - {file = "lupa-1.13-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a940be5b38b68b344691558ffde1b44377ad66c105661f6f58c7d4c0c227d8ea"}, - {file = "lupa-1.13-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:807b27c13f7598af9343455204a6a23b6b919180f01668c9b8fa4f9b0d75dedb"}, - {file = "lupa-1.13-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a52d5a8305f4854f91ee39f5ee6f175f4d38f362c6b00483fe618ae6f9dff5b"}, - {file = "lupa-1.13-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0ad47549359df03b3e59796ba09df548e1fd046f9245391dae79699c9ffec0f6"}, - {file = "lupa-1.13-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fbf99cea003b38a146dff5333ba58edb8165e01c42f15d7f76fdb72e761b5827"}, - {file = "lupa-1.13-cp36-cp36m-win32.whl", hash = "sha256:a101c84097fdfa7b1a38f9d5a3055759da4e222c255ab8e5ac5b683704e62c97"}, - {file = "lupa-1.13-cp36-cp36m-win_amd64.whl", hash = "sha256:00376b3bcb00bb57e067740ea9ff00f610a44aff5338ea93d3198a035f8965c6"}, - {file = "lupa-1.13-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:91001c9667d60b69c3ad623dc315d7b59712e1617fe6204e5852c31cda778678"}, - {file = "lupa-1.13-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:65c9d034d7215e8929a4ab48c9d9d372786ef47c8e61c294851bf0b8f5b4fbf4"}, - {file = "lupa-1.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:928527222b2a15bd3dcea646f7585852097302c078c338fb0f184ce560d48c6c"}, - {file = "lupa-1.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:5e157d97e379931a7fa90d9afa66600f796960bc062e04a9bb37f24fa7c5c967"}, - {file = "lupa-1.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a67336d542d71e095c07dacc72c16158745ae4ef08e8a7bfe75827da604b4979"}, - {file = "lupa-1.13-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0c5cd027c998db5b29ca8dd956c255d50914aed614d1c9edb68bc3315f916f59"}, - {file = "lupa-1.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:76b06355f0b3d3aece5c38d20a66ab7d3046add95b8d04b677ade162fce2ffd0"}, - {file = "lupa-1.13-cp37-cp37m-win32.whl", hash = "sha256:2a6b0a7e45390de36d11dd8705b2a0a10739ba8ed2e99c130e983ad72d56ddc9"}, - {file = "lupa-1.13-cp37-cp37m-win_amd64.whl", hash = "sha256:42ffbe43119225cc58c7ebd2210123b9367b098ac25a7f0ef5d473e2f65fc0d9"}, - {file = "lupa-1.13-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:7ff445a5d8ab25e623f871c600af58f1cd6207f6873a42c3b8c1683f13a22db0"}, - {file = "lupa-1.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:dd0404f11b9473372fe2a8bdf0d64b361852ae08699d6dcde1215db3bd6c7b9c"}, - {file = "lupa-1.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:14419b29152667fb2d78c6d5176f9a704c765aeecb80fe6c079a8dba9f864529"}, - {file = "lupa-1.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:9e644032b40b59420ffa0d58ca1705351785ce8e39b77d9f1a8c4cf78e371adb"}, - {file = "lupa-1.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c090991e2b701ded6c9e330ea582a74dd9cb09069b3de9ae897b938bd97dc98f"}, - {file = "lupa-1.13-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6812f16530a1dc88f66c76a002e1c16039d3d98e1ff283a2efd5a492342ba00c"}, - {file = "lupa-1.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff3989ab562fb62e9df2290739c7f82e05d5ba7d2fa2ea319991885dfc818c81"}, - {file = "lupa-1.13-cp38-cp38-win32.whl", hash = "sha256:48fa15cf24d297c50f21bff1fe1883f7a6a15b34b70db5a6c18d2dfbed6b6e16"}, - {file = "lupa-1.13-cp38-cp38-win_amd64.whl", hash = "sha256:ea32a62d404c3d9e119e83b653aa56c034cae63a4e830aefa15bf3a25299b29e"}, - {file = "lupa-1.13-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:80d36fbdc6218332232b4c214a2f9c36b13136b546dca0b3d19aca12d77e1f8e"}, - {file = "lupa-1.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:db4745132f8abe0c9daac155af9d196926c9e10662d999edd805756d91502a01"}, - {file = "lupa-1.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:938fb12c556737f9e4ffb7912540e35423d1be3166c6d4099ca4f3e177fe619e"}, - {file = "lupa-1.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:de913a471ee6dc86435b647dda3cdb787990b164d8c8c63ca03d6e934f305a55"}, - {file = "lupa-1.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:488d1bd773f10331ca67b0914c880900316634fd14538f76c3c2fbc7e6b56043"}, - {file = "lupa-1.13-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dc101e6d82ffa1b3fcfc77f2430a10c02def972cf0f8c7a229e272697e22e35c"}, - {file = "lupa-1.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:361a55883b692d25478a69104d8ecce4cad058ba39ec1b7378b1209f86867687"}, - {file = "lupa-1.13-cp39-cp39-win32.whl", hash = "sha256:9a6cd192e789fbc7f6a777a17b5b517c447a6dc6049e60c1becb300f86205345"}, - {file = "lupa-1.13-cp39-cp39-win_amd64.whl", hash = "sha256:9fe47cda7cc81bd9b111f1317ed60e3da2620f4fef5360b690dcf62f88bbc668"}, - {file = "lupa-1.13-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:7d860dc0062b3001993355b12b939f68e0e2871a19a81427d2a9ced893574b58"}, - {file = "lupa-1.13-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6c0358386f16afb50145b143774791c942c93a9721078a17983486a2d9f8f45b"}, - {file = "lupa-1.13-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:a46962ebdc6278e82520c66d5dd1eed50099aa2f56b6827b7a4f001664d9ad1d"}, - {file = "lupa-1.13-pp37-pypy37_pp73-win32.whl", hash = "sha256:436daf32385bcb9b6b9f922cbc0b64d133db141f0f7d8946a3a653e83b478713"}, - {file = "lupa-1.13-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:f1165e89aa8d2a0644619517e04410b9f5e3da2c9b3d105bf53f70e786f91f79"}, - {file = "lupa-1.13-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:325069e4f3cf4b1232d03fb330ba1449867fc7dd727ecebaf0e602ddcacaf9d4"}, - {file = "lupa-1.13-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:ce59c335b80ec4f9e98181970c18552f51adba5c3380ef5d46bdb3246b87963d"}, - {file = "lupa-1.13-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ad263ba6e54a13ac036364ae43ba7613c869c5ee6ff7dbb86791685a6cba13c5"}, - {file = "lupa-1.13-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:86f4f46ee854e36cf5b6cf2317075023f395eede53efec0a694bc4a01fc03ab7"}, - {file = "lupa-1.13-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:59799f40774dd5b8cfb99b11d6ce3a3f3a141e112472874389d47c81a7377ef9"}, - {file = "lupa-1.13.tar.gz", hash = "sha256:e1d94ac2a630d271027dac2c21d1428771d9ea9d4d88f15f20a7781340f02a4e"}, + {file = "lupa-1.14.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:20b486cda76ff141cfb5f28df9c757224c9ed91e78c5242d402d2e9cb699d464"}, + {file = "lupa-1.14.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c685143b18c79a3a1fa25a4cc774a87b5a61c606f249bcf824d125d8accb6b2c"}, + {file = "lupa-1.14.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3865f9dbe9a84bd6a471250e52068aaf1147f206a51905fb6d93e1db9efb00ee"}, + {file = "lupa-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:2dacdddd5e28c6f5fd96a46c868ec5c34b0fad1ec7235b5bbb56f06183a37f20"}, + {file = "lupa-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e754cbc6cacc9bca6ff2b39025e9659a2098420639d214054b06b466825f4470"}, + {file = "lupa-1.14.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e36f3eb70705841bce9c15e12bc6fc3b2f4f68a41ba0e4af303b22fc4d8667c"}, + {file = "lupa-1.14.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0aac06098d46729edd2d04e80b55d9d310e902f042f27521308df77cb1ba0191"}, + {file = "lupa-1.14.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:9706a192339efa1a6b7d806389572a669dd9ae2250469ff1ce13f684085af0b4"}, + {file = "lupa-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d688a35f7fe614720ed7b820cbb739b37eff577a764c2003e229c2a752201cea"}, + {file = "lupa-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:36d888bd42589ecad21a5fb957b46bc799640d18eff2fd0c47a79ffb4a1b286c"}, + {file = "lupa-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:0423acd739cf25dbdbf1e33a0aa8026f35e1edea0573db63d156f14a082d77c8"}, + {file = "lupa-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7068ae0d6a1a35ea8718ef6e103955c1ee143181bf0684604a76acc67f69de55"}, + {file = "lupa-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5fef8b755591f0466438ad0a3e92ecb21dd6bb1f05d0215139b6ff8c87b2ce65"}, + {file = "lupa-1.14.1-cp310-cp310-win32.whl", hash = "sha256:4a44e1fd0e9f4a546fbddd2e0fd913c823c9ac58a5f3160fb4f9109f633cb027"}, + {file = "lupa-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:b83100cd7b48a7ca85dda4e9a6a5e7bc3312691e7f94c6a78d1f9a48a86a7fec"}, + {file = "lupa-1.14.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:1b8bda50c61c98ff9bb41d1f4934640c323e9f1539021810016a2eae25a66c3d"}, + {file = "lupa-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa1449aa1ab46c557344867496dee324b47ede0c41643df8f392b00262d21b12"}, + {file = "lupa-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a17ebf91b3aa1c5c36661e34c9cf10e04bb4cc00076e8b966f86749647162050"}, + {file = "lupa-1.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:b1d9cfa469e7a2ad7e9a00fea7196b0022aa52f43a2043c2e0be92122e7bcfe8"}, + {file = "lupa-1.14.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bc4f5e84aee0d567aa2e116ff6844d06086ef7404d5102807e59af5ce9daf3c0"}, + {file = "lupa-1.14.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:40cf2eb90087dfe8ee002740469f2c4c5230d5e7d10ffb676602066d2f9b1ac9"}, + {file = "lupa-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:63a27c38295aa971730795941270fff2ce65576f68ec63cb3ecb90d7a4526d03"}, + {file = "lupa-1.14.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:457330e7a5456c4415fc6d38822036bd4cff214f9d8f7906200f6b588f1b2932"}, + {file = "lupa-1.14.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d61fb507a36e18dc68f2d9e9e2ea19e1114b1a5e578a36f18e9be7a17d2931d1"}, + {file = "lupa-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:f26b73d10130ad73e07d45dfe9b7c3833e3a2aa1871a4ecf5ce2dc1abeeae74d"}, + {file = "lupa-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:297d801ba8e4e882b295c25d92f1634dde5e76d07ec6c35b13882401248c485d"}, + {file = "lupa-1.14.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c8bddd22eaeea0ce9d302b390d8bc606f003bf6c51be68e8b007504433b91280"}, + {file = "lupa-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1661c890861cf0f7002d7a7e00f50c885577954c2d85a7173b218d3228fa3869"}, + {file = "lupa-1.14.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2ee480d31555f00f8bf97dd949c596508bd60264cff1921a3797a03dd369e8cd"}, + {file = "lupa-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:1ff93560c2546d7627ab2f95b5e88f000705db70a3d6041ac29d050f094f2a35"}, + {file = "lupa-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:47f1459e2c98480c291ae3b70688d762f82dbb197ef121d529aa2c4e8bab1ba3"}, + {file = "lupa-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8986dba002346505ee44c78303339c97a346b883015d5cf3aaa0d76d3b952744"}, + {file = "lupa-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8912459fddf691e70f2add799a128822bae725826cfb86f69720a38bdfa42410"}, + {file = "lupa-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:9b9d1b98391959ae531bbb8df7559ac2c408fcbd33721921b6a05fd6414161e0"}, + {file = "lupa-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:61ff409040fa3a6c358b7274c10e556ba22afeb3470f8d23cd0a6bf418fb30c9"}, + {file = "lupa-1.14.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:350ba2218eea800898854b02753dc0c9cfe83db315b30c0dc10ab17493f0321a"}, + {file = "lupa-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:46dcbc0eae63899468686bb1dfc2fe4ed21fe06f69416113f039d88aab18f5dc"}, + {file = "lupa-1.14.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7ad96923e2092d8edbf0c1b274f9b522690b932ed47a70d9a0c1c329f169f107"}, + {file = "lupa-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:364b291bf2b55555c87b4bffb4db5a9619bcdb3c02e58aebde5319c3c59ec9b2"}, + {file = "lupa-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ed071efc8ee231fac1fcd6b6fce44dc6da75a352b9b78403af89a48d759743c"}, + {file = "lupa-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bce60847bebb4aa9ed3436fab3e84585e9094e15e1cb8d32e16e041c4ef65331"}, + {file = "lupa-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5fbe7f83b0007cda3b158a93726c80dfd39003a8c5c5d608f6fdf8c60c42117f"}, + {file = "lupa-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4bd789967cbb5c84470f358c7fa8fcbf7464185adbd872a6c3de9b42d29a6d26"}, + {file = "lupa-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:ca58da94a6495dda0063ba975fe2e6f722c5e84c94f09955671b279c41cfde96"}, + {file = "lupa-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:51d6965663b2be1a593beabfa10803fdbbcf0b293aa4a53ea09a23db89787d0d"}, + {file = "lupa-1.14.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d251ba009996a47231615ea6b78123c88446979ae99b5585269ec46f7a9197aa"}, + {file = "lupa-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:abe3fc103d7bd34e7028d06db557304979f13ebf9050ad0ea6c1cc3a1caea017"}, + {file = "lupa-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:4ea185c394bf7d07e9643d868e50cc94a530bb298d4bdae4915672b3809cc72b"}, + {file = "lupa-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:6aff7257b5953de620db489899406cddb22093d1124fc5b31f8900e44a9dbc2a"}, + {file = "lupa-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d6f5bfbd8fc48c27786aef8f30c84fd9197747fa0b53761e69eb968d81156cbf"}, + {file = "lupa-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dec7580b86975bc5bdf4cc54638c93daaec10143b4acc4a6c674c0f7e27dd363"}, + {file = "lupa-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:96a201537930813b34145daf337dcd934ddfaebeba6452caf8a32a418e145e82"}, + {file = "lupa-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c0efaae8e7276f4feb82cba43c3cd45c82db820c9dab3965a8f2e0cb8b0bc30b"}, + {file = "lupa-1.14.1-cp38-cp38-win32.whl", hash = "sha256:b6953854a343abdfe11aa52a2d021fadf3d77d0cd2b288b650f149b597e0d02d"}, + {file = "lupa-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:c79ced2aaf7577e3d06933cf0d323fa968e6864c498c376b0bd475ded86f01f3"}, + {file = "lupa-1.14.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:72589a21a3776c7dd4b05374780e7ecf1b49c490056077fc91486461935eaaa3"}, + {file = "lupa-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:30d356a433653b53f1fe29477faaf5e547b61953b971b010d2185a561f4ce82a"}, + {file = "lupa-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2116eb467797d5a134b2c997dfc7974b9a84b3aa5776c17ba8578ed4f5f41a9b"}, + {file = "lupa-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:24d6c3435d38614083d197f3e7bcfe6d3d9eb02ee393d60a4ab9c719bc000162"}, + {file = "lupa-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9144ecfa5e363f03e4d1c1e678b081cd223438be08f96604fca478591c3e3b53"}, + {file = "lupa-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:69be1d6c3f3ab9fc988c9a0e5801f23f68e2c8b5900a8fd3ae57d1d0e9c5539c"}, + {file = "lupa-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:77b587043d0bee9cc738e00c12718095cf808dd269b171f852bd82026c664c69"}, + {file = "lupa-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:62530cf0a9c749a3cd13ad92b31eaf178939d642b6176b46cfcd98f6c5006383"}, + {file = "lupa-1.14.1-cp39-cp39-win32.whl", hash = "sha256:d891b43b8810191eb4c42a0bc57c32f481098029aac42b176108e09ffe118cdc"}, + {file = "lupa-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:cf643bc48a152e2c572d8be7fc1de1c417a6a9648d337ffedebf00f57016b786"}, + {file = "lupa-1.14.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0ac862c6d2eb542ac70d294a8e960b9ae7f46297559733b4c25f9e3c945e522a"}, + {file = "lupa-1.14.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:0a15680f425b91ec220eb84b0ab59d24c4bee69d15b88245a6998a7d38c78ba6"}, + {file = "lupa-1.14.1-pp37-pypy37_pp73-win32.whl", hash = "sha256:8a064d72991ba53aeea9720d95f2055f7f8a1e2f35b32a35d92248b63a94bcd1"}, + {file = "lupa-1.14.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6d87d6c51e6c3b6326d18af83e81f4860ba0b287cda1101b1ab8562389d598f5"}, + {file = "lupa-1.14.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b3efe9d887cfdf459054308ecb716e0eb11acb9a96c3022ee4e677c1f510d244"}, + {file = "lupa-1.14.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:723fff6fcab5e7045e0fa79014729577f98082bd1fd1050f907f83a41e4c9865"}, + {file = "lupa-1.14.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:930092a27157241d07d6d09ff01d5530a9e4c0dd515228211f2902b7e88ec1f0"}, + {file = "lupa-1.14.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f6bc9852bdf7b16840c984a1e9f952815f7d4b3764585d20d2e062bd1128074"}, + {file = "lupa-1.14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:8f65d2007092a04616c215fea5ad05ba8f661bd0f45cde5265d27150f64d3dd8"}, + {file = "lupa-1.14.1.tar.gz", hash = "sha256:d0fd4e60ad149fe25c90530e2a0e032a42a6f0455f29ca0edb8170d6ec751c6e"}, ] lxml = [ {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, @@ -1779,8 +1798,8 @@ pip-licenses = [ {file = "pip_licenses-3.5.4-py3-none-any.whl", hash = "sha256:5e23593c670b8db616b627c68729482a65bb88498eefd8df337762fdaf7936a8"}, ] platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, + {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1791,40 +1810,22 @@ pre-commit = [ {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, ] psutil = [ - {file = "psutil-5.9.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:8f024fbb26c8daf5d70287bb3edfafa22283c255287cf523c5d81721e8e5d82c"}, - {file = "psutil-5.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:b2f248ffc346f4f4f0d747ee1947963613216b06688be0be2e393986fe20dbbb"}, - {file = "psutil-5.9.2-cp27-cp27m-win32.whl", hash = "sha256:b1928b9bf478d31fdffdb57101d18f9b70ed4e9b0e41af751851813547b2a9ab"}, - {file = "psutil-5.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:404f4816c16a2fcc4eaa36d7eb49a66df2d083e829d3e39ee8759a411dbc9ecf"}, - {file = "psutil-5.9.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:94e621c6a4ddb2573d4d30cba074f6d1aa0186645917df42c811c473dd22b339"}, - {file = "psutil-5.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:256098b4f6ffea6441eb54ab3eb64db9ecef18f6a80d7ba91549195d55420f84"}, - {file = "psutil-5.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:614337922702e9be37a39954d67fdb9e855981624d8011a9927b8f2d3c9625d9"}, - {file = "psutil-5.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39ec06dc6c934fb53df10c1672e299145ce609ff0611b569e75a88f313634969"}, - {file = "psutil-5.9.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3ac2c0375ef498e74b9b4ec56df3c88be43fe56cac465627572dbfb21c4be34"}, - {file = "psutil-5.9.2-cp310-cp310-win32.whl", hash = "sha256:e4c4a7636ffc47b7141864f1c5e7d649f42c54e49da2dd3cceb1c5f5d29bfc85"}, - {file = "psutil-5.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:f4cb67215c10d4657e320037109939b1c1d2fd70ca3d76301992f89fe2edb1f1"}, - {file = "psutil-5.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dc9bda7d5ced744622f157cc8d8bdd51735dafcecff807e928ff26bdb0ff097d"}, - {file = "psutil-5.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75291912b945a7351d45df682f9644540d564d62115d4a20d45fa17dc2d48f8"}, - {file = "psutil-5.9.2-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4018d5f9b6651f9896c7a7c2c9f4652e4eea53f10751c4e7d08a9093ab587ec"}, - {file = "psutil-5.9.2-cp36-cp36m-win32.whl", hash = "sha256:f40ba362fefc11d6bea4403f070078d60053ed422255bd838cd86a40674364c9"}, - {file = "psutil-5.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9770c1d25aee91417eba7869139d629d6328a9422ce1cdd112bd56377ca98444"}, - {file = "psutil-5.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42638876b7f5ef43cef8dcf640d3401b27a51ee3fa137cb2aa2e72e188414c32"}, - {file = "psutil-5.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91aa0dac0c64688667b4285fa29354acfb3e834e1fd98b535b9986c883c2ce1d"}, - {file = "psutil-5.9.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fb54941aac044a61db9d8eb56fc5bee207db3bc58645d657249030e15ba3727"}, - {file = "psutil-5.9.2-cp37-cp37m-win32.whl", hash = "sha256:7cbb795dcd8ed8fd238bc9e9f64ab188f3f4096d2e811b5a82da53d164b84c3f"}, - {file = "psutil-5.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5d39e3a2d5c40efa977c9a8dd4f679763c43c6c255b1340a56489955dbca767c"}, - {file = "psutil-5.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd331866628d18223a4265371fd255774affd86244fc307ef66eaf00de0633d5"}, - {file = "psutil-5.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b315febaebae813326296872fdb4be92ad3ce10d1d742a6b0c49fb619481ed0b"}, - {file = "psutil-5.9.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7929a516125f62399d6e8e026129c8835f6c5a3aab88c3fff1a05ee8feb840d"}, - {file = "psutil-5.9.2-cp38-cp38-win32.whl", hash = "sha256:561dec454853846d1dd0247b44c2e66a0a0c490f937086930ec4b8f83bf44f06"}, - {file = "psutil-5.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:67b33f27fc0427483b61563a16c90d9f3b547eeb7af0ef1b9fe024cdc9b3a6ea"}, - {file = "psutil-5.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3591616fa07b15050b2f87e1cdefd06a554382e72866fcc0ab2be9d116486c8"}, - {file = "psutil-5.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b29f581b5edab1f133563272a6011925401804d52d603c5c606936b49c8b97"}, - {file = "psutil-5.9.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4642fd93785a29353d6917a23e2ac6177308ef5e8be5cc17008d885cb9f70f12"}, - {file = "psutil-5.9.2-cp39-cp39-win32.whl", hash = "sha256:ed29ea0b9a372c5188cdb2ad39f937900a10fb5478dc077283bf86eeac678ef1"}, - {file = "psutil-5.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:68b35cbff92d1f7103d8f1db77c977e72f49fcefae3d3d2b91c76b0e7aef48b8"}, - {file = "psutil-5.9.2.tar.gz", hash = "sha256:feb861a10b6c3bb00701063b37e4afc754f8217f0f09c42280586bd6ac712b5c"}, -] -PTable = [ + {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, + {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, + {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, + {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, + {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, + {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, + {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, + {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, +] +ptable = [ {file = "PTable-0.9.2.tar.gz", hash = "sha256:aa7fc151cb40f2dabcd2275ba6f7fd0ff8577a86be3365cd3fb297cbe09cc292"}, ] py = [ @@ -1928,6 +1929,10 @@ pydantic = [ {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] +pydis-core = [ + {file = "pydis_core-9.1.1-py3-none-any.whl", hash = "sha256:db6e29c8eb4c12ce29fbf57f7b7214a1bdf702f9464320f4c39fef4dc8741bc0"}, + {file = "pydis_core-9.1.1.tar.gz", hash = "sha256:a73fe400d9b7cea5d87cfb53d1bb9ca8fd7fd3b48c1c1b74c43117319acefdac"}, +] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, @@ -1976,7 +1981,7 @@ python-frontmatter = [ {file = "python-frontmatter-1.0.0.tar.gz", hash = "sha256:e98152e977225ddafea6f01f40b4b0f1de175766322004c826ca99842d19a7cd"}, {file = "python_frontmatter-1.0.0-py3-none-any.whl", hash = "sha256:766ae75f1b301ffc5fe3494339147e0fd80bc3deff3d7590a93991978b579b08"}, ] -PyYAML = [ +pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -2229,8 +2234,8 @@ sentry-sdk = [ {file = "sentry_sdk-1.9.10-py2.py3-none-any.whl", hash = "sha256:2469240f6190aaebcb453033519eae69cfe8cc602065b4667e18ee14fc1e35dc"}, ] setuptools = [ - {file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"}, - {file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"}, + {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, + {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, ] sgmllib3k = [ {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, @@ -2252,8 +2257,8 @@ soupsieve = [ {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, ] statsd = [ - {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, - {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, + {file = "statsd-4.0.1-py2.py3-none-any.whl", hash = "sha256:c2676519927f7afade3723aca9ca8ea986ef5b059556a980a867721ca69df093"}, + {file = "statsd-4.0.1.tar.gz", hash = "sha256:99763da81bfea8daf6b3d22d11aaccb01a8d0f52ea521daab37e758a4ca7d128"}, ] taskipy = [ {file = "taskipy-1.10.3-py3-none-any.whl", hash = "sha256:4c0070ca53868d97989f7ab5c6f237525d52ee184f9b967576e8fe427ed9d0b8"}, @@ -2276,12 +2281,12 @@ typing-extensions = [ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, ] virtualenv = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, + {file = "virtualenv-20.16.7-py3-none-any.whl", hash = "sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29"}, + {file = "virtualenv-20.16.7.tar.gz", hash = "sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e"}, ] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, diff --git a/pyproject.toml b/pyproject.toml index 2019d847ac..f6a31cdf8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" python = "3.10.*" # See https://bot-core.pythondiscord.com/ for docs. -bot-core = { url = "https://github.com/python-discord/bot-core/archive/refs/tags/v8.2.1.zip", extras = ["async-rediscache"] } +pydis_core = { version = "9.1.1", extras = ["async-rediscache"] } redis = "4.3.4" fakeredis = { version = "1.9.3", extras = ["lua"] } @@ -34,7 +34,6 @@ pyyaml = "6.0" rapidfuzz = "2.11.1" regex = "2022.9.13" sentry-sdk = "1.9.10" -statsd = "3.3.0" tldextract = "3.4.0" pydantic = "1.10.2" From e4a8593e744cbdf370a1f20df67a06add00a2425 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 11:04:41 +0000 Subject: [PATCH 25/50] Bump all deps to latest --- poetry.lock | 674 +++++++++++++++++++++---------------------------- pyproject.toml | 32 +-- 2 files changed, 303 insertions(+), 403 deletions(-) diff --git a/poetry.lock b/poetry.lock index b273319597..4461e8b3fc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -143,11 +143,11 @@ unicode-backport = ["unicodedata2"] [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coloredlogs" @@ -179,31 +179,17 @@ toml = ["tomli"] [[package]] name = "deepdiff" -version = "5.8.1" -description = "Deep Difference and Search of any Python object/data." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -ordered-set = ">=4.1.0,<4.2.0" - -[package.extras] -cli = ["clevercsv (==0.7.1)", "click (==8.0.3)", "pyyaml (==5.4.1)", "toml (==0.10.2)"] - -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +version = "6.2.1" +description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" [package.dependencies] -wrapt = ">=1.10,<2" +ordered-set = ">=4.0.2,<4.2.0" [package.extras] -dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] +cli = ["clevercsv (==0.7.4)", "click (==8.1.3)", "pyyaml (==6.0)", "toml (==0.10.2)"] [[package]] name = "discord-py" @@ -232,7 +218,7 @@ python-versions = "*" [[package]] name = "emoji" -version = "2.1.0" +version = "2.2.0" description = "Emoji for Python" category = "main" optional = false @@ -241,6 +227,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] dev = ["coverage", "coveralls", "pytest"] +[[package]] +name = "exceptiongroup" +version = "1.0.4" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "execnet" version = "1.9.0" @@ -254,7 +251,7 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.9.3" +version = "2.0.0" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false @@ -262,7 +259,7 @@ python-versions = ">=3.7,<4.0" [package.dependencies] lupa = {version = ">=1.13,<2.0", optional = true, markers = "extra == \"lua\""} -redis = "<4.4" +redis = "<4.5" sortedcontainers = ">=2.4.0,<3.0.0" [package.extras] @@ -294,16 +291,16 @@ testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pyt [[package]] name = "flake8" -version = "5.0.4" +version = "6.0.0" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8.1" [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" +pycodestyle = ">=2.10.0,<2.11.0" +pyflakes = ">=3.0.0,<3.1.0" [[package]] name = "flake8-annotations" @@ -319,18 +316,18 @@ flake8 = ">=3.7" [[package]] name = "flake8-bugbear" -version = "22.9.23" +version = "22.10.27" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" flake8 = ">=3.0.0" [package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"] [[package]] name = "flake8-docstrings" @@ -346,7 +343,7 @@ pydocstyle = ">=2.1" [[package]] name = "flake8-isort" -version = "5.0.0" +version = "5.0.3" description = "flake8 plugin that integrates isort ." category = "dev" optional = false @@ -496,11 +493,11 @@ python-versions = ">=3.6" [[package]] name = "more-itertools" -version = "8.14.0" +version = "9.0.0" description = "More routines for operating on iterables, beyond itertools" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [[package]] name = "mslex" @@ -564,14 +561,14 @@ flake8 = ">=3.9.1" [[package]] name = "pip-licenses" -version = "3.5.4" +version = "4.0.1" description = "Dump the software license list of Python packages installed with pip." category = "dev" optional = false -python-versions = "~=3.6" +python-versions = "~=3.8" [package.dependencies] -PTable = "*" +prettytable = ">=2.3.0" [package.extras] test = ["docutils", "pytest-cov", "pytest-pycodestyle", "pytest-runner"] @@ -617,31 +614,29 @@ toml = "*" virtualenv = ">=20.0.8" [[package]] -name = "psutil" -version = "5.9.4" -description = "Cross-platform lib for process and system monitoring in Python." +name = "prettytable" +version = "3.5.0" +description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" + +[package.dependencies] +wcwidth = "*" [package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"] [[package]] -name = "ptable" -version = "0.9.2" -description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" +name = "psutil" +version = "5.9.4" +description = "Cross-platform lib for process and system monitoring in Python." category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "pycares" @@ -659,7 +654,7 @@ idna = ["idna (>=2.1)"] [[package]] name = "pycodestyle" -version = "2.9.1" +version = "2.10.0" description = "Python style guide checker" category = "dev" optional = false @@ -721,7 +716,7 @@ toml = ["toml"] [[package]] name = "pyflakes" -version = "2.5.0" +version = "3.0.1" description = "passive checker of Python programs" category = "dev" optional = false @@ -748,7 +743,7 @@ python-versions = "*" [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -757,11 +752,11 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -781,32 +776,20 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] -[[package]] -name = "pytest-forked" -version = "1.4.0" -description = "run tests in isolated forked subprocesses" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -py = "*" -pytest = ">=3.10" - [[package]] name = "pytest-subtests" -version = "0.8.0" +version = "0.9.0" description = "unittest subTest() support and subtests fixture" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] pytest = ">=7.0" [[package]] name = "pytest-xdist" -version = "2.5.0" +version = "3.0.2" description = "pytest xdist plugin for distributed testing and loop-on-failing modes" category = "dev" optional = false @@ -815,7 +798,6 @@ python-versions = ">=3.6" [package.dependencies] execnet = ">=1.1" pytest = ">=6.2.0" -pytest-forked = "*" [package.extras] psutil = ["psutil (>=3.0)"] @@ -869,18 +851,18 @@ python-versions = ">=3.6" [[package]] name = "rapidfuzz" -version = "2.11.1" +version = "2.13.2" description = "rapid fuzzy string matching" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] full = ["numpy"] [[package]] name = "redis" -version = "4.3.4" +version = "4.3.5" description = "Python client for Redis database and key-value store" category = "main" optional = false @@ -888,7 +870,6 @@ python-versions = ">=3.6" [package.dependencies] async-timeout = ">=4.0.2" -deprecated = ">=1.2.3" packaging = ">=20.4" [package.extras] @@ -897,7 +878,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "regex" -version = "2022.9.13" +version = "2022.10.31" description = "Alternative regular expression module, to replace re." category = "main" optional = false @@ -935,7 +916,7 @@ six = "*" [[package]] name = "sentry-sdk" -version = "1.9.10" +version = "1.11.1" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false @@ -957,6 +938,7 @@ fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)"] httpx = ["httpx (>=0.16.0)"] pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] rq = ["rq (>=0.6)"] @@ -1109,12 +1091,12 @@ docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sp testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = "*" [[package]] name = "yarl" @@ -1131,7 +1113,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.10.*" -content-hash = "b35ea1d531f76e14f06bbed58d6797cc139064fe146f4142a622876f6f2fbe86" +content-hash = "8c741bfe48fa990572979642dbb94c170385a15793c234044a08cde3dce629e1" [metadata.files] aiodns = [ @@ -1330,8 +1312,8 @@ charset-normalizer = [ {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coloredlogs = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, @@ -1390,12 +1372,8 @@ coverage = [ {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] deepdiff = [ - {file = "deepdiff-5.8.1-py3-none-any.whl", hash = "sha256:e9aea49733f34fab9a0897038d8f26f9d94a97db1790f1b814cced89e9e0d2b7"}, - {file = "deepdiff-5.8.1.tar.gz", hash = "sha256:8d4eb2c4e6cbc80b811266419cb71dd95a157094a3947ccf937a94d44943c7b8"}, -] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, + {file = "deepdiff-6.2.1-py3-none-any.whl", hash = "sha256:8ba27c185f9197b78c316ce7bb0c743d25d14f7cdb8ec3b340437dbc93dcbff2"}, + {file = "deepdiff-6.2.1.tar.gz", hash = "sha256:3fe134dde5b3922ff8c51fc1e95a972e659c853797231b836a5ccf15532fd516"}, ] discord-py = [ {file = "discord.py-2.1.0-py3-none-any.whl", hash = "sha256:a2cfa9f09e3013aaaa43600cc8dfaf67c532dd34afcb71e550f5a0dc9133a5e0"}, @@ -1406,15 +1384,19 @@ distlib = [ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] emoji = [ - {file = "emoji-2.1.0.tar.gz", hash = "sha256:56a8c5e906c11694eb7694b78e5452d745030869b3945f6306a8151ff5cdbc39"}, + {file = "emoji-2.2.0.tar.gz", hash = "sha256:a2986c21e4aba6b9870df40ef487a17be863cb7778dcf1c01e25917b7cd210bb"}, +] +exceptiongroup = [ + {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, + {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, ] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] fakeredis = [ - {file = "fakeredis-1.9.3-py3-none-any.whl", hash = "sha256:74a2f1e5e8781014418fe734b156808d5d1a2d15edec982fada3d6e7603f8536"}, - {file = "fakeredis-1.9.3.tar.gz", hash = "sha256:ea7e4ed076def2eea36188662586a9f2271946ae56ebc2de6a998c82b33df776"}, + {file = "fakeredis-2.0.0-py3-none-any.whl", hash = "sha256:fb3186cbbe4c549f922b0f08eb84b09c0e51ecf8efbed3572d20544254f93a97"}, + {file = "fakeredis-2.0.0.tar.gz", hash = "sha256:6d1dc2417921b7ce56a80877afa390d6335a3154146f201a86e3a14417bdc79e"}, ] feedparser = [ {file = "feedparser-6.0.10-py3-none-any.whl", hash = "sha256:79c257d526d13b944e965f6095700587f27388e50ea16fd245babe4dfae7024f"}, @@ -1425,24 +1407,24 @@ filelock = [ {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] flake8 = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, + {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, + {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, ] flake8-annotations = [ {file = "flake8-annotations-2.9.1.tar.gz", hash = "sha256:11f09efb99ae63c8f9d6b492b75fe147fbc323179fddfe00b2e56eefeca42f57"}, {file = "flake8_annotations-2.9.1-py3-none-any.whl", hash = "sha256:a4385158a7a9fc8af1d8820a2f4c8d03387997006a83f5f8bfe5bc6085bdf88a"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-22.9.23.tar.gz", hash = "sha256:17b9623325e6e0dcdcc80ed9e4aa811287fcc81d7e03313b8736ea5733759937"}, - {file = "flake8_bugbear-22.9.23-py3-none-any.whl", hash = "sha256:cd2779b2b7ada212d7a322814a1e5651f1868ab0d3f24cc9da66169ab8fda474"}, + {file = "flake8-bugbear-22.10.27.tar.gz", hash = "sha256:a6708608965c9e0de5fff13904fed82e0ba21ac929fe4896459226a797e11cd5"}, + {file = "flake8_bugbear-22.10.27-py3-none-any.whl", hash = "sha256:6ad0ab754507319060695e2f2be80e6d8977cfcea082293089a9226276bd825d"}, ] flake8-docstrings = [ {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, ] flake8-isort = [ - {file = "flake8-isort-5.0.0.tar.gz", hash = "sha256:e336f928c7edc509684930ab124414194b7f4e237c712af8fcbdf49d8747b10c"}, - {file = "flake8_isort-5.0.0-py3-none-any.whl", hash = "sha256:c73f9cbd1bf209887f602a27b827164ccfeba1676801b2aa23cb49051a1be79c"}, + {file = "flake8-isort-5.0.3.tar.gz", hash = "sha256:0951398c343c67f4933407adbbfb495d4df7c038650c5d05753a006efcfeb390"}, + {file = "flake8_isort-5.0.3-py3-none-any.whl", hash = "sha256:8c4ab431d87780d0c8336e9614e50ef11201bc848ef64ca017532dec39d4bf49"}, ] flake8-string-format = [ {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, @@ -1709,8 +1691,8 @@ mccabe = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] more-itertools = [ - {file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"}, - {file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"}, + {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, + {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, ] mslex = [ {file = "mslex-0.3.0-py2.py3-none-any.whl", hash = "sha256:380cb14abf8fabf40e56df5c8b21a6d533dc5cbdcfe42406bbf08dda8f42e42a"}, @@ -1794,8 +1776,8 @@ pep8-naming = [ {file = "pep8_naming-0.13.2-py3-none-any.whl", hash = "sha256:59e29e55c478db69cffbe14ab24b5bd2cd615c0413edf790d47d3fb7ba9a4e23"}, ] pip-licenses = [ - {file = "pip-licenses-3.5.4.tar.gz", hash = "sha256:a8b4dabe2b83901f9ac876afc47b57cff9a5ebe19a6d90c0b2579fa8cf2db176"}, - {file = "pip_licenses-3.5.4-py3-none-any.whl", hash = "sha256:5e23593c670b8db616b627c68729482a65bb88498eefd8df337762fdaf7936a8"}, + {file = "pip-licenses-4.0.1.tar.gz", hash = "sha256:05a180f5610b262e2d56eea99f04e380db7080e79655abf1c916125f39fe207d"}, + {file = "pip_licenses-4.0.1-py3-none-any.whl", hash = "sha256:5896c18b7897e38fdd7be9a9ea0de02d6ff3264b7411967d6b679019ddc31878"}, ] platformdirs = [ {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, @@ -1809,6 +1791,10 @@ pre-commit = [ {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, ] +prettytable = [ + {file = "prettytable-3.5.0-py3-none-any.whl", hash = "sha256:fe391c3b545800028edf5dbb6a5360893feb398367fcc1cf8d7a5b29ce5c59a1"}, + {file = "prettytable-3.5.0.tar.gz", hash = "sha256:52f682ba4efe29dccb38ff0fe5bac8a23007d0780ff92a8b85af64bc4fc74d72"}, +] psutil = [ {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, @@ -1825,13 +1811,6 @@ psutil = [ {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, ] -ptable = [ - {file = "PTable-0.9.2.tar.gz", hash = "sha256:aa7fc151cb40f2dabcd2275ba6f7fd0ff8577a86be3365cd3fb297cbe09cc292"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] pycares = [ {file = "pycares-4.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5dc6418e87729105d93162155793002b3fa95490e2f2df33afec08b0b0d44989"}, {file = "pycares-4.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9481ee42df7e34c9ef7b2f045e534062b980b2c971677868df9f17730b147ceb"}, @@ -1884,8 +1863,8 @@ pycares = [ {file = "pycares-4.2.2.tar.gz", hash = "sha256:e1f57a8004370080694bd6fb969a1ffc9171a59c6824d54f791c1b2e4d298385"}, ] pycodestyle = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, + {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, + {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, ] pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, @@ -1938,8 +1917,8 @@ pydocstyle = [ {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] pyflakes = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, + {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, + {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, ] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, @@ -1950,24 +1929,20 @@ pyreadline3 = [ {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-cov = [ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, ] -pytest-forked = [ - {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, - {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, -] pytest-subtests = [ - {file = "pytest-subtests-0.8.0.tar.gz", hash = "sha256:46eb376022e926950816ccc23502de3277adcc1396652ddb3328ce0289052c4d"}, - {file = "pytest_subtests-0.8.0-py3-none-any.whl", hash = "sha256:4e28ca52cf7a46645c1ded7933745b69334cdc97a412ed4431f7be7cef9a0994"}, + {file = "pytest-subtests-0.9.0.tar.gz", hash = "sha256:c0317cd5f6a5eb3e957e89dbe4fc3322a9afddba2db8414355ed2a2cb91a844e"}, + {file = "pytest_subtests-0.9.0-py3-none-any.whl", hash = "sha256:f5f616b92c13405909d210569d6d3914db6fe156333ff5426534f97d5b447861"}, ] pytest-xdist = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, + {file = "pytest-xdist-3.0.2.tar.gz", hash = "sha256:688da9b814370e891ba5de650c9327d1a9d861721a524eb917e620eec3e90291"}, + {file = "pytest_xdist-3.0.2-py3-none-any.whl", hash = "sha256:9feb9a18e1790696ea23e1434fa73b325ed4998b0e9fcb221f16fd1945e6df1b"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -2024,202 +1999,189 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] rapidfuzz = [ - {file = "rapidfuzz-2.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:134a467692216e05a8806efe40e3bcae9aa81b9e051b209a4244b639a168c78e"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2bc3ec87df5eaad59e6e02e6517047fb268a48866f3531c4b8b59c2c78069fe5"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65b8611c9f5385a2986e11e85137cdecf40610e5d5f250d96a9ed32b7e995c4a"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1477455b82d6db7336ef769f507a55bba9fe9f1c96dc531d7c2c510630307d6"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dafe8c6e74fea0fdcfec002bc77aee40b4891b14ea513e6092402609ac8dac00"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24569412e1aac1ac008548cdcd40da771e14467f4bacab9f9abfe5bbb5dfe8be"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e3164736ed071dc743994b9228ead52b63010aba24b1621de81b3ac39d490b9"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b480a78227457a0b65e0b23afbda9c152dee4e1b41ccc058db8c41ea7a82ab0"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bd595bd23a4e1c72d5f5ac416ea49b9a3d87e11fb2db4b960378038ce9bb12f7"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:30773e23bebe27ddcf7644d6ebb143bf7c9adeb18019a963172174ef522c0831"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:dc0f695b32700b14f404cccaebc25eea6db323418385568297995aee8b5278f8"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2d3652804ae17920eaa965b1e057ee0ea32d5bb02f50147c82a1d350a86fc3f1"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9081542fea2baeebda8caa43a54ecd8a152a05ff3271c38ac8eae447377cef54"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-win32.whl", hash = "sha256:ea5bc5bae1cf447b79be04f05e73b6ea39a5df63374f70cc5d6862337462d4d9"}, - {file = "rapidfuzz-2.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:86c34175830cacac1c16d2182a0f725afbd40042955b7572c8475e3b6a5d8ada"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:588dd5f520683af53a9d9d0cabde0987788c0ea9adfda3b058a9c27f448b2b3f"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d1d8192820d8489a8e3ef160cbe38f5ff974db5263c76438cf44e7574743353b"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ced719fcae6f2a348ac596b67f6d7c26ff3d9d2b7378237953ac5e162d8a4e2e"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f71edc8503d08bc5d35187eb72f13b7ec78647f1c14bb90a758ae795b049f788"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705ccd8de2b7b5295c6a230a3919fc9db8da9d2a6347c15c871fcb2202abd237"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d24181dfdfcc3d9b37333fea2f5bf9f51e034bd9e0ba67a871f18686b797c739"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecfe2fe942edabcd1553701237710de296d3eb45472f9128662c95da98e9ed43"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e8d37d67a6e4713ddb6053eb3007a3ca15eddd23f2e4a5039c39e666c10b3a"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:804c7c67dc316f77b01b9bef5e75f727b73ff1015ff0514972b59dc05eec4d81"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8d6fa1d009fcb9a9169548c29d65a1f05c0fcf1ac966f40e35035307d6a17050"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:bd7a1992e91c90197c34ccc674bd64262262627083c99896b79e2c9f5fe28075"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c288e239fc3aaae3865e43e1f35b606f92ee687a0801e4d46c45d7849aebbe35"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eec5ad2f06701e57a2cb483c849704bdf8ea76195918550ab2fc4287970f1c76"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-win32.whl", hash = "sha256:d51b9183ebce60d4795ceaef24b8db2df3ed04307ee787d6adafcc196330a47c"}, - {file = "rapidfuzz-2.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:553e8e3dce321ed33e8b437586e7765d78e6d8fbb236b02768b46e1b2b91b41e"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3b6573607568438dfc3d4341b0b00d326ac2cf86281df97e7f8c0348e2f89b5e"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:769cf4099f53507231ba04cbf9ee16bea3c193767efc9bdf5e6c59e67e6b5cea"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6e6395404b0239cff7873a18a94839343a44429624f2a70a27b914cc5059580"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5cd1ea9fa396243d34f7bac5bb5787f89310f13fd2b092d11940c6cd7bd0bd8"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bc00bd6b6407dc7a8eb31964bcc38862c25e7f5f5982f912f265eb3c4d83140"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f91b867d7eca3b99c25e06e7e3a6f84cd4ccb99f390721670ba956f79167c9"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7dd6a439fb09dc9ba463de3f5c8e20f097225816b33a66380b68c8561a08045c"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3f1c030e2d61d77cb14814640618e29cf13e4554340a3baa9191d162a4dfcd9e"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9924497dec6a30b5158ef7cc9c60a87c6c46d9f7b7bb7254d4f157b57b531fb8"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:56aa67bf938e8dcc5e940f183538f09041441f1c4c5a86abe748416950db9d27"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:13ce1019ddce7419502fac43b62ac166d3d6d290b727050e3de5bda79a6beb59"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-win32.whl", hash = "sha256:254d5a800de54c416fa9b220e442a4861b272c1223139ae3dee0aea1c9f27c9c"}, - {file = "rapidfuzz-2.11.1-cp36-cp36m-win_amd64.whl", hash = "sha256:16a2edf3ea888c9d3582761a2bbaa734e03f6db25d96e73edd4dcef6883897ee"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:faba219b270b78e9494cfe3d955d7b45c10799c18ee47ec24b1ada93978d491b"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b12420d5b769cd7e1478a8085aeea1ad0ffc8f7fedc86c48b8d598e1602f5ad"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2fe220d4b100b00734d9388e33296ac8f585c763548c372ca17b24affa178e0"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c10724490b87fcb86161e5ceb17893626d13363e31efee77aa8e251ee16dcdd5"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47e163d6a6676be9a3a7e93d5a2c3c65a43c1530b680903ebdba951e07ee7999"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4f577ded3e40695d5e0796e8b7f4fa78577d873627e0d0692f7060ad73af314"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cc3103e31d27352afe4c5a71702e09185850187d299145d5e98f9fb99a3be498"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2183fc91971c0853f6170225577d24d81b865d416104b433de53e55a6d2a476a"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:86038b9777b2aa0ebf8c586b81cba166ccde7e6d744aad576cd98c1a07be4c53"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:f5ed8d4e1545f08bd3745cc47742b3689f1a652b00590caeb32caf3297d01e06"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bfabc6130752f4f77584b2ecbba2adf6fe469b06c52cb974ba8304f1f63bb24f"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-win32.whl", hash = "sha256:dad6697c6b9e02dd45f73e22646913daad743afd27dadb0b6a430a1573fb4566"}, - {file = "rapidfuzz-2.11.1-cp37-cp37m-win_amd64.whl", hash = "sha256:adc7c6cb3dde5c284d84c7c6f4602b1545ba89c6ebb857b337d0428befb344e5"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:52639268dffc8900892a5e57964228fb187512b0f249de9a45ba37c6f2bc52a5"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a48ff6b6258a32f50f876a6c74fa2f506c1de3b11773d6bf31b6715255807a48"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5080ad715e39b8a2d82339cf4170785e9092c7625ec2095ff3590fdb0a532a41"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b402e99593483a8b05a09fb2a20379ecaa9b0d1f1cf32957b42134bd3305731"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f09ff49b28e557615a9ad4d5eedbfd5b886fccb3ec35d85dd34c51348c4bf98"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8f36d8bd399c7d695182e467b4428adb940a157014ab605bbe4d0ab0a1976e"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e459287f0daaee3ee0108123d7e9a1c1c136e94d4382533a93cb509d54dc1ea3"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c9e2acfa25c7667b70913d63887f76e981badc1e95a2878257d28b96f5a10c"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:931a939ba5e5574f769507038fdf400dbbc46aab2866d4e5e96d83a29f081712"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5e89f50f5f3be2b851e9714015e1a26c6546e6b42f3df69b86200af8eacf9d8c"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:64133c9f45cb88b508d52427339b796c76e1790300c7ea4d2ed210f224e0698d"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3d50a2ca8cd1cea13afd2ff8e052ba49860c64cc3e617398670fd6a8d11e450f"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bafd18a27dbe3197e460809468a7c47d9d29d1ebab6a878d5bb5a71fda2056d6"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-win32.whl", hash = "sha256:c822853e9d54979eb5fcf9e54c1f90e5c18eeb399571383ac768cff47d6d6ada"}, - {file = "rapidfuzz-2.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:a7f5a77466c4701062469bce29358ca0797db2bc6d8f6c3cd4e13f418cca10bc"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bf5277ff74c9980245697ea227057d0f05b31c96bc73bae2697c1a48d4980e45"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6803ef01f4056d61120e37acba8953e6b3149363e85caaba40ee8d49753fe7bd"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:475aacad5d5c4f9ad920b4232cc196d79a1777fe1eada9122103c30154d18af4"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a98c63d1f5ec2c15adf5dc81c461c8d88c16395956f4518b78e2e04b3285b1e5"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a11b70ebb2d7317d69bdb1f692a0eda292a4cddfe9ccb760a8d1a9e763811dd"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:984d40ecda0bc0109c4239d782dfe87362d02b286548672f8a2468eabbf48a69"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c457f779992a0f5527455cdc17c387268ae9f712d4e29d691704c83c6e58c2d"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578934d7524f8378175295e6411b737d35d393d91d4661c739daa8ea2b185836"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9226824c132d38f2337d2c76e3009acc036f0b05f20e95e82f8195400e1e366"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0e64ab58b19866ad3df53e651a429871d744f8794cca25c553396b25d679a1ac"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c77cec595dc80f97a1b32413fb1b618e4da8ba132697e075ad8e4025c4058575"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:5aff0ac1723f7c8d751869a51e6b12d703fd6e6153228d68d8773f19bd5bd968"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17ba5fb474515356608cdb8d750f95c12f3e4dc9a0e2c9d7caca3d4cee55048e"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-win32.whl", hash = "sha256:cad5088f1adb9161f2def653908328cfa1dc9bc57e7e41ccdc9339d31cc576d1"}, - {file = "rapidfuzz-2.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:ea4f0d056a95cfdabde667a1796f9ba5296d2776bce2fd4d4cb5674e0e10671f"}, - {file = "rapidfuzz-2.11.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:036f904bcac16d726273eee7ec0636978af31d151f30c95b611240e22592ab79"}, - {file = "rapidfuzz-2.11.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c7b0a4929bfd3945d9c2022cff0b683a39accf5594897fa9004cee4f402b06"}, - {file = "rapidfuzz-2.11.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72d33b0d76a658d8b692b3e42c45539939bac26ff5b71b516cb20fa6d8ff7f6"}, - {file = "rapidfuzz-2.11.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d181889218d80f6beb5ae3838bc23e201d2a1fae688baaa40d82ef9080594315"}, - {file = "rapidfuzz-2.11.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7750b950a6987bce114b9f36413399712422f4f49b2ad43f4b4ee3af34968b99"}, - {file = "rapidfuzz-2.11.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:12e14b0c43e3bc0c679ef09bfcbcaf9397534e03b8854c417086779a79e08bb2"}, - {file = "rapidfuzz-2.11.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09de4fd3dbcc73f61b85af006372f48fee7d4324de227702b9da0d2572445d26"}, - {file = "rapidfuzz-2.11.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54264d70af59224d6874fcc5828da50d99668055574fe254849cab96f3b80e43"}, - {file = "rapidfuzz-2.11.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7478341137e65a0227fda4f3e39b3d50e6ec7dd4f767077dd435b412c2f2c129"}, - {file = "rapidfuzz-2.11.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:68d46ad148c9cb8be532b5dd7bc246b067e81d4cfabad19b4cb6ac4031cab124"}, - {file = "rapidfuzz-2.11.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea4107a5cc00a05c92be47047662000296d2ccc7ba93aaa030cd5ecab8d5ffaf"}, - {file = "rapidfuzz-2.11.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8a5e65cab629ca5bb4b1d2b410f8444384b60364ab528508200acfdf9e659d"}, - {file = "rapidfuzz-2.11.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c305ea5405f8615e6ecd39cb28acc7a362713ba3c17c7737b591b377d1afd9ec"}, - {file = "rapidfuzz-2.11.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0429a7a51d1372afaca969ee3170f9975f2fe6e187b485aeef55d3e8d7d934e0"}, - {file = "rapidfuzz-2.11.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:42d18db6f7e1e6ef85a8e673b2fa3352727cc56e60e48e7c9268fe0286ab9f91"}, - {file = "rapidfuzz-2.11.1.tar.gz", hash = "sha256:61152fa1e3df04b4e748f09338f36ca32f7953829f4e630d26f7f564f4cb527b"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91c049f7591d9e9f8bcc3c556c0c4b448223f564ad04511a8719d28f5d38daed"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:26e4b7f7941b92546a9b06ed75b40b5d7ceace8f3074d06cb3369349388d700d"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba2a8fbd21079093118c40e8e80068750c1619a5988e54220ea0929de48e7d65"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de707808f1997574014d9ba87c2d9f8a619688d615520e3dce958bf4398514c7"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba3f47a5b82de7304ae08e2a111ccc90a6ea06ecc3f25d7870d08be0973c94cb"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a181b6ef9b480b56b29bdc58dc50c198e93d33398d2f8e57da05cbddb095bd9e"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1e569953a2abe945f116a6c22b71e8fc02d7c27068af2af40990115f25c93e4"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:026f6ecd8948e168a89fc015ef34b6bcb200f30ac33f1480554d722181b38bea"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daf5e4f6b048c225a494c941a21463a0d397c39a080db8fece9b3136297ed240"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e39ae60598ed533f513db6d0370755685666024ab187a144fc688dd16cfa2d33"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e8d71f1611431c445ced872b303cd61f215551a11df0c7171e5993bed84867d5"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f5d07dca69bf5a9f1e1cd5756ded6c197a27e8d8f2d8a3d99565add37a3bd1ec"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ac95981911559c842e1e4532e2f89ca255531db1d87257e5e69cd8c0c0d585fc"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-win32.whl", hash = "sha256:b4162b96d0908cb0ca218513eab559e9a77c8a1d9705c9133813634d9db27f4f"}, + {file = "rapidfuzz-2.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:84fd3cfc1cb872019e60a3844b1deedb176de0b9ded11bf30147137ac65185f5"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a599cc5cec196c0776faf65b74ac957354bd036f878905a16be9e20884870d02"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dbad2b7dad98b854a468d2c6a0b11464f68ce841428aded2f24f201a17a144eb"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad78fb90540dc752b532345065146371acd3804a917c31fdd8a337951da9def2"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed0f99e0037b7f9f7117493e8723851c9eece4629906b2d5da21d3ef124149a2"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9abdffc590ef08d27dfd14d32e571f4a0f5f797f433f00c5faf4cf56ab62792a"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352c920e166e838bc560014885ba979df656938fcc29a12c73ff06dc76b150d8"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c40acbadc965e72f1b44b3c665a59ec78a5e959757e52520bf73687c84ce6854"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4053d5b62cedec83ff67d55e50da35f7736bed0a3b2af51fa6143f5fef3785"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0c324d82871fe50471f7ba38a21c3e68167e868f541f57ac0ef23c053bbef6e6"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cb4bd75518838b141dab8fe663de988c4d08502999068dc0b3949d43bd86ace6"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4b785ffbc16795fca27c9e899993df7721d886249061689c48dbfe60fa7d02a1"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:1f363bf95d79dbafa8eac17697965e02e74da6f21b231b3fb808b2185bfed337"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f7cfc25d8143a7570f5e4c9da072a1e1c335d81a6926eb10c1fd3f637fa3c022"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-win32.whl", hash = "sha256:580f32cda7f911fef8266c7d811e580c18734cd12308d099b9975b914f33fcaf"}, + {file = "rapidfuzz-2.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:98be3e873c8f9d90a982891b2b061521ba4e5e49552ba2d3c1b0806dd5677f88"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:de8ec700127b645b0e2e28e694a2bba6dcb6a305ef080ad312f3086d47fb6973"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ec73e6d3ad9442cfb5b94c137cf4241fff2860d81a9ee8be8c3d987bb400c0"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da5b7f35fc824cff36a2baa62486d5b427bf0fd7714c19704b5a7df82c2950b4"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f186b3a32d78af7a805584a7e1c2fdf6f6fd62939936e4f3df869158c147a55"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68f2e23eec59fc77bef164157889a2f7fb9800c47d615c58ee3809e2be3c8509"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4291a8c02d32aa6ebdffe63cf91abc2846383de95ae04a275f036c4e7a27f9ba"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a2eeee09ff716c8ff75942c1b93f0bca129590499f1127cbeb1b5cefbdc0c3d5"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2345656b30d7e18d18a4df5b765e4059111860a69bf3a36608a7d625e92567e6"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e1dd1a328464dd2ae70f0e31ec403593fbb1b254bab7ac9f0cd08ba71c797d0"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:54fe1835f96c1033cdb7e4677497e784704c81d028c962d2222239ded93d978b"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6b68b6a12411cfacca16ace22d42ae8e9946315d79f49c6c97089789c235e795"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-win32.whl", hash = "sha256:9a740ddd3f7725c80e500f16b1b02b83a58b47164c0f3ddd9379208629c8c4b5"}, + {file = "rapidfuzz-2.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:378554acdcf8370cc5c777b1312921a2a670f68888e999ea1305599c55b67f5d"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa96955f2878116239db55506fe825f574651a8893d07a83de7b3c76a2f0386e"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4df886481ca27a6d53d30a73625fb86dd308cf7d6d99d32e0dfbfcc8e8a75b9"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c66f3b8e93cdc3063ffd7224cad84951834d9434ffd27fa3fabad2e942ddab7"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d6d5ab0f12f2d7ae6aad77af67ae6253b6c1d54c320484f1acd2fce38b39ac2"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f0574d5d97722cfaf51b7dd667c8c836fa9fdf5a7d8158a787b98ee2788f6c5"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:83ff31d33c1391a0a6b23273b7f839dc8f7b5fb75ddca59ce4f334b83ca822bb"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94d8c65f48665f82064bea8a48ff185409a309ba396f5aec3a846831cbe36e6d"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c065a83883af2a9a0303b6c06844a700af0db97ff6dc894324f656ad8efe405"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:773c60a5368a361253efea194552ff9ed6879756f6feb71b61b514723f8cb726"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:12ece1a4d024297afa4b76d2ce71c2c65fc7eaa487a9ae9f6e17c160253cfd23"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2b491f2fac36718247070c3343f53aadbbe8684f3e0cf3b6cce1bd099e1d05cb"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:31370273787dca851e2df6f32f1ec8c61f86e9bbeb1cc42787020b6dfff952fd"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:47b5b227dc0bd53530dda55f344e1b24087fa99bb1bd7fceb6f5a2b1e2831ad4"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-win32.whl", hash = "sha256:8f09a16ae84b1decb9df9a7e393ec84a0b2a11da6356c3eedcf86da8cabe3071"}, + {file = "rapidfuzz-2.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:e038e187270cbb987cf7c5d4b574fce7a32bc3d9593e9346d129874a7dc08dc3"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:aee5dce78e157e503269121ad6f886acab4b1ab3e3956bcdf0549d54596eab57"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80073e897af0669f496d23899583b5c2f0decc2ec06aa7c36a3b8fb16eda5e0e"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ce40c2a68fe28e05a4f66229c11885ef928086fbcd2eff086decdacfe5254da9"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd268701bf930bbb2d12f6f7f75c681e16fee646ea1663d258e825bf919ca7a1"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5d93e77881497f76e77056feea4c375732d27151151273d6e4cb8a1defbf17a"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b27c3e2b1789a635b9df1d74838ae032dc2dbc596ece5d89f9de2c37ba0a6dfe"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e49f412fe58c793af61b04fb5536534dfc95000b6c2bf0bfa42fcf7eb1453d42"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27bbdee91718019e251d315c6e9b03aa5b7663b90e4228ac1ddb0a567ff3634b"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b51d45cb9ed81669206e338413ba224c06a8900ab0cc9106f4750ac73dc687bb"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3479a2fd88504cc41eb707650e81fd7ce864f2418fee24f7224775b539536b39"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7adb4327453c1550f51d6ba13d718a84091f82230c1d0daca6db628e57d0fa5a"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3a4e87aae287d757d9c5b045c819c985b02b38dea3f75630cc24d53826e640be"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e175b1643306558a3d7604789c4a8c217a64406fe82bf1a9e52efb5dea53ae"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-win32.whl", hash = "sha256:fb896fafa206db4d55f4412135c3ae28fbc56b8afc476970d0c5f29d2ce50948"}, + {file = "rapidfuzz-2.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:37a9a8f5737b8e429291148be67d2dd8ba779a69a87ad95d2785bb3d80fd1df7"}, + {file = "rapidfuzz-2.13.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d6cb51a8459e7160366c6c7b31e8f9a671f7d617591c0ad305f2697707061da2"}, + {file = "rapidfuzz-2.13.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:343fe1fcbbf55c994b22962bfb46f6b6903faeac5a2671b2f0fa5e3664de3e66"}, + {file = "rapidfuzz-2.13.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d9d081cd8e0110661c8a3e728d7b491a903bb54d34de40b17d19144563bd5f6"}, + {file = "rapidfuzz-2.13.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f93a6740fef239a8aca6521cc1891d448664115b53528a3dd7f95c1781a5fa6"}, + {file = "rapidfuzz-2.13.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:deaf26cc23cfbf90650993108de888533635b981a7157a0234b4753527ac6e5c"}, + {file = "rapidfuzz-2.13.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b6a0617ba60f81a8df3b9ddca09f591a0a0c8269402169825fcd50daa03e5c25"}, + {file = "rapidfuzz-2.13.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bee1065d55edfeabdb98211bb673cb44a8b118cded42d743f7d59c07b05a80d"}, + {file = "rapidfuzz-2.13.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e5afd5477332ceeb960e2002d5bb0b04ad00b40037a0ab1de9916041badcf00"}, + {file = "rapidfuzz-2.13.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eead76c172ba08d49ea621016cf84031fff1ee33d7db751d7003e491e55e66af"}, + {file = "rapidfuzz-2.13.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:83b1e8aca6c3fad058d8a2b7653b7496df0c4aca903d589bb0e4184868290767"}, + {file = "rapidfuzz-2.13.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:41610c3a9be4febcbcac2b69b2f45d0da33e39d1194e5ffa3dd3a104d5a67a70"}, + {file = "rapidfuzz-2.13.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aacc4eb58d6bccf6ec571619bee35861d4103961b9873d9b0829d347ca8a63e"}, + {file = "rapidfuzz-2.13.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:791d90aa1c68b5485f6340a8dc485aba7e9bcb729572449174ded0692e7e7ad0"}, + {file = "rapidfuzz-2.13.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d4f94b408c9f9218d61e8af55e43c8102f813eea2cf82de10906b032ddcb9aa"}, + {file = "rapidfuzz-2.13.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ac6a8a34f858f3862798383f51012788df6be823e2874fa426667a4da94ded7e"}, + {file = "rapidfuzz-2.13.2.tar.gz", hash = "sha256:1c67007161655c59e13bba130a2db29d7c9e5c81bcecb8846a3dd7386065eb24"}, ] redis = [ - {file = "redis-4.3.4-py3-none-any.whl", hash = "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54"}, - {file = "redis-4.3.4.tar.gz", hash = "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"}, + {file = "redis-4.3.5-py3-none-any.whl", hash = "sha256:46652271dc7525cd5a9667e5b0ca983c848c75b2b8f7425403395bb8379dcf25"}, + {file = "redis-4.3.5.tar.gz", hash = "sha256:30c07511627a4c5c4d970e060000772f323174f75e745a26938319817ead7a12"}, ] regex = [ - {file = "regex-2022.9.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0394265391a86e2bbaa7606e59ac71bd9f1edf8665a59e42771a9c9adbf6fd4f"}, - {file = "regex-2022.9.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86df2049b18745f3cd4b0f4c4ef672bfac4b80ca488e6ecfd2bbfe68d2423a2c"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce331b076b2b013e7d7f07157f957974ef0b0881a808e8a4a4b3b5105aee5d04"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:360ffbc9357794ae41336b681dff1c0463193199dfb91fcad3ec385ea4972f46"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18e503b1e515a10282b3f14f1b3d856194ecece4250e850fad230842ed31227f"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e167d1ccd41d27b7b6655bb7a2dcb1b1eb1e0d2d662043470bd3b4315d8b2b"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4146cb7ae6029fc83b5c905ec6d806b7e5568dc14297c423e66b86294bad6c39"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a1aec4ae549fd7b3f52ceaf67e133010e2fba1538bf4d5fc5cd162a5e058d5df"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cab548d6d972e1de584161487b2ac1aa82edd8430d1bde69587ba61698ad1cfb"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3d64e1a7e6d98a4cdc8b29cb8d8ed38f73f49e55fbaa737bdb5933db99b9de22"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:67a4c625361db04ae40ef7c49d3cbe2c1f5ff10b5a4491327ab20f19f2fb5d40"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:5d0dd8b06896423211ce18fba0c75dacc49182a1d6514c004b535be7163dca0f"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4318f69b79f9f7d84a7420e97d4bfe872dc767c72f891d4fea5fa721c74685f7"}, - {file = "regex-2022.9.13-cp310-cp310-win32.whl", hash = "sha256:26df88c9636a0c3f3bd9189dd435850a0c49d0b7d6e932500db3f99a6dd604d1"}, - {file = "regex-2022.9.13-cp310-cp310-win_amd64.whl", hash = "sha256:6fe1dd1021e0f8f3f454ce2811f1b0b148f2d25bb38c712fec00316551e93650"}, - {file = "regex-2022.9.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83cc32a1a2fa5bac00f4abc0e6ce142e3c05d3a6d57e23bd0f187c59b4e1e43b"}, - {file = "regex-2022.9.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2effeaf50a6838f3dd4d3c5d265f06eabc748f476e8441892645ae3a697e273"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a786a55d00439d8fae4caaf71581f2aaef7297d04ee60345c3594efef5648a"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b701dbc124558fd2b1b08005eeca6c9160e209108fbcbd00091fcfac641ac7"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab81cc4d58026861445230cfba27f9825e9223557926e7ec22156a1a140d55c"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0c5cc3d1744a67c3b433dce91e5ef7c527d612354c1f1e8576d9e86bc5c5e2"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:518272f25da93e02af4f1e94985f5042cec21557ef3591027d0716f2adda5d0a"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8418ee2cb857b83881b8f981e4c636bc50a0587b12d98cb9b947408a3c484fe7"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cfa4c956ff0a977c4823cb3b930b0a4e82543b060733628fec7ab3eb9b1abe37"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a1c4d17879dd4c4432c08a1ca1ab379f12ab54af569e945b6fc1c4cf6a74ca45"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:77c2879d3ba51e5ca6c2b47f2dcf3d04a976a623a8fc8236010a16c9e0b0a3c7"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2885ec6eea629c648ecc9bde0837ec6b92208b7f36381689937fe5d64a517e8"}, - {file = "regex-2022.9.13-cp311-cp311-win32.whl", hash = "sha256:2dda4b096a6f630d6531728a45bd12c67ec3badf44342046dc77d4897277d4f2"}, - {file = "regex-2022.9.13-cp311-cp311-win_amd64.whl", hash = "sha256:592b9e2e1862168e71d9e612bfdc22c451261967dbd46681f14e76dfba7105fd"}, - {file = "regex-2022.9.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:df8fe00b60e4717662c7f80c810ba66dcc77309183c76b7754c0dff6f1d42054"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995e70bb8c91d1b99ed2aaf8ec44863e06ad1dfbb45d7df95f76ef583ec323a9"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad75173349ad79f9d21e0d0896b27dcb37bfd233b09047bc0b4d226699cf5c87"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7681c49da1a2d4b905b4f53d86c9ba4506e79fba50c4a664d9516056e0f7dfcc"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bc8edc5f8ef0ebb46f3fa0d02bd825bbe9cc63d59e428ffb6981ff9672f6de1"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bee775ff05c9d519195bd9e8aaaccfe3971db60f89f89751ee0f234e8aeac5"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1a901ce5cd42658ab8f8eade51b71a6d26ad4b68c7cfc86b87efc577dfa95602"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:14a7ab070fa3aec288076eed6ed828587b805ef83d37c9bfccc1a4a7cfbd8111"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d23ac6b4bf9e32fcde5fcdb2e1fd5e7370d6693fcac51ee1d340f0e886f50d1f"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:4cdbfa6d2befeaee0c899f19222e9b20fc5abbafe5e9c43a46ef819aeb7b75e5"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ab07934725e6f25c6f87465976cc69aef1141e86987af49d8c839c3ffd367c72"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1371dc73e921f3c2e087c05359050f3525a9a34b476ebc8130e71bec55e97"}, - {file = "regex-2022.9.13-cp36-cp36m-win32.whl", hash = "sha256:fcbd1edff1473d90dc5cf4b52d355cf1f47b74eb7c85ba6e45f45d0116b8edbd"}, - {file = "regex-2022.9.13-cp36-cp36m-win_amd64.whl", hash = "sha256:fe428822b7a8c486bcd90b334e9ab541ce6cc0d6106993d59f201853e5e14121"}, - {file = "regex-2022.9.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d7430f041755801b712ec804aaf3b094b9b5facbaa93a6339812a8e00d7bd53a"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:079c182f99c89524069b9cd96f5410d6af437e9dca576a7d59599a574972707e"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59bac44b5a07b08a261537f652c26993af9b1bbe2a29624473968dd42fc29d56"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a59d0377e58d96a6f11636e97992f5b51b7e1e89eb66332d1c01b35adbabfe8a"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d68eb704b24bc4d441b24e4a12653acd07d2c39940548761e0985a08bc1fff"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0385d66e73cdd4462f3cc42c76a6576ddcc12472c30e02a2ae82061bff132c32"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:db45016364eec9ddbb5af93c8740c5c92eb7f5fc8848d1ae04205a40a1a2efc6"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:03ff695518482b946a6d3d4ce9cbbd99a21320e20d94913080aa3841f880abcd"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6b32b45433df1fad7fed738fe15200b6516da888e0bd1fdd6aa5e50cc16b76bc"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:003a2e1449d425afc817b5f0b3d4c4aa9072dd5f3dfbf6c7631b8dc7b13233de"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a9eb9558e1d0f78e07082d8a70d5c4d631c8dd75575fae92105df9e19c736730"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f6e0321921d2fdc082ef90c1fd0870f129c2e691bfdc4937dcb5cd308aba95c4"}, - {file = "regex-2022.9.13-cp37-cp37m-win32.whl", hash = "sha256:3f3b4594d564ed0b2f54463a9f328cf6a5b2a32610a90cdff778d6e3e561d08b"}, - {file = "regex-2022.9.13-cp37-cp37m-win_amd64.whl", hash = "sha256:8aba0d01e3dfd335f2cb107079b07fdddb4cd7fb2d8c8a1986f9cb8ce9246c24"}, - {file = "regex-2022.9.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:944567bb08f52268d8600ee5bdf1798b2b62ea002cc692a39cec113244cbdd0d"}, - {file = "regex-2022.9.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b664a4d33ffc6be10996606dfc25fd3248c24cc589c0b139feb4c158053565e"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06cc1190f3db3192ab8949e28f2c627e1809487e2cfc435b6524c1ce6a2f391"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c57d50d4d5eb0c862569ca3c840eba2a73412f31d9ecc46ef0d6b2e621a592b"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19a4da6f513045f5ba00e491215bd00122e5bd131847586522463e5a6b2bd65f"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a926339356fe29595f8e37af71db37cd87ff764e15da8ad5129bbaff35bcc5a6"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:091efcfdd4178a7e19a23776dc2b1fafb4f57f4d94daf340f98335817056f874"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:880dbeb6bdde7d926b4d8e41410b16ffcd4cb3b4c6d926280fea46e2615c7a01"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:73b985c9fc09a7896846e26d7b6f4d1fd5a20437055f4ef985d44729f9f928d0"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c0b7cb9598795b01f9a3dd3f770ab540889259def28a3bf9b2fa24d52edecba3"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:37e5a26e76c46f54b3baf56a6fdd56df9db89758694516413757b7d127d4c57b"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:99945ddb4f379bb9831c05e9f80f02f079ba361a0fb1fba1fc3b267639b6bb2e"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dcbcc9e72a791f622a32d17ff5011326a18996647509cac0609a7fc43adc229"}, - {file = "regex-2022.9.13-cp38-cp38-win32.whl", hash = "sha256:d3102ab9bf16bf541ca228012d45d88d2a567c9682a805ae2c145a79d3141fdd"}, - {file = "regex-2022.9.13-cp38-cp38-win_amd64.whl", hash = "sha256:14216ea15efc13f28d0ef1c463d86d93ca7158a79cd4aec0f9273f6d4c6bb047"}, - {file = "regex-2022.9.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a165a05979e212b2c2d56a9f40b69c811c98a788964e669eb322de0a3e420b4"}, - {file = "regex-2022.9.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:14c71437ffb89479c89cc7022a5ea2075a842b728f37205e47c824cc17b30a42"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee7045623a5ace70f3765e452528b4c1f2ce669ed31959c63f54de64fe2f6ff7"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e521d9db006c5e4a0f8acfef738399f72b704913d4e083516774eb51645ad7c"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86548b8234b2be3985dbc0b385e35f5038f0f3e6251464b827b83ebf4ed90e5"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b39ee3b280e15824298b97cec3f7cbbe6539d8282cc8a6047a455b9a72c598"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6e6e61e9a38b6cc60ca3e19caabc90261f070f23352e66307b3d21a24a34aaf"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d837ccf3bd2474feabee96cd71144e991472e400ed26582edc8ca88ce259899c"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6adfe300848d61a470ec7547adc97b0ccf86de86a99e6830f1d8c8d19ecaf6b3"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d5b003d248e6f292475cd24b04e5f72c48412231961a675edcb653c70730e79e"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d5edd3eb877c9fc2e385173d4a4e1d792bf692d79e25c1ca391802d36ecfaa01"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:50e764ffbd08b06aa8c4e86b8b568b6722c75d301b33b259099f237c46b2134e"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d43bd402b27e0e7eae85c612725ba1ce7798f20f6fab4e8bc3de4f263294f03"}, - {file = "regex-2022.9.13-cp39-cp39-win32.whl", hash = "sha256:7fcf7f94ccad19186820ac67e2ec7e09e0ac2dac39689f11cf71eac580503296"}, - {file = "regex-2022.9.13-cp39-cp39-win_amd64.whl", hash = "sha256:322bd5572bed36a5b39952d88e072738926759422498a96df138d93384934ff8"}, - {file = "regex-2022.9.13.tar.gz", hash = "sha256:f07373b6e56a6f3a0df3d75b651a278ca7bd357a796078a26a958ea1ce0588fd"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, + {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, + {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, + {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, + {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, + {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, + {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, + {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, + {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, + {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, + {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, + {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, + {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, + {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, + {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, + {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, ] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, @@ -2230,8 +2192,8 @@ requests-file = [ {file = "requests_file-1.5.1-py2.py3-none-any.whl", hash = "sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"}, ] sentry-sdk = [ - {file = "sentry-sdk-1.9.10.tar.gz", hash = "sha256:4fbace9a763285b608c06f01a807b51acb35f6059da6a01236654e08b0ee81ff"}, - {file = "sentry_sdk-1.9.10-py2.py3-none-any.whl", hash = "sha256:2469240f6190aaebcb453033519eae69cfe8cc602065b4667e18ee14fc1e35dc"}, + {file = "sentry-sdk-1.11.1.tar.gz", hash = "sha256:675f6279b6bb1fea09fd61751061f9a90dca3b5929ef631dd50dc8b3aeb245e9"}, + {file = "sentry_sdk-1.11.1-py2.py3-none-any.whl", hash = "sha256:8b4ff696c0bdcceb3f70bbb87a57ba84fd3168b1332d493fcd16c137f709578c"}, ] setuptools = [ {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, @@ -2288,71 +2250,9 @@ virtualenv = [ {file = "virtualenv-20.16.7-py3-none-any.whl", hash = "sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29"}, {file = "virtualenv-20.16.7.tar.gz", hash = "sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e"}, ] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] yarl = [ {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, diff --git a/pyproject.toml b/pyproject.toml index f6a31cdf8c..ac6d3982de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,16 +10,16 @@ python = "3.10.*" # See https://bot-core.pythondiscord.com/ for docs. pydis_core = { version = "9.1.1", extras = ["async-rediscache"] } -redis = "4.3.4" -fakeredis = { version = "1.9.3", extras = ["lua"] } +redis = "4.3.5" +fakeredis = { version = "2.0.0", extras = ["lua"] } aiohttp = "3.8.3" arrow = "1.2.3" beautifulsoup4 = "4.11.1" -colorama = { version = "0.4.5", markers = "sys_platform == 'win32'" } +colorama = { version = "0.4.6", markers = "sys_platform == 'win32'" } coloredlogs = "15.0.1" -deepdiff = "5.8.1" -emoji = "2.1.0" +deepdiff = "6.2.1" +emoji = "2.2.0" feedparser = "6.0.10" lxml = "4.9.1" @@ -27,34 +27,34 @@ lxml = "4.9.1" # See https://github.com/python-discord/bot/pull/2156 markdownify = "0.6.1" -more-itertools = "8.14.0" +more-itertools = "9.0.0" python-dateutil = "2.8.2" python-frontmatter = "1.0.0" pyyaml = "6.0" -rapidfuzz = "2.11.1" -regex = "2022.9.13" -sentry-sdk = "1.9.10" +rapidfuzz = "2.13.2" +regex = "2022.10.31" +sentry-sdk = "1.11.1" tldextract = "3.4.0" pydantic = "1.10.2" [tool.poetry.dev-dependencies] coverage = "6.5.0" -flake8 = "5.0.4" +flake8 = "6.0.0" flake8-annotations = "2.9.1" -flake8-bugbear = "22.9.23" +flake8-bugbear = "22.10.27" flake8-docstrings = "1.6.0" flake8-string-format = "0.3.0" flake8-tidy-imports = "4.8.0" flake8-todo = "0.7" -flake8-isort = "5.0.0" +flake8-isort = "5.0.3" pep8-naming = "0.13.2" pre-commit = "2.20.0" -pip-licenses = "3.5.4" -pytest = "7.1.3" +pip-licenses = "4.0.1" +pytest = "7.2.0" pytest-cov = "4.0.0" python-dotenv = "0.21.0" -pytest-xdist = "2.5.0" -pytest-subtests = "0.8.0" +pytest-xdist = "3.0.2" +pytest-subtests = "0.9.0" taskipy = "1.10.3" From 78b6062f5c441e8adbdba4a309320171cf72ae77 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 11:13:38 +0000 Subject: [PATCH 26/50] Update botcore module name --- bot/__init__.py | 2 +- bot/__main__.py | 4 ++-- bot/bot.py | 6 +++--- bot/converters.py | 6 +++--- bot/decorators.py | 2 +- bot/exts/backend/error_handler.py | 2 +- bot/exts/backend/logging.py | 2 +- bot/exts/backend/sync/_cog.py | 2 +- bot/exts/backend/sync/_syncers.py | 2 +- bot/exts/filters/antispam.py | 2 +- bot/exts/filters/filter_lists.py | 2 +- bot/exts/filters/filtering.py | 6 +++--- bot/exts/fun/off_topic_names.py | 2 +- bot/exts/help_channels/_channel.py | 2 +- bot/exts/info/codeblock/_cog.py | 2 +- bot/exts/info/doc/_batch_parser.py | 2 +- bot/exts/info/doc/_cog.py | 4 ++-- bot/exts/info/information.py | 2 +- bot/exts/info/subscribe.py | 2 +- bot/exts/moderation/defcon.py | 4 ++-- bot/exts/moderation/incidents.py | 2 +- bot/exts/moderation/infraction/_scheduler.py | 4 ++-- bot/exts/moderation/infraction/_utils.py | 2 +- bot/exts/moderation/metabase.py | 2 +- bot/exts/moderation/modlog.py | 2 +- bot/exts/moderation/modpings.py | 2 +- bot/exts/moderation/silence.py | 2 +- bot/exts/moderation/stream.py | 2 +- bot/exts/moderation/voice_gate.py | 2 +- bot/exts/moderation/watchchannels/_watchchannel.py | 4 ++-- bot/exts/recruitment/talentpool/_api.py | 2 +- bot/exts/recruitment/talentpool/_cog.py | 2 +- bot/exts/recruitment/talentpool/_review.py | 2 +- bot/exts/utils/reminders.py | 8 ++++---- bot/exts/utils/snekbox.py | 4 ++-- bot/exts/utils/thread_bumper.py | 2 +- bot/utils/messages.py | 2 +- tests/bot/exts/backend/sync/test_base.py | 2 +- tests/bot/exts/backend/sync/test_cog.py | 2 +- tests/bot/exts/backend/test_error_handler.py | 2 +- tests/bot/exts/filters/test_filtering.py | 2 +- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- tests/helpers.py | 4 ++-- 43 files changed, 59 insertions(+), 59 deletions(-) diff --git a/bot/__init__.py b/bot/__init__.py index c652897bee..290ca682b3 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -2,7 +2,7 @@ import os from typing import TYPE_CHECKING -from botcore.utils import apply_monkey_patches +from pydis_core.utils import apply_monkey_patches from bot import log diff --git a/bot/__main__.py b/bot/__main__.py index 02af2e9ef6..c8843e1a39 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -3,9 +3,9 @@ import aiohttp import discord from async_rediscache import RedisSession -from botcore import StartupError -from botcore.site_api import APIClient from discord.ext import commands +from pydis_core import StartupError +from pydis_core.site_api import APIClient from redis import RedisError import bot diff --git a/bot/bot.py b/bot/bot.py index aff07cd32b..6164ba9fd2 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -2,8 +2,8 @@ from collections import defaultdict import aiohttp -from botcore import BotBase -from botcore.utils import scheduling +from pydis_core import BotBase +from pydis_core.utils import scheduling from sentry_sdk import push_scope from bot import constants, exts @@ -21,7 +21,7 @@ def __init__(self, base: Exception): class Bot(BotBase): - """A subclass of `botcore.BotBase` that implements bot-specific functions.""" + """A subclass of `pydis_core.BotBase` that implements bot-specific functions.""" def __init__(self, *args, **kwargs): diff --git a/bot/converters.py b/bot/converters.py index e97a25bddb..544513c905 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -8,12 +8,12 @@ import dateutil.parser import discord from aiohttp import ClientConnectorError -from botcore.site_api import ResponseCodeError -from botcore.utils import unqualify -from botcore.utils.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta from discord.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, MemberConverter, UserConverter from discord.utils import escape_markdown, snowflake_time +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils import unqualify +from pydis_core.utils.regex import DISCORD_INVITE from bot import exts, instance as bot_instance from bot.constants import URLs diff --git a/bot/decorators.py b/bot/decorators.py index 466770c3ac..2ddc7ee96f 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -5,10 +5,10 @@ from contextlib import suppress import arrow -from botcore.utils import scheduling from discord import Member, NotFound from discord.ext import commands from discord.ext.commands import Cog, Context +from pydis_core.utils import scheduling from bot.constants import Channels, DEBUG_MODE, RedirectOutput from bot.log import get_logger diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index f9ded79f0a..cc2b5ef567 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -1,9 +1,9 @@ import copy import difflib -from botcore.site_api import ResponseCodeError from discord import Embed from discord.ext.commands import ChannelNotFound, Cog, Context, TextChannelConverter, VoiceChannelConverter, errors +from pydis_core.site_api import ResponseCodeError from sentry_sdk import push_scope from bot.bot import Bot diff --git a/bot/exts/backend/logging.py b/bot/exts/backend/logging.py index b9504c2eba..eba9f3c74c 100644 --- a/bot/exts/backend/logging.py +++ b/bot/exts/backend/logging.py @@ -1,6 +1,6 @@ -from botcore.utils import scheduling from discord import Embed from discord.ext.commands import Cog +from pydis_core.utils import scheduling from bot.bot import Bot from bot.constants import Channels, DEBUG_MODE diff --git a/bot/exts/backend/sync/_cog.py b/bot/exts/backend/sync/_cog.py index 433ff50249..8c7dbb54e1 100644 --- a/bot/exts/backend/sync/_cog.py +++ b/bot/exts/backend/sync/_cog.py @@ -1,10 +1,10 @@ import asyncio from typing import Any, Dict -from botcore.site_api import ResponseCodeError from discord import Member, Role, User from discord.ext import commands from discord.ext.commands import Cog, Context +from pydis_core.site_api import ResponseCodeError from bot import constants from bot.bot import Bot diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py index 8976245e30..f68674f8dd 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -3,10 +3,10 @@ from collections import namedtuple import discord.errors -from botcore.site_api import ResponseCodeError from discord import Guild from discord.ext.commands import Context from more_itertools import chunked +from pydis_core.site_api import ResponseCodeError import bot from bot.log import get_logger diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index b4e7a33f03..d7783292d5 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -8,9 +8,9 @@ from typing import Dict, Iterable, List, Set import arrow -from botcore.utils import scheduling from discord import Colour, Member, Message, MessageType, NotFound, Object, TextChannel from discord.ext.commands import Cog +from pydis_core.utils import scheduling from bot import rules from bot.bot import Bot diff --git a/bot/exts/filters/filter_lists.py b/bot/exts/filters/filter_lists.py index c429b0eb93..5387442048 100644 --- a/bot/exts/filters/filter_lists.py +++ b/bot/exts/filters/filter_lists.py @@ -5,9 +5,9 @@ import arrow import discord -from botcore.site_api import ResponseCodeError from discord.ext import tasks from discord.ext.commands import BadArgument, Cog, Context, IDConverter, command, group, has_any_role +from pydis_core.site_api import ResponseCodeError from bot import constants from bot.bot import Bot diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 23ce00c7d0..23a6f2d927 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -10,13 +10,13 @@ import regex import tldextract from async_rediscache import RedisCache -from botcore.site_api import ResponseCodeError -from botcore.utils import scheduling -from botcore.utils.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta from discord import ChannelType, Colour, Embed, Forbidden, HTTPException, Member, Message, NotFound, TextChannel from discord.ext.commands import Cog from discord.utils import escape_markdown +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils import scheduling +from pydis_core.utils.regex import DISCORD_INVITE from bot.bot import Bot from bot.constants import Bot as BotConfig, Channels, Colours, Filter, Guild, Icons, URLs diff --git a/bot/exts/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py index 5c5fa1dd59..86be8edaef 100644 --- a/bot/exts/fun/off_topic_names.py +++ b/bot/exts/fun/off_topic_names.py @@ -6,11 +6,11 @@ from functools import partial from typing import Optional -from botcore.site_api import ResponseCodeError from discord import ButtonStyle, Colour, Embed, Interaction from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role from discord.ui import Button, View +from pydis_core.site_api import ResponseCodeError from bot.bot import Bot from bot.constants import Bot as BotConfig, Channels, MODERATION_ROLES, NEGATIVE_REPLIES diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 5fc39b6236..74d65107bf 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -3,7 +3,7 @@ import textwrap import discord -from botcore.utils import members +from pydis_core.utils import members import bot from bot import constants diff --git a/bot/exts/info/codeblock/_cog.py b/bot/exts/info/codeblock/_cog.py index 0605a26e7f..a431175fda 100644 --- a/bot/exts/info/codeblock/_cog.py +++ b/bot/exts/info/codeblock/_cog.py @@ -2,9 +2,9 @@ from typing import Optional import discord -from botcore.utils import scheduling from discord import Message, RawMessageUpdateEvent from discord.ext.commands import Cog +from pydis_core.utils import scheduling from bot import constants from bot.bot import Bot diff --git a/bot/exts/info/doc/_batch_parser.py b/bot/exts/info/doc/_batch_parser.py index 41a15fb6e8..53d931830b 100644 --- a/bot/exts/info/doc/_batch_parser.py +++ b/bot/exts/info/doc/_batch_parser.py @@ -8,8 +8,8 @@ from typing import Deque, Dict, List, NamedTuple, Optional, Union import discord -from botcore.utils import scheduling from bs4 import BeautifulSoup +from pydis_core.utils import scheduling import bot from bot.constants import Channels diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py index c35349c3c6..2d0f284061 100644 --- a/bot/exts/info/doc/_cog.py +++ b/bot/exts/info/doc/_cog.py @@ -10,9 +10,9 @@ import aiohttp import discord -from botcore.site_api import ResponseCodeError -from botcore.utils.scheduling import Scheduler from discord.ext import commands +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot from bot.constants import MODERATION_ROLES, RedirectOutput diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 733597dd8d..1a6cfcb590 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -6,10 +6,10 @@ from typing import Any, DefaultDict, Mapping, Optional, Set, Tuple, Union import rapidfuzz -from botcore.site_api import ResponseCodeError from discord import AllowedMentions, Colour, Embed, Guild, Message, Role from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role from discord.utils import escape_markdown +from pydis_core.site_api import ResponseCodeError from bot import constants from bot.bot import Bot diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index e36ce807c6..36304539fb 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -5,9 +5,9 @@ import arrow import discord -from botcore.utils import members from discord.ext import commands from discord.interactions import Interaction +from pydis_core.utils import members from bot import constants from bot.bot import Bot diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 7c924ff14e..ee870ea57a 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -7,12 +7,12 @@ import arrow from async_rediscache import RedisCache -from botcore.utils import scheduling -from botcore.utils.scheduling import Scheduler from dateutil.relativedelta import relativedelta from discord import Colour, Embed, Forbidden, Member, TextChannel, User from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role +from pydis_core.utils import scheduling +from pydis_core.utils.scheduling import Scheduler from redis import RedisError from bot.bot import Bot diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index 1ddbe9857a..ce83ca3fed 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -6,8 +6,8 @@ import discord from async_rediscache import RedisCache -from botcore.utils import scheduling from discord.ext.commands import Cog, Context, MessageConverter, MessageNotFound +from pydis_core.utils import scheduling from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Guild, Roles, Webhooks diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 4c275a1f0f..9b8e67ec5a 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -7,9 +7,9 @@ import arrow import dateutil.parser import discord -from botcore.site_api import ResponseCodeError -from botcore.utils import scheduling from discord.ext.commands import Context +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils import scheduling from bot import constants from bot.bot import Bot diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index c03081b07c..2cf7f8efb0 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -2,8 +2,8 @@ import arrow import discord -from botcore.site_api import ResponseCodeError from discord.ext.commands import Context +from pydis_core.site_api import ResponseCodeError import bot from bot.constants import Colours, Icons diff --git a/bot/exts/moderation/metabase.py b/bot/exts/moderation/metabase.py index c630198822..aeb589b5b5 100644 --- a/bot/exts/moderation/metabase.py +++ b/bot/exts/moderation/metabase.py @@ -8,8 +8,8 @@ from aiohttp.client_exceptions import ClientResponseError from arrow import Arrow from async_rediscache import RedisCache -from botcore.utils.scheduling import Scheduler from discord.ext.commands import Cog, Context, group, has_any_role +from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot from bot.constants import Metabase as MetabaseConfig, Roles diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index a1ed714be5..d916d1f4db 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -6,13 +6,13 @@ from itertools import zip_longest import discord -from botcore.site_api import ResponseCodeError from dateutil.relativedelta import relativedelta from deepdiff import DeepDiff from discord import Colour, Message, Thread from discord.abc import GuildChannel from discord.ext.commands import Cog, Context from discord.utils import escape_markdown, format_dt, snowflake_time +from pydis_core.site_api import ResponseCodeError from sentry_sdk import add_breadcrumb from bot.bot import Bot diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 7c8e4ac138..16423b3d09 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -3,10 +3,10 @@ import arrow from async_rediscache import RedisCache -from botcore.utils.scheduling import Scheduler from dateutil.parser import isoparse, parse as dateutil_parse from discord import Member from discord.ext.commands import Cog, Context, group, has_any_role +from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 578551d241..682791593e 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -5,11 +5,11 @@ from typing import Optional, OrderedDict, Union from async_rediscache import RedisCache -from botcore.utils.scheduling import Scheduler from discord import Guild, PermissionOverwrite, TextChannel, Thread, VoiceChannel from discord.ext import commands, tasks from discord.ext.commands import Context from discord.utils import MISSING +from pydis_core.utils.scheduling import Scheduler from bot import constants from bot.bot import Bot diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index a96e965116..f0d8c23b8d 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -5,8 +5,8 @@ import discord from arrow import Arrow from async_rediscache import RedisCache -from botcore.utils import scheduling from discord.ext import commands +from pydis_core.utils import scheduling from bot.bot import Bot from bot.constants import ( diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py index 90f88d040a..1901d1c573 100644 --- a/bot/exts/moderation/voice_gate.py +++ b/bot/exts/moderation/voice_gate.py @@ -5,9 +5,9 @@ import arrow import discord from async_rediscache import RedisCache -from botcore.site_api import ResponseCodeError from discord import Colour, Member, VoiceState from discord.ext.commands import Cog, Context, command +from pydis_core.site_api import ResponseCodeError from bot.bot import Bot from bot.constants import Bot as BotConfig, Channels, MODERATION_ROLES, Roles, VoiceGate as GateConf diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index 6eaedf6b3f..2871eb5def 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -7,10 +7,10 @@ from typing import Any, Dict, Optional import discord -from botcore.site_api import ResponseCodeError -from botcore.utils import scheduling from discord import Color, DMChannel, Embed, HTTPException, Message, errors from discord.ext.commands import Cog, Context +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils import scheduling from bot.bot import Bot from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig, Icons diff --git a/bot/exts/recruitment/talentpool/_api.py b/bot/exts/recruitment/talentpool/_api.py index 2cb15a14dc..fee23826d7 100644 --- a/bot/exts/recruitment/talentpool/_api.py +++ b/bot/exts/recruitment/talentpool/_api.py @@ -1,7 +1,7 @@ from datetime import datetime -from botcore.site_api import APIClient from pydantic import BaseModel, Field, parse_obj_as +from pydis_core.site_api import APIClient class NominationEntry(BaseModel): diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 94737fc6c1..dbc3ea538f 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -5,10 +5,10 @@ import discord from async_rediscache import RedisCache -from botcore.site_api import ResponseCodeError from discord import Color, Embed, Member, PartialMessage, RawReactionActionEvent, User from discord.ext import commands, tasks from discord.ext.commands import BadArgument, Cog, Context, group, has_any_role +from pydis_core.site_api import ResponseCodeError from bot.bot import Bot from bot.constants import Bot as BotConfig, Channels, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index a74e1ce2b0..876f95369e 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -8,8 +8,8 @@ from datetime import datetime, timedelta, timezone from typing import List, Optional, Union -from botcore.site_api import ResponseCodeError from discord import Embed, Emoji, Member, Message, NotFound, PartialMessage, TextChannel +from pydis_core.site_api import ResponseCodeError from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Guild, Roles diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 803e2ea522..1991a687f8 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -5,11 +5,11 @@ from operator import itemgetter import discord -from botcore.site_api import ResponseCodeError -from botcore.utils import scheduling -from botcore.utils.scheduling import Scheduler from dateutil.parser import isoparse from discord.ext.commands import Cog, Context, Greedy, group +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils import scheduling +from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot from bot.constants import ( @@ -218,7 +218,7 @@ async def try_get_content_from_reply(ctx: Context) -> t.Optional[str]: """ Attempts to get content from the referenced message, if applicable. - Differs from botcore.utils.commands.clean_text_or_reply as allows for messages with no content. + Differs from pydis_core.utils.commands.clean_text_or_reply as allows for messages with no content. """ content = None if reference := ctx.message.reference: diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 1909569595..53012a5e10 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -7,10 +7,10 @@ from textwrap import dedent from typing import Literal, Optional, Tuple -from botcore.utils import interactions -from botcore.utils.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX from discord import AllowedMentions, HTTPException, Interaction, Message, NotFound, Reaction, User, enums, ui from discord.ext.commands import Cog, Command, Context, Converter, command, guild_only +from pydis_core.utils import interactions +from pydis_core.utils.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX from bot.bot import Bot from bot.constants import Channels, MODERATION_ROLES, Roles, URLs diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index a2f208484e..0384119f5d 100644 --- a/bot/exts/utils/thread_bumper.py +++ b/bot/exts/utils/thread_bumper.py @@ -1,8 +1,8 @@ import typing as t import discord -from botcore.site_api import ResponseCodeError from discord.ext import commands +from pydis_core.site_api import ResponseCodeError from bot import constants from bot.bot import Bot diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 8a968f6598..27f2eac974 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -6,8 +6,8 @@ from typing import Callable, List, Optional, Sequence, Union import discord -from botcore.utils import scheduling from discord.ext.commands import Context +from pydis_core.utils import scheduling import bot from bot.constants import Emojis, MODERATION_ROLES, NEGATIVE_REPLIES diff --git a/tests/bot/exts/backend/sync/test_base.py b/tests/bot/exts/backend/sync/test_base.py index a17c1fa104..4dacfda17a 100644 --- a/tests/bot/exts/backend/sync/test_base.py +++ b/tests/bot/exts/backend/sync/test_base.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from botcore.site_api import ResponseCodeError +from pydis_core.site_api import ResponseCodeError from bot.exts.backend.sync._syncers import Syncer from tests import helpers diff --git a/tests/bot/exts/backend/sync/test_cog.py b/tests/bot/exts/backend/sync/test_cog.py index 87b76c6b41..2ce9509650 100644 --- a/tests/bot/exts/backend/sync/test_cog.py +++ b/tests/bot/exts/backend/sync/test_cog.py @@ -2,7 +2,7 @@ from unittest import mock import discord -from botcore.site_api import ResponseCodeError +from pydis_core.site_api import ResponseCodeError from bot import constants from bot.exts.backend import sync diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 562c827b9e..adb0252a5e 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -1,8 +1,8 @@ import unittest from unittest.mock import AsyncMock, MagicMock, call, patch -from botcore.site_api import ResponseCodeError from discord.ext.commands import errors +from pydis_core.site_api import ResponseCodeError from bot.errors import InvalidInfractedUserError, LockedResourceError from bot.exts.backend import error_handler diff --git a/tests/bot/exts/filters/test_filtering.py b/tests/bot/exts/filters/test_filtering.py index bd26532f19..e47cf627be 100644 --- a/tests/bot/exts/filters/test_filtering.py +++ b/tests/bot/exts/filters/test_filtering.py @@ -11,7 +11,7 @@ class FilteringCogTests(unittest.IsolatedAsyncioTestCase): def setUp(self): """Instantiate the bot and cog.""" self.bot = MockBot() - with patch("botcore.utils.scheduling.create_task", new=lambda task, **_: task.close()): + with patch("pydis_core.utils.scheduling.create_task", new=lambda task, **_: task.close()): self.cog = filtering.Filtering(self.bot) @autospec(filtering.Filtering, "_get_filterlist_items", pass_mocks=False, return_value=["TOKEN"]) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 29dadf3722..122935e37e 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -3,8 +3,8 @@ from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch -from botcore.site_api import ResponseCodeError from discord import Embed, Forbidden, HTTPException, NotFound +from pydis_core.site_api import ResponseCodeError from bot.constants import Colours, Icons from bot.exts.moderation.infraction import _utils as utils diff --git a/tests/helpers.py b/tests/helpers.py index a4b919dcb1..4b980ac217 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -9,9 +9,9 @@ import discord from aiohttp import ClientSession -from botcore.async_stats import AsyncStatsClient -from botcore.site_api import APIClient from discord.ext.commands import Context +from pydis_core.async_stats import AsyncStatsClient +from pydis_core.site_api import APIClient from bot.bot import Bot from tests._autospec import autospec # noqa: F401 other modules import it via this module From f47c864d57b42b28c3265f032162e4d5816bc51a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 13:15:34 +0000 Subject: [PATCH 27/50] Ignore Discord errors when clearing snekbox reactions These errors can be caused by the message being deleted, or the message being in an archived thread --- bot/exts/utils/snekbox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 53012a5e10..8a2e68b282 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -402,15 +402,16 @@ async def continue_job( return None, None code = await self.get_code(new_message, ctx.command) - await ctx.message.clear_reaction(REDO_EMOJI) with contextlib.suppress(HTTPException): + await ctx.message.clear_reaction(REDO_EMOJI) await response.delete() if code is None: return None, None except asyncio.TimeoutError: - await ctx.message.clear_reaction(REDO_EMOJI) + with contextlib.suppress(HTTPException): + await ctx.message.clear_reaction(REDO_EMOJI) return None, None codeblocks = await CodeblockConverter.convert(ctx, code) From 5da9d6a3d62e661940869d92be72b46ad3209534 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 12:08:19 +0000 Subject: [PATCH 28/50] Get, and store, the help forum channel object on cog load --- bot/exts/help_channels/_cog.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index bb2f43c5a9..051532c74b 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -28,7 +28,12 @@ class HelpForum(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - self.help_forum_channel_id = constants.Channels.help_system_forum + self.help_forum_channel: discord.ForumChannel = None + + async def cog_load(self) -> None: + """Schedule the auto-archive check for all open posts.""" + log.trace("Initialising help forum cog.") + self.help_forum_channel = self.bot.get_channel(constants.Channels.help_system_forum) async def close_check(self, ctx: commands.Context) -> bool: """Return True if the channel is a help post, and the user is the claimant or has a whitelisted role.""" @@ -116,7 +121,7 @@ async def rename_help_post(self, ctx: commands.Context, *, title: str) -> None: @commands.Cog.listener() async def on_thread_create(self, thread: discord.Thread) -> None: """Defer application of new post logic for posts the help forum to the _channel helper.""" - if thread.parent_id != self.help_forum_channel_id: + if thread.parent_id != self.help_forum_channel.id: return await self.post_with_disallowed_title_check(thread) @@ -125,7 +130,7 @@ async def on_thread_create(self, thread: discord.Thread) -> None: @commands.Cog.listener() async def on_thread_update(self, before: discord.Thread, after: discord.Thread) -> None: """Defer application archive logic for posts in the help forum to the _channel helper.""" - if after.parent_id != self.help_forum_channel_id: + if after.parent_id != self.help_forum_channel.id: return if not before.archived and after.archived: await _channel.help_thread_archived(after) @@ -135,7 +140,7 @@ async def on_thread_update(self, before: discord.Thread, after: discord.Thread) @commands.Cog.listener() async def on_raw_thread_delete(self, deleted_thread_event: discord.RawThreadDeleteEvent) -> None: """Defer application of new post logic for posts the help forum to the _channel helper.""" - if deleted_thread_event.parent_id == self.help_forum_channel_id: + if deleted_thread_event.parent_id == self.help_forum_channel.id: await _channel.help_thread_deleted(deleted_thread_event) @commands.Cog.listener() From dc99ba4125cd2cd95208b9972a30b3dcc90877b6 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 13:28:04 +0000 Subject: [PATCH 29/50] Consistantly refer to help posts as posts, not threads --- bot/exts/help_channels/_channel.py | 82 +++++++++++++++--------------- bot/exts/help_channels/_cog.py | 12 ++--- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 74d65107bf..3a290bdfde 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -41,22 +41,22 @@ def is_help_forum_post(channel: discord.abc.GuildChannel) -> bool: return getattr(channel, "parent_id", None) == constants.Channels.help_system_forum -async def _close_help_thread(closed_thread: discord.Thread, closed_on: _stats.ClosingReason) -> None: - """Close the help thread and record stats.""" +async def _close_help_post(closed_post: discord.Thread, closing_reason: _stats.ClosingReason) -> None: + """Close the help post and record stats.""" embed = discord.Embed(description=DORMANT_MSG) - await closed_thread.send(embed=embed) - await closed_thread.edit(archived=True, locked=True, reason="Locked a dormant help channel") + await closed_post.send(embed=embed) + await closed_post.edit(archived=True, locked=True, reason="Locked a dormant help post") _stats.report_post_count() - await _stats.report_complete_session(closed_thread, closed_on) + await _stats.report_complete_session(closed_post, closing_reason) - poster = closed_thread.owner - cooldown_role = closed_thread.guild.get_role(constants.Roles.help_cooldown) + poster = closed_post.owner + cooldown_role = closed_post.guild.get_role(constants.Roles.help_cooldown) if poster is None: # We can't include the owner ID/name here since the thread only contains None log.info( - f"Failed to remove cooldown role for owner of thread ({closed_thread.id}). " + f"Failed to remove cooldown role for owner of post ({closed_post.id}). " f"The user is likely no longer on the server." ) return @@ -64,7 +64,7 @@ async def _close_help_thread(closed_thread: discord.Thread, closed_on: _stats.Cl await members.handle_role_change(poster, poster.remove_roles, cooldown_role) -async def send_opened_post_message(thread: discord.Thread) -> None: +async def send_opened_post_message(post: discord.Thread) -> None: """Send the opener message in the new help post.""" embed = discord.Embed( color=constants.Colours.bright_green, @@ -72,24 +72,24 @@ async def send_opened_post_message(thread: discord.Thread) -> None: ) embed.set_author(name=POST_TITLE) embed.set_footer(text=POST_FOOTER) - await thread.send(embed=embed) + await post.send(embed=embed) -async def send_opened_post_dm(thread: discord.Thread) -> None: +async def send_opened_post_dm(post: discord.Thread) -> None: """Send the opener a DM message with a jump link to their new post.""" embed = discord.Embed( - title="Help channel opened", - description=f"You opened {thread.mention}.", + title="Help post opened", + description=f"You opened {post.mention}.", colour=constants.Colours.bright_green, - timestamp=thread.created_at, + timestamp=post.created_at, ) embed.set_thumbnail(url=constants.Icons.green_questionmark) - message = thread.starter_message + message = post.starter_message if not message: try: - message = await thread.fetch_message(thread.id) + message = await post.fetch_message(post.id) except discord.HTTPException: - log.warning(f"Could not fetch message for thread {thread.id}") + log.warning(f"Could not fetch message for post {post.id}") return formatted_message = textwrap.shorten(message.content, width=100, placeholder="...").strip() @@ -105,21 +105,21 @@ async def send_opened_post_dm(thread: discord.Thread) -> None: ) try: - await thread.owner.send(embed=embed) - log.trace(f"Sent DM to {thread.owner} ({thread.owner_id}) after posting in help forum.") + await post.owner.send(embed=embed) + log.trace(f"Sent DM to {post.owner} ({post.owner_id}) after posting in help forum.") except discord.errors.Forbidden: log.trace( - f"Ignoring to send DM to {thread.owner} ({thread.owner_id}) after posting in help forum: DMs disabled.", + f"Ignoring to send DM to {post.owner} ({post.owner_id}) after posting in help forum: DMs disabled.", ) -async def help_thread_opened(opened_thread: discord.Thread, *, reopen: bool = False) -> None: +async def help_post_opened(opened_post: discord.Thread, *, reopen: bool = False) -> None: """Apply new post logic to a new help forum post.""" _stats.report_post_count() - if not isinstance(opened_thread.owner, discord.Member): - log.debug(f"{opened_thread.owner_id} isn't a member. Closing post.") - await _close_help_thread(opened_thread, _stats.ClosingReason.CLEANUP) + if not isinstance(opened_post.owner, discord.Member): + log.debug(f"{opened_post.owner_id} isn't a member. Closing post.") + await _close_help_post(opened_post, _stats.ClosingReason.CLEANUP) return # Discord sends the open event long before the thread is ready for actions in the API. @@ -127,30 +127,30 @@ async def help_thread_opened(opened_thread: discord.Thread, *, reopen: bool = Fa # We sleep here to try and delay our code enough so the thread is ready in the API. await asyncio.sleep(2) - await send_opened_post_dm(opened_thread) + await send_opened_post_dm(opened_post) try: - await opened_thread.starter_message.pin() + await opened_post.starter_message.pin() except discord.HTTPException as e: # Suppress if the message was not found, most likely deleted if e.code != 10008: raise e - await send_opened_post_message(opened_thread) + await send_opened_post_message(opened_post) - cooldown_role = opened_thread.guild.get_role(constants.Roles.help_cooldown) - await members.handle_role_change(opened_thread.owner, opened_thread.owner.add_roles, cooldown_role) + cooldown_role = opened_post.guild.get_role(constants.Roles.help_cooldown) + await members.handle_role_change(opened_post.owner, opened_post.owner.add_roles, cooldown_role) -async def help_thread_closed(closed_thread: discord.Thread) -> None: +async def help_post_closed(closed_post: discord.Thread) -> None: """Apply archive logic to a manually closed help forum post.""" - await _close_help_thread(closed_thread, _stats.ClosingReason.COMMAND) + await _close_help_post(closed_post, _stats.ClosingReason.COMMAND) -async def help_thread_archived(archived_thread: discord.Thread) -> None: +async def help_post_archived(archived_post: discord.Thread) -> None: """Apply archive logic to an archived help forum post.""" - async for thread_update in archived_thread.guild.audit_logs(limit=50, action=discord.AuditLogAction.thread_update): - if thread_update.target.id != archived_thread.id: + async for thread_update in archived_post.guild.audit_logs(limit=50, action=discord.AuditLogAction.thread_update): + if thread_update.target.id != archived_post.id: continue # Don't apply close logic if the post was archived by the bot, as it @@ -158,13 +158,13 @@ async def help_thread_archived(archived_thread: discord.Thread) -> None: if thread_update.user.id == bot.instance.user.id: return - await _close_help_thread(archived_thread, _stats.ClosingReason.INACTIVE) + await _close_help_post(archived_post, _stats.ClosingReason.INACTIVE) -async def help_thread_deleted(deleted_thread_event: discord.RawThreadDeleteEvent) -> None: - """Record appropriate stats when a help thread is deleted.""" +async def help_post_deleted(deleted_post_event: discord.RawThreadDeleteEvent) -> None: + """Record appropriate stats when a help post is deleted.""" _stats.report_post_count() - cached_thread = deleted_thread_event.thread - if cached_thread and not cached_thread.archived: - # If the thread is in the bot's cache, and it was not archived before deleting, report a complete session. - await _stats.report_complete_session(cached_thread, _stats.ClosingReason.DELETED) + cached_post = deleted_post_event.thread + if cached_post and not cached_post.archived: + # If the post is in the bot's cache, and it was not archived before deleting, report a complete session. + await _stats.report_complete_session(cached_post, _stats.ClosingReason.DELETED) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 051532c74b..742cf38dd7 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -31,7 +31,7 @@ def __init__(self, bot: Bot): self.help_forum_channel: discord.ForumChannel = None async def cog_load(self) -> None: - """Schedule the auto-archive check for all open posts.""" + """Archive all idle open posts, schedule check for later for active open posts.""" log.trace("Initialising help forum cog.") self.help_forum_channel = self.bot.get_channel(constants.Channels.help_system_forum) @@ -58,7 +58,7 @@ async def post_with_disallowed_title_check(self, post: discord.Thread) -> None: mod_alerts = self.bot.get_channel(constants.Channels.mod_alerts) await mod_alerts.send( f"<@&{constants.Roles.moderators}>\n" - f"<@{post.owner_id}> ({post.owner_id}) opened the thread {post.mention} ({post.id}), " + f"<@{post.owner_id}> ({post.owner_id}) opened the post {post.mention} ({post.id}), " "which triggered the token filter with its name!\n" f"**Match:** {match.group()}" ) @@ -79,7 +79,7 @@ async def close_command(self, ctx: commands.Context) -> None: # Don't use a discord.py check because the check needs to fail silently. if await self.close_check(ctx): log.info(f"Close command invoked by {ctx.author} in #{ctx.channel}.") - await _channel.help_thread_closed(ctx.channel) + await _channel.help_post_closed(ctx.channel) @help_forum_group.command(name="dm", root_aliases=("helpdm",)) async def help_dm_command( @@ -125,7 +125,7 @@ async def on_thread_create(self, thread: discord.Thread) -> None: return await self.post_with_disallowed_title_check(thread) - await _channel.help_thread_opened(thread) + await _channel.help_post_opened(thread) @commands.Cog.listener() async def on_thread_update(self, before: discord.Thread, after: discord.Thread) -> None: @@ -133,7 +133,7 @@ async def on_thread_update(self, before: discord.Thread, after: discord.Thread) if after.parent_id != self.help_forum_channel.id: return if not before.archived and after.archived: - await _channel.help_thread_archived(after) + await _channel.help_post_archived(after) if before.name != after.name: await self.post_with_disallowed_title_check(after) @@ -141,7 +141,7 @@ async def on_thread_update(self, before: discord.Thread, after: discord.Thread) async def on_raw_thread_delete(self, deleted_thread_event: discord.RawThreadDeleteEvent) -> None: """Defer application of new post logic for posts the help forum to the _channel helper.""" if deleted_thread_event.parent_id == self.help_forum_channel.id: - await _channel.help_thread_deleted(deleted_thread_event) + await _channel.help_post_deleted(deleted_thread_event) @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: From 7d33a854403bc8269403056458058537b51d2c47 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 13:28:32 +0000 Subject: [PATCH 30/50] Don't load the help channel cog if disabled --- bot/exts/help_channels/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bot/exts/help_channels/__init__.py b/bot/exts/help_channels/__init__.py index 00b4a735bd..643497b9ff 100644 --- a/bot/exts/help_channels/__init__.py +++ b/bot/exts/help_channels/__init__.py @@ -1,8 +1,15 @@ from bot.bot import Bot +from bot.constants import HelpChannels from bot.exts.help_channels._cog import HelpForum +from bot.log import get_logger + +log = get_logger(__name__) async def setup(bot: Bot) -> None: """Load the HelpForum cog.""" + if not HelpChannels.enable: + log.warning("HelpChannel.enabled set to false, not loading help channel cog.") + return await bot.add_cog(HelpForum(bot)) From 707ace7304bba6351100f44893e65e53e071b658 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 13:31:54 +0000 Subject: [PATCH 31/50] Auto archive help forum posts after inactivity I decided to keep the archive logic simple, and just go for 30 minutes since last message, rather than the hybrid of 30 mins + 10 depending on who sent the last message. The reason for using the hybrid approach previously was due to us running out of channels frequently Since this is no longer a problem, I decided to keep the logic simple. --- bot/constants.py | 2 + bot/exts/help_channels/_channel.py | 61 +++++++++++++++++++++++++++++- bot/exts/help_channels/_cog.py | 9 +++++ config-default.yml | 7 ++++ 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/bot/constants.py b/bot/constants.py index 24862059e1..9851aea97d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -615,6 +615,8 @@ class HelpChannels(metaclass=YAMLGetter): section = 'help_channels' enable: bool + idle_minutes: int + deleted_idle_minutes: int cmd_whitelist: List[int] diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 3a290bdfde..48dcc13c65 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,9 +1,11 @@ """Contains all logic to handle changes to posts in the help forum.""" import asyncio import textwrap +from datetime import timedelta +import arrow import discord -from pydis_core.utils import members +from pydis_core.utils import members, scheduling import bot from bot import constants @@ -168,3 +170,60 @@ async def help_post_deleted(deleted_post_event: discord.RawThreadDeleteEvent) -> if cached_post and not cached_post.archived: # If the post is in the bot's cache, and it was not archived before deleting, report a complete session. await _stats.report_complete_session(cached_post, _stats.ClosingReason.DELETED) + + +async def get_closing_time(post: discord.Thread) -> tuple[arrow.Arrow, _stats.ClosingReason]: + """ + Return the time at which the given help `post` should be closed along with the reason. + + The time is calculated by first checking if the opening message is deleted. + If it is, then get the last 100 messages (the most that can be fetched in one API call). + If less than 100 message are returned, and none are from the post owner, then assume the poster + has sent no further messages and close deleted_idle_minutes after the post creation time. + + Otherwise, use the most recent message's create_at date and add `idle_minutes_claimant`. + """ + try: + starter_message = post.starter_message or await post.fetch_message(post.id) + except discord.NotFound: + starter_message = None + + last_100_messages = [message async for message in post.history(limit=100, oldest_first=False)] + + if starter_message is None and len(last_100_messages) < 100: + if not discord.utils.get(last_100_messages, author__id=post.owner_id): + time = arrow.Arrow.fromdatetime(post.created_at) + time += timedelta(minutes=constants.HelpChannels.deleted_idle_minutes) + return time, _stats.ClosingReason.DELETED + + time = arrow.Arrow.fromdatetime(last_100_messages[0].created_at) + time += timedelta(minutes=constants.HelpChannels.idle_minutes) + return time, _stats.ClosingReason.INACTIVE + + +async def maybe_archive_idle_post(post: discord.Thread, scheduler: scheduling.Scheduler, has_task: bool = True) -> None: + """ + Archive the `post` if idle, or schedule the archive for later if still active. + + If `has_task` is True and rescheduling is required, the extant task to make the post + dormant will first be cancelled. + """ + log.trace(f"Handling open post #{post} ({post.id}).") + + closing_time, closing_reason = await get_closing_time(post) + + if closing_time < (arrow.utcnow() + timedelta(seconds=1)): + # Closing time is in the past. + # Add 1 second due to POSIX timestamps being lower resolution than datetime objects. + log.info( + f"#{post} ({post.id}) is idle past {closing_time} and will be archived. Reason: {closing_reason.value}" + ) + await _close_help_post(post, closing_reason) + return + + if has_task: + scheduler.cancel(post.id) + delay = (closing_time - arrow.utcnow()).seconds + log.info(f"#{post} ({post.id}) is still active; scheduling it to be archived after {delay} seconds.") + + scheduler.schedule_later(delay, post.id, maybe_archive_idle_post(post, scheduler, has_task=True)) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 742cf38dd7..0c3510c29d 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -4,6 +4,7 @@ import discord from discord.ext import commands +from pydis_core.utils import scheduling from bot import constants from bot.bot import Bot @@ -28,13 +29,21 @@ class HelpForum(commands.Cog): def __init__(self, bot: Bot): self.bot = bot + self.scheduler = scheduling.Scheduler(self.__class__.__name__) self.help_forum_channel: discord.ForumChannel = None + async def cog_unload(self) -> None: + """Cancel all scheduled tasks on unload.""" + self.scheduler.cancel_all() + async def cog_load(self) -> None: """Archive all idle open posts, schedule check for later for active open posts.""" log.trace("Initialising help forum cog.") self.help_forum_channel = self.bot.get_channel(constants.Channels.help_system_forum) + for post in self.help_forum_channel.channels: + await _channel.maybe_archive_idle_post(post, self.scheduler, has_task=False) + async def close_check(self, ctx: commands.Context) -> bool: """Return True if the channel is a help post, and the user is the claimant or has a whitelisted role.""" if not _channel.is_help_forum_post(ctx.channel): diff --git a/config-default.yml b/config-default.yml index c9d043ff7c..1d7a2ff780 100644 --- a/config-default.yml +++ b/config-default.yml @@ -493,6 +493,13 @@ free: help_channels: enable: true + # Allowed duration of inactivity before archiving a help post + idle_minutes: 30 + + # Allowed duration of inactivity when post is empty (due to deleted messages) + # before archiving a help post + deleted_idle_minutes: 5 + # Roles which are allowed to use the command which makes channels dormant cmd_whitelist: - *HELPERS_ROLE From 8a9a2793e19d13351459b3dcd5b55c0e56e6748c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 13:37:32 +0000 Subject: [PATCH 32/50] Listen for thread starter on_message rather than thread_create thread_create events are triggered before the thread's starter message is available, so listening for starter messages with on_message instead ensures we have a message object to use. --- bot/exts/help_channels/_channel.py | 6 ------ bot/exts/help_channels/_cog.py | 23 +++++++++++++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 48dcc13c65..ffdd1d2bfe 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,5 +1,4 @@ """Contains all logic to handle changes to posts in the help forum.""" -import asyncio import textwrap from datetime import timedelta @@ -124,11 +123,6 @@ async def help_post_opened(opened_post: discord.Thread, *, reopen: bool = False) await _close_help_post(opened_post, _stats.ClosingReason.CLEANUP) return - # Discord sends the open event long before the thread is ready for actions in the API. - # This causes actions such as fetching the message, pinning message, etc to fail. - # We sleep here to try and delay our code enough so the thread is ready in the API. - await asyncio.sleep(2) - await send_opened_post_dm(opened_post) try: diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 0c3510c29d..5da0cd52cf 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -127,15 +127,30 @@ async def rename_help_post(self, ctx: commands.Context, *, title: str) -> None: await ctx.channel.edit(name=title) - @commands.Cog.listener() - async def on_thread_create(self, thread: discord.Thread) -> None: + @commands.Cog.listener("on_message") + async def new_post_listener(self, message: discord.Message) -> None: """Defer application of new post logic for posts the help forum to the _channel helper.""" + if not isinstance(message.channel, discord.Thread): + return + thread = message.channel + + if not message.id == thread.id: + # Opener messages have the same ID as the thread + return + if thread.parent_id != self.help_forum_channel.id: return await self.post_with_disallowed_title_check(thread) await _channel.help_post_opened(thread) + delay = min(constants.HelpChannels.deleted_idle_minutes, constants.HelpChannels.idle_minutes) * 60 + self.scheduler.schedule_later( + delay, + thread.id, + _channel.maybe_archive_idle_post(thread, self.scheduler) + ) + @commands.Cog.listener() async def on_thread_update(self, before: discord.Thread, after: discord.Thread) -> None: """Defer application archive logic for posts in the help forum to the _channel helper.""" @@ -152,8 +167,8 @@ async def on_raw_thread_delete(self, deleted_thread_event: discord.RawThreadDele if deleted_thread_event.parent_id == self.help_forum_channel.id: await _channel.help_post_deleted(deleted_thread_event) - @commands.Cog.listener() - async def on_message(self, message: discord.Message) -> None: + @commands.Cog.listener("on_message") + async def new_post_message_listener(self, message: discord.Message) -> None: """Defer application of new message logic for messages in the help forum to the _message helper.""" if not _channel.is_help_forum_post(message.channel): return None From 00e74ed5f6eefb0f9a841fa06d3fe9d8ff8fdd86 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 14:06:48 +0000 Subject: [PATCH 33/50] Ensure constants.Channels.help_system_forum is a forum channel on load --- bot/exts/help_channels/_cog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 5da0cd52cf..146391dcfe 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -40,8 +40,10 @@ async def cog_load(self) -> None: """Archive all idle open posts, schedule check for later for active open posts.""" log.trace("Initialising help forum cog.") self.help_forum_channel = self.bot.get_channel(constants.Channels.help_system_forum) + if not isinstance(self.help_forum_channel, discord.ForumChannel): + raise TypeError("Channels.help_system_forum is not a forum channel!") - for post in self.help_forum_channel.channels: + for post in self.help_forum_channel.threads: await _channel.maybe_archive_idle_post(post, self.scheduler, has_task=False) async def close_check(self, ctx: commands.Context) -> bool: From 1b2db72ea08e4b64fd77aac38daf2ba9c6d2513a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 14:46:27 +0000 Subject: [PATCH 34/50] Update CODEOWNERS --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0bc2bb7933..7cd00a0d6f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,7 +5,6 @@ bot/exts/info/codeblock/** @MarkKoz bot/exts/utils/extensions.py @MarkKoz bot/exts/utils/snekbox.py @MarkKoz @jb3 -bot/exts/help_channels/** @MarkKoz bot/exts/moderation/** @mbaruh @Den4200 @ks129 @jb3 bot/exts/info/** @Den4200 @jb3 bot/exts/info/information.py @mbaruh @jb3 From e0086aa61d2d9d1c210a2a775dabaffa95c5b552 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 26 Nov 2022 16:37:15 +0000 Subject: [PATCH 35/50] Catch case where starter message is deleted before pinning --- bot/exts/help_channels/_channel.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index ffdd1d2bfe..25a1bf4d24 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -127,9 +127,11 @@ async def help_post_opened(opened_post: discord.Thread, *, reopen: bool = False) try: await opened_post.starter_message.pin() - except discord.HTTPException as e: + except (discord.HTTPException, AttributeError) as e: # Suppress if the message was not found, most likely deleted - if e.code != 10008: + # The message being deleted could be surfaced as an AttributeError on .starter_message, + # or as an exception from the Discord API, depending on timing and cache status. + if isinstance(e, discord.HTTPException) and e.code != 10008: raise e await send_opened_post_message(opened_post) From 0ed06cbe5483b9118fafc34ab487f0051715b04e Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 27 Nov 2022 10:49:18 +0000 Subject: [PATCH 36/50] Don't close already closed posts --- bot/exts/help_channels/_channel.py | 4 ++++ bot/exts/help_channels/_cog.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 25a1bf4d24..0cee248174 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -204,6 +204,10 @@ async def maybe_archive_idle_post(post: discord.Thread, scheduler: scheduling.Sc If `has_task` is True and rescheduling is required, the extant task to make the post dormant will first be cancelled. """ + if post.locked: + log.trace(f"Not closing already closed post #{post} ({post.id}).") + return + log.trace(f"Handling open post #{post} ({post.id}).") closing_time, closing_reason = await get_closing_time(post) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 146391dcfe..31f30b7aaf 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -91,6 +91,8 @@ async def close_command(self, ctx: commands.Context) -> None: if await self.close_check(ctx): log.info(f"Close command invoked by {ctx.author} in #{ctx.channel}.") await _channel.help_post_closed(ctx.channel) + if ctx.channel.id in self.scheduler: + self.scheduler.cancel(ctx.channel.id) @help_forum_group.command(name="dm", root_aliases=("helpdm",)) async def help_dm_command( From e2cd734d7c61f125ae9fca0334c7d2f15f989325 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 27 Nov 2022 14:49:41 +0000 Subject: [PATCH 37/50] Typehint bot.get_cog calls in information cog --- bot/exts/info/information.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 1a6cfcb590..24e12a3130 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -3,7 +3,7 @@ import textwrap from collections import defaultdict from textwrap import shorten -from typing import Any, DefaultDict, Mapping, Optional, Set, Tuple, Union +from typing import Any, DefaultDict, Mapping, Optional, Set, TYPE_CHECKING, Tuple, Union import rapidfuzz from discord import AllowedMentions, Colour, Embed, Guild, Message, Role @@ -31,6 +31,11 @@ " all members of the community to have read and understood these." ) +if TYPE_CHECKING: + from bot.exts.moderation.defcon import Defcon + from bot.exts.moderation.watchchannels.bigbrother import BigBrother + from bot.exts.recruitment.talentpool._cog import TalentPool + class Information(Cog): """A cog with commands for generating embeds with server info, such as server stats and user info.""" @@ -79,17 +84,20 @@ def get_member_counts(guild: Guild) -> dict[str, int]: def get_extended_server_info(self, ctx: Context) -> str: """Return additional server info only visible in moderation channels.""" talentpool_info = "" - if cog := self.bot.get_cog("Talentpool"): - num_nominated = len(cog.cache) if cog.cache else "-" + talentpool_cog: TalentPool | None = self.bot.get_cog("Talentpool") + if talentpool_cog: + num_nominated = len(talentpool_cog.cache) if talentpool_cog.cache else "-" talentpool_info = f"Nominated: {num_nominated}\n" bb_info = "" - if cog := self.bot.get_cog("Big Brother"): - bb_info = f"BB-watched: {len(cog.watched_users)}\n" + bb_cog: BigBrother | None = self.bot.get_cog("Big Brother") + if bb_cog: + bb_info = f"BB-watched: {len(bb_cog.watched_users)}\n" defcon_info = "" - if cog := self.bot.get_cog("Defcon"): - threshold = time.humanize_delta(cog.threshold) if cog.threshold else "-" + defcon_cog: Defcon | None = self.bot.get_cog("Defcon") + if defcon_cog: + threshold = time.humanize_delta(defcon_cog.threshold) if defcon_cog.threshold else "-" defcon_info = f"Defcon threshold: {threshold}\n" verification = f"Verification level: {ctx.guild.verification_level.name}\n" From 7900bce46ad8028f16575f81a76ddc9a53a14c8c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 27 Nov 2022 14:51:18 +0000 Subject: [PATCH 38/50] Fetch active nominations from the API for !server command The nomination cache was recently removed, so accessing that no longer works. --- bot/exts/info/information.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 24e12a3130..c680da2bc1 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -81,12 +81,12 @@ def get_member_counts(guild: Guild) -> dict[str, int]: ) return role_stats - def get_extended_server_info(self, ctx: Context) -> str: + async def get_extended_server_info(self, ctx: Context) -> str: """Return additional server info only visible in moderation channels.""" talentpool_info = "" talentpool_cog: TalentPool | None = self.bot.get_cog("Talentpool") if talentpool_cog: - num_nominated = len(talentpool_cog.cache) if talentpool_cog.cache else "-" + num_nominated = len(await talentpool_cog.api.get_nominations(active=True)) talentpool_info = f"Nominated: {num_nominated}\n" bb_info = "" @@ -232,7 +232,7 @@ async def server_info(self, ctx: Context) -> None: # Additional info if ran in moderation channels if is_mod_channel(ctx.channel): - embed.add_field(name="Moderation:", value=self.get_extended_server_info(ctx)) + embed.add_field(name="Moderation:", value=await self.get_extended_server_info(ctx)) await ctx.send(embed=embed) From 1f592cb782e3ae7e7304cdaf8ff5dab2352b380a Mon Sep 17 00:00:00 2001 From: shtlrs Date: Sun, 27 Nov 2022 17:35:35 +0100 Subject: [PATCH 39/50] include Thread & PrivateChannel in get_or_fetch_channel return types --- bot/utils/channel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/utils/channel.py b/bot/utils/channel.py index 821a3732ab..20f433a3fd 100644 --- a/bot/utils/channel.py +++ b/bot/utils/channel.py @@ -48,7 +48,9 @@ def is_in_category(channel: discord.TextChannel, category_id: int) -> bool: return getattr(channel, "category_id", None) == category_id -async def get_or_fetch_channel(channel_id: int) -> discord.abc.GuildChannel: +async def get_or_fetch_channel( + channel_id: int +) -> discord.abc.GuildChannel | discord.abc.PrivateChannel | discord.Thread: """Attempt to get or fetch a channel and return it.""" log.trace(f"Getting the channel {channel_id}.") From e4cf98f6ee423a32f62ee470dc7bd798288f636c Mon Sep 17 00:00:00 2001 From: Steele Farnsworth Date: Sun, 27 Nov 2022 12:13:53 -0500 Subject: [PATCH 40/50] Update return.md (#2325) Made the tag more brief without any substantial changes to its overall approach. Co-authored-by: wookie184 --- bot/resources/tags/return.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/bot/resources/tags/return.md b/bot/resources/tags/return.md index e37f0eebce..1d65ab1ae0 100644 --- a/bot/resources/tags/return.md +++ b/bot/resources/tags/return.md @@ -1,27 +1,25 @@ **Return Statement** -When calling a function, you'll often want it to give you a value back. In order to do that, you must `return` it. The reason for this is because functions have their own scope. Any values defined within the function body are inaccessible outside of that function. - -*For more information about scope, see `!tags scope`* +A value created inside a function can't be used outside of it unless you `return` it. Consider the following function: ```py def square(n): - return n*n + return n * n ``` -If we wanted to store 5 squared in a variable called `x`, we could do that like so: +If we wanted to store 5 squared in a variable called `x`, we would do: `x = square(5)`. `x` would now equal `25`. **Common Mistakes** ```py >>> def square(n): -... n*n # calculates then throws away, returns None +... n * n # calculates then throws away, returns None ... >>> x = square(5) >>> print(x) None >>> def square(n): -... print(n*n) # calculates and prints, then throws away and returns None +... print(n * n) # calculates and prints, then throws away and returns None ... >>> x = square(5) 25 @@ -29,7 +27,6 @@ None None ``` **Things to note** -• `print()` and `return` do **not** accomplish the same thing. `print()` will only print the value, it will not be accessible outside of the function afterwards. -• A function will return `None` if it ends without reaching an explicit `return` statement. -• When you want to print a value calculated in a function, instead of printing inside the function, it is often better to return the value and print the *function call* instead. -• [Official documentation for `return`](https://docs.python.org/3/reference/simple_stmts.html#the-return-statement) +• `print()` and `return` do **not** accomplish the same thing. `print()` will show the value, and then it will be gone. +• A function will return `None` if it ends without a `return` statement. +• When you want to print a value from a function, it's best to return the value and print the *function call* instead, like `print(square(5))`. From dab4a60fe68acdccdffe2652737c7f1c9e3c9572 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 28 Nov 2022 18:24:44 +0000 Subject: [PATCH 41/50] Use the old stat slugs in new help system --- bot/exts/help_channels/_channel.py | 1 + bot/exts/help_channels/_stats.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 0cee248174..5c47a3559f 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -117,6 +117,7 @@ async def send_opened_post_dm(post: discord.Thread) -> None: async def help_post_opened(opened_post: discord.Thread, *, reopen: bool = False) -> None: """Apply new post logic to a new help forum post.""" _stats.report_post_count() + bot.instance.stats.incr("help.claimed") if not isinstance(opened_post.owner, discord.Member): log.debug(f"{opened_post.owner_id} isn't a member. Closing post.") diff --git a/bot/exts/help_channels/_stats.py b/bot/exts/help_channels/_stats.py index 8ab93f19da..1075b439e0 100644 --- a/bot/exts/help_channels/_stats.py +++ b/bot/exts/help_channels/_stats.py @@ -23,7 +23,7 @@ class ClosingReason(Enum): def report_post_count() -> None: """Report post count stats of the help forum.""" help_forum = bot.instance.get_channel(constants.Channels.help_system_forum) - bot.instance.stats.gauge("help_forum.total.in_use", len(help_forum.threads)) + bot.instance.stats.gauge("help.total.in_use", len(help_forum.threads)) async def report_complete_session(help_session_post: discord.Thread, closed_on: ClosingReason) -> None: @@ -32,13 +32,13 @@ async def report_complete_session(help_session_post: discord.Thread, closed_on: `closed_on` is the reason why the post was closed. See `ClosingReason` for possible reasons. """ - bot.instance.stats.incr(f"help_forum.dormant_calls.{closed_on.value}") + bot.instance.stats.incr(f"help.dormant_calls.{closed_on.value}") open_time = discord.utils.snowflake_time(help_session_post.id) in_use_time = arrow.utcnow() - open_time - bot.instance.stats.timing("help_forum.in_use_time", in_use_time) + bot.instance.stats.timing("help.in_use_time", in_use_time) if await _caches.posts_with_non_claimant_messages.get(help_session_post.id): - bot.instance.stats.incr("help_forum.sessions.answered") + bot.instance.stats.incr("help.sessions.answered") else: - bot.instance.stats.incr("help_forum.sessions.unanswered") + bot.instance.stats.incr("help.sessions.unanswered") From 44d36074188af093d7b56eac48d312cd2862b7ad Mon Sep 17 00:00:00 2001 From: Janine vN Date: Tue, 29 Nov 2022 18:40:03 -0500 Subject: [PATCH 42/50] Rename print-return to return-gif Adds the previous name as an alias as well --- bot/resources/tags/{print-return.md => return-gif.md} | 1 + 1 file changed, 1 insertion(+) rename bot/resources/tags/{print-return.md => return-gif.md} (87%) diff --git a/bot/resources/tags/print-return.md b/bot/resources/tags/return-gif.md similarity index 87% rename from bot/resources/tags/print-return.md rename to bot/resources/tags/return-gif.md index 89d37053fe..1229151fe7 100644 --- a/bot/resources/tags/print-return.md +++ b/bot/resources/tags/return-gif.md @@ -1,4 +1,5 @@ --- +aliases: ["print-return", "return-jif"] embed: title: Print and Return image: From 36ee87200f837e2bec038f274ac3ccc39d7e45d5 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Wed, 30 Nov 2022 19:11:49 +0000 Subject: [PATCH 43/50] Don't count bot messages as an answer in a thread --- bot/exts/help_channels/_caches.py | 2 +- bot/exts/help_channels/_cog.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_caches.py b/bot/exts/help_channels/_caches.py index 5d98f99d3b..3369fc0a61 100644 --- a/bot/exts/help_channels/_caches.py +++ b/bot/exts/help_channels/_caches.py @@ -9,6 +9,6 @@ # RedisCache[discord.TextChannel.id, str[set[discord.User.id]]] session_participants = RedisCache(namespace="HelpChannels.session_participants") -# Stores posts that have had a non-claimant reply. +# Stores posts that have had a non-claimant, non-bot, reply. # Currently only used to determine whether the post was answered or not when collecting stats. posts_with_non_claimant_messages = RedisCache(namespace="HelpChannels.posts_with_non_claimant_messages") diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 31f30b7aaf..bc6bd03037 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -179,5 +179,5 @@ async def new_post_message_listener(self, message: discord.Message) -> None: await _message.notify_session_participants(message) - if message.author.id != message.channel.owner_id: + if not message.author.bot and message.author.id != message.channel.owner_id: await _caches.posts_with_non_claimant_messages.set(message.channel.id, "sentinel") From 5708021c42e8d4315b233c5e06ff447ea4334576 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 4 Dec 2022 14:04:03 +0000 Subject: [PATCH 44/50] Fix help channels with no content not opening properly Chanels that just contained one or more images as an exampole would have the message.content equal to the empty string which != None, so the current check never got hit. Closes #2355 Closes BOT-3BW --- bot/exts/help_channels/_channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 5c47a3559f..fad2a32a9f 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -94,7 +94,7 @@ async def send_opened_post_dm(post: discord.Thread) -> None: return formatted_message = textwrap.shorten(message.content, width=100, placeholder="...").strip() - if formatted_message is None: + if not formatted_message: # This most likely means the initial message is only an image or similar formatted_message = "No text content." From 00710789e98617ca5219eefbe5a17490eaf440c8 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 7 Dec 2022 14:48:27 +0000 Subject: [PATCH 45/50] Remove thread creation logging --- bot/exts/moderation/modlog.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index d916d1f4db..2c0d45da08 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -846,23 +846,6 @@ async def on_thread_delete(self, thread: Thread) -> None: ) ) - @Cog.listener() - async def on_thread_create(self, thread: Thread) -> None: - """Log thread creation.""" - if self.is_channel_ignored(thread.id): - log.trace("Ignoring creation of thread %s (%d)", thread.mention, thread.id) - return - - await self.send_log_message( - Icons.hash_green, - Colours.soft_green, - "Thread created", - ( - f"Thread {thread.mention} ({thread.name}, `{thread.id}`) from {thread.parent.mention} " - f"(`{thread.parent.id}`) created" - ) - ) - @Cog.listener() async def on_voice_state_update( self, From 082308949f48ce792d080baa106046360bbcfb02 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 7 Dec 2022 14:49:21 +0000 Subject: [PATCH 46/50] Move thread logging to message change log --- bot/exts/moderation/modlog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 2c0d45da08..eeaf69139b 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -826,7 +826,8 @@ async def on_thread_update(self, before: Thread, after: Thread) -> None: ( f"Thread {after.mention} ({after.name}, `{after.id}`) from {after.parent.mention} " f"(`{after.parent.id}`) was {action}" - ) + ), + channel_id=Channels.message_log, ) @Cog.listener() @@ -843,7 +844,8 @@ async def on_thread_delete(self, thread: Thread) -> None: ( f"Thread {thread.mention} ({thread.name}, `{thread.id}`) from {thread.parent.mention} " f"(`{thread.parent.id}`) deleted" - ) + ), + channel_id=Channels.message_log, ) @Cog.listener() From 533695dc4c09a0d00150661f1aba23d6134642b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Dec 2022 08:44:21 +0000 Subject: [PATCH 47/50] Bump certifi from 2022.9.24 to 2022.12.7 Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.9.24 to 2022.12.7. - [Release notes](https://github.com/certifi/python-certifi/releases) - [Commits](https://github.com/certifi/python-certifi/compare/2022.09.24...2022.12.07) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4461e8b3fc..f418c05169 100644 --- a/poetry.lock +++ b/poetry.lock @@ -105,7 +105,7 @@ lxml = ["lxml"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -1234,8 +1234,8 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, ] certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, From 5e0c8b78e520abbc780aba89ce470ba5a087a458 Mon Sep 17 00:00:00 2001 From: Bradley Reynolds Date: Tue, 20 Dec 2022 12:41:15 -0600 Subject: [PATCH 48/50] update create reminder permission error to mention bot-commands --- bot/exts/utils/reminders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 1991a687f8..368f085104 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -13,7 +13,7 @@ from bot.bot import Bot from bot.constants import ( - Guild, Icons, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES + Channels, Guild, Icons, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES ) from bot.converters import Duration, UnambiguousUser from bot.errors import LockedResourceError @@ -280,7 +280,8 @@ async def new_reminder( # If they don't have permission to set a reminder in this channel if ctx.channel.id not in WHITELISTED_CHANNELS: - await send_denial(ctx, "Sorry, you can't do that here!") + bot_commands = ctx.guild.get_channel(Channels.bot_commands) + await send_denial(ctx, f"Sorry, you can only do that in {bot_commands.mention}!") return # Get their current active reminders From 5bde6089203c95b971a259bb23e1f3569bff128a Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Thu, 29 Dec 2022 01:07:38 +0100 Subject: [PATCH 49/50] Link previous nomination threads to a user nomination's history (#2319) Co-authored-by: Amrou Bellalouna Closes https://github.com/python-discord/bot/issues/2304 --- bot/exts/recruitment/talentpool/_api.py | 4 ++++ bot/exts/recruitment/talentpool/_cog.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/bot/exts/recruitment/talentpool/_api.py b/bot/exts/recruitment/talentpool/_api.py index fee23826d7..c00c8c09cd 100644 --- a/bot/exts/recruitment/talentpool/_api.py +++ b/bot/exts/recruitment/talentpool/_api.py @@ -23,6 +23,7 @@ class Nomination(BaseModel): ended_at: datetime | None entries: list[NominationEntry] reviewed: bool + thread_id: int | None class NominationAPI: @@ -65,6 +66,7 @@ async def edit_nomination( end_reason: str | None = None, active: bool | None = None, reviewed: bool | None = None, + thread_id: int | None = None, ) -> Nomination: """ Edit a nomination. @@ -78,6 +80,8 @@ async def edit_nomination( data["active"] = active if reviewed is not None: data["reviewed"] = reviewed + if thread_id is not None: + data["thread_id"] = thread_id result = await self.site_api.patch(f"bot/nominations/{nomination_id}", json=data) return Nomination.parse_obj(result) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index dbc3ea538f..a41d9e8c5b 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -17,6 +17,7 @@ from bot.log import get_logger from bot.pagination import LinePaginator from bot.utils import time +from bot.utils.channel import get_or_fetch_channel from bot.utils.members import get_or_fetch_member from ._api import Nomination, NominationAPI @@ -489,6 +490,17 @@ async def _nomination_to_string(self, nomination: Nomination) -> str: entries_string = "\n\n".join(entries) start_date = time.discord_timestamp(nomination.inserted_at) + + thread_jump_url = "*Not created*" + + if nomination.thread_id: + try: + thread = await get_or_fetch_channel(nomination.thread_id) + except discord.HTTPException: + thread_jump_url = "*Not found*" + else: + thread_jump_url = f'[Jump to thread!]({thread.jump_url})' + if nomination.active: lines = textwrap.dedent( f""" @@ -496,6 +508,7 @@ async def _nomination_to_string(self, nomination: Nomination) -> str: Status: **Active** Date: {start_date} Nomination ID: `{nomination.id}` + Nomination vote thread: {thread_jump_url} {entries_string} =============== @@ -509,6 +522,7 @@ async def _nomination_to_string(self, nomination: Nomination) -> str: Status: Inactive Date: {start_date} Nomination ID: `{nomination.id}` + Nomination vote thread: {thread_jump_url} {entries_string} From 870cf7fb25152afbe31f2a2c4d3cc0f26a714d41 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sat, 31 Dec 2022 17:52:54 +0100 Subject: [PATCH 50/50] Link previous nomination threads to a user nomination's history (#2373) * add thread_id to pydantic model * add thread mentions to review history * add list of threads to a review's markdown file * replace thread mention with jump url * add style to thread not found message * use get_or_fetch_channel to look from thread * use jump_url in the review markdown file * catch HttpException when threads are not found * remove link syntaxt for previous nomination threads upon getting a review * check for whether thread_id is null or not in history nominations Co-authored-by: Amrou Bellalouna --- bot/exts/recruitment/talentpool/_review.py | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 876f95369e..f41e08fe10 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta, timezone from typing import List, Optional, Union +import discord from discord import Embed, Emoji, Member, Message, NotFound, PartialMessage, TextChannel from pydis_core.site_api import ResponseCodeError @@ -16,6 +17,7 @@ from bot.exts.recruitment.talentpool._api import Nomination, NominationAPI from bot.log import get_logger from bot.utils import time +from bot.utils.channel import get_or_fetch_channel from bot.utils.members import get_or_fetch_member from bot.utils.messages import count_unique_users_reaction, pin_no_system_message @@ -180,7 +182,7 @@ async def post_review(self, nomination: Nomination) -> None: ) message = await thread.send(f"<@&{Roles.mod_team}> <@&{Roles.admins}>") - await self.api.edit_nomination(nomination.id, reviewed=True) + await self.api.edit_nomination(nomination.id, reviewed=True, thread_id=thread.id) bump_cog: ThreadBumper = self.bot.get_cog("ThreadBumper") if bump_cog: @@ -433,11 +435,30 @@ async def _previous_nominations_review(self, member: Member) -> Optional[str]: nomination_times = f"{num_entries} times" if num_entries > 1 else "once" rejection_times = f"{len(history)} times" if len(history) > 1 else "once" + thread_jump_urls = [] + + for nomination in history: + if nomination.thread_id is None: + continue + try: + thread = await get_or_fetch_channel(nomination.thread_id) + except discord.HTTPException: + # Nothing to do here + pass + else: + thread_jump_urls.append(thread.jump_url) + + if not thread_jump_urls: + nomination_vote_threads = "No nomination threads have been found for this user." + else: + nomination_vote_threads = ", ".join(thread_jump_urls) + end_time = time.format_relative(history[0].ended_at) review = ( f"They were nominated **{nomination_times}** before" f", but their nomination was called off **{rejection_times}**." + f"\nList of all of their nomination threads: {nomination_vote_threads}" f"\nThe last one ended {end_time} with the reason: {history[0].end_reason}" )