diff options
author | 2023-02-28 21:38:57 +0200 | |
---|---|---|
committer | 2023-02-28 21:38:57 +0200 | |
commit | 66627a91334d07106ecc0ccb594755f841309c08 (patch) | |
tree | 9c05aa0971ac50336506efa6c52c097678a665fc | |
parent | Fix antispam alerting (diff) | |
parent | Merge pull request #2421 from shtlrs/bump-pydis-core-to-9-5-0 (diff) |
Merge branch 'main' into new-filters
157 files changed, 2417 insertions, 2815 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2b3e422da..816bdf290 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,7 +4,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 diff --git a/.gitignore b/.gitignore index 177345908..6691dbea1 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8a90ac00..47fd80f97 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: python-check-blanket-noqa - repo: https://github.com/pycqa/isort - rev: 5.8.0 + rev: 5.12.0 hooks: - id: isort name: isort (python) diff --git a/bot/__init__.py b/bot/__init__.py index c652897be..290ca682b 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -2,7 +2,7 @@ import asyncio 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 02af2e9ef..c8843e1a3 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -3,9 +3,9 @@ import asyncio 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 e40c3f8c1..f56aec38e 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -1,9 +1,8 @@ import asyncio -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 +20,7 @@ class StartupError(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/constants.py b/bot/constants.py index 1d6ab8e7e..17a1b90f8 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -363,9 +363,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 @@ -392,8 +389,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 @@ -445,6 +441,8 @@ class Channels(metaclass=YAMLGetter): big_brother_logs: int + roles: int + class Webhooks(metaclass=YAMLGetter): section = "guild" @@ -467,6 +465,7 @@ class Roles(metaclass=YAMLGetter): lovefest: int pyweek_announcements: int revival_of_code: int + legacy_help_channels_access: int contributors: int help_cooldown: int @@ -583,19 +582,9 @@ class HelpChannels(metaclass=YAMLGetter): section = 'help_channels' enable: bool - cmd_whitelist: List[int] - idle_minutes_claimant: int - idle_minutes_others: int + idle_minutes: 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 + cmd_whitelist: List[int] class RedirectOutput(metaclass=YAMLGetter): diff --git a/bot/converters.py b/bot/converters.py index 3db5c6e10..21623b597 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -8,12 +8,12 @@ from ssl import CertificateError 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, 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 466770c3a..2ddc7ee96 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -5,10 +5,10 @@ import typing as t 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 f9ded79f0..8883f7566 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 import Embed, Member 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 @@ -66,7 +66,7 @@ class ErrorHandler(Cog): if isinstance(e, errors.CommandNotFound) and not getattr(ctx, "invoked_from_error_handler", False): if await self.try_silence(ctx): return - if await self.try_run_eval(ctx): + if await self.try_run_fixed_codeblock(ctx): return await self.try_get_tag(ctx) # Try to look for a tag with the command's name elif isinstance(e, errors.UserInputError): @@ -167,32 +167,37 @@ class ErrorHandler(Cog): by `on_command_error`, but the `invoked_from_error_handler` attribute will be added to the context to prevent infinite recursion in the case of a CommandNotFound exception. """ - tags_get_command = self.bot.get_command("tags get") - if not tags_get_command: - log.debug("Not attempting to parse message as a tag as could not find `tags get` command.") + tags_cog = self.bot.get_cog("Tags") + if not tags_cog: + log.debug("Not attempting to parse message as a tag as could not find `Tags` cog.") return + tags_get_command = tags_cog.get_command_ctx - ctx.invoked_from_error_handler = True + maybe_tag_name = ctx.invoked_with + if not maybe_tag_name or not isinstance(ctx.author, Member): + return - log_msg = "Cancelling attempt to fall back to a tag due to failed checks." + ctx.invoked_from_error_handler = True try: - if not await tags_get_command.can_run(ctx): - log.debug(log_msg) + if not await self.bot.can_run(ctx): + log.debug("Cancelling attempt to fall back to a tag due to failed checks.") return - except errors.CommandError as tag_error: - log.debug(log_msg) - await self.on_command_error(ctx, tag_error) - return - if await ctx.invoke(tags_get_command, argument_string=ctx.message.content): - return + if await tags_get_command(ctx, maybe_tag_name): + return - if not any(role.id in MODERATION_ROLES for role in ctx.author.roles): - await self.send_command_suggestion(ctx, ctx.invoked_with) + if not any(role.id in MODERATION_ROLES for role in ctx.author.roles): + await self.send_command_suggestion(ctx, maybe_tag_name) + except Exception as err: + log.debug("Error while attempting to invoke tag fallback.") + if isinstance(err, errors.CommandError): + await self.on_command_error(ctx, err) + else: + await self.on_command_error(ctx, errors.CommandInvokeError(err)) - async def try_run_eval(self, ctx: Context) -> bool: + async def try_run_fixed_codeblock(self, ctx: Context) -> bool: """ - Attempt to run eval command with backticks directly after command. + Attempt to run eval or timeit command with triple backticks directly after command. For example: !eval```print("hi")``` @@ -204,11 +209,18 @@ class ErrorHandler(Cog): msg.content = command + " " + sep + end new_ctx = await self.bot.get_context(msg) - eval_command = self.bot.get_command("eval") - if eval_command is None or new_ctx.command != eval_command: + if new_ctx.command is None: + return False + + allowed_commands = [ + self.bot.get_command("eval"), + self.bot.get_command("timeit"), + ] + + if new_ctx.command not in allowed_commands: return False - log.debug("Running fixed eval command.") + log.debug("Running %r command with fixed codeblock.", new_ctx.command.qualified_name) new_ctx.invoked_from_error_handler = True await self.bot.invoke(new_ctx) diff --git a/bot/exts/backend/logging.py b/bot/exts/backend/logging.py index b9504c2eb..eba9f3c74 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 433ff5024..8c7dbb54e 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 8976245e3..f68674f8d 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -3,10 +3,10 @@ import typing as t 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/filtering/_filter_lists/antispam.py b/bot/exts/filtering/_filter_lists/antispam.py index c5ce292e3..0e7ab2bdc 100644 --- a/bot/exts/filtering/_filter_lists/antispam.py +++ b/bot/exts/filtering/_filter_lists/antispam.py @@ -8,9 +8,9 @@ from itertools import takewhile from operator import add, or_ import arrow -from botcore.utils import scheduling -from botcore.utils.logging import get_logger from discord import Member +from pydis_core.utils import scheduling +from pydis_core.utils.logging import get_logger from bot.exts.filtering._filter_context import FilterContext from bot.exts.filtering._filter_lists.filter_list import ListType, SubscribingAtomicList, UniquesListBase diff --git a/bot/exts/filtering/_filter_lists/invite.py b/bot/exts/filtering/_filter_lists/invite.py index c2a75e831..bd0eaa122 100644 --- a/bot/exts/filtering/_filter_lists/invite.py +++ b/bot/exts/filtering/_filter_lists/invite.py @@ -2,9 +2,9 @@ from __future__ import annotations import typing -from botcore.utils.regex import DISCORD_INVITE from discord import Embed, Invite from discord.errors import NotFound +from pydis_core.utils.regex import DISCORD_INVITE import bot from bot.exts.filtering._filter_context import Event, FilterContext diff --git a/bot/exts/filtering/_filter_lists/unique.py b/bot/exts/filtering/_filter_lists/unique.py index fdf22e3b6..a5a04d25a 100644 --- a/bot/exts/filtering/_filter_lists/unique.py +++ b/bot/exts/filtering/_filter_lists/unique.py @@ -1,4 +1,4 @@ -from botcore.utils.logging import get_logger +from pydis_core.utils.logging import get_logger from bot.exts.filtering._filter_context import FilterContext from bot.exts.filtering._filter_lists.filter_list import ListType, UniquesListBase diff --git a/bot/exts/filtering/_filters/antispam/mentions.py b/bot/exts/filtering/_filters/antispam/mentions.py index 29a2d5606..f3c945e16 100644 --- a/bot/exts/filtering/_filters/antispam/mentions.py +++ b/bot/exts/filtering/_filters/antispam/mentions.py @@ -3,9 +3,9 @@ from itertools import takewhile from typing import ClassVar import arrow -from botcore.utils.logging import get_logger from discord import DeletedReferencedMessage, MessageType, NotFound from pydantic import BaseModel +from pydis_core.utils.logging import get_logger import bot from bot.exts.filtering._filter_context import Event, FilterContext diff --git a/bot/exts/filtering/_filters/invite.py b/bot/exts/filtering/_filters/invite.py index f68adbb23..799a302b9 100644 --- a/bot/exts/filtering/_filters/invite.py +++ b/bot/exts/filtering/_filters/invite.py @@ -1,6 +1,6 @@ -from botcore.utils.regex import DISCORD_INVITE from discord import NotFound from discord.ext.commands import BadArgument +from pydis_core.utils.regex import DISCORD_INVITE import bot from bot.exts.filtering._filter_context import FilterContext diff --git a/bot/exts/filtering/_filters/unique/discord_token.py b/bot/exts/filtering/_filters/unique/discord_token.py index b2ac43cdb..6174ee30b 100644 --- a/bot/exts/filtering/_filters/unique/discord_token.py +++ b/bot/exts/filtering/_filters/unique/discord_token.py @@ -4,9 +4,9 @@ from collections.abc import Callable, Coroutine from typing import ClassVar, NamedTuple import discord -from botcore.utils.logging import get_logger -from botcore.utils.members import get_or_fetch_member from pydantic import BaseModel, Field +from pydis_core.utils.logging import get_logger +from pydis_core.utils.members import get_or_fetch_member import bot from bot import constants, utils diff --git a/bot/exts/filtering/_filters/unique/rich_embed.py b/bot/exts/filtering/_filters/unique/rich_embed.py index 00c28e571..2ee469f51 100644 --- a/bot/exts/filtering/_filters/unique/rich_embed.py +++ b/bot/exts/filtering/_filters/unique/rich_embed.py @@ -1,6 +1,6 @@ import re -from botcore.utils.logging import get_logger +from pydis_core.utils.logging import get_logger from bot.exts.filtering._filter_context import Event, FilterContext from bot.exts.filtering._filters.filter import UniqueFilter diff --git a/bot/exts/filtering/_filters/unique/webhook.py b/bot/exts/filtering/_filters/unique/webhook.py index 16ff1b213..965ef42eb 100644 --- a/bot/exts/filtering/_filters/unique/webhook.py +++ b/bot/exts/filtering/_filters/unique/webhook.py @@ -1,7 +1,7 @@ import re from collections.abc import Callable, Coroutine -from botcore.utils.logging import get_logger +from pydis_core.utils.logging import get_logger import bot from bot import constants diff --git a/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py b/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py index 48249115b..c3c44616d 100644 --- a/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py +++ b/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py @@ -4,11 +4,11 @@ from typing import ClassVar import arrow import discord.abc -from botcore.utils.logging import get_logger -from botcore.utils.members import get_or_fetch_member from discord import Colour, Embed, Member, User from discord.errors import Forbidden from pydantic import validator +from pydis_core.utils.logging import get_logger +from pydis_core.utils.members import get_or_fetch_member from typing_extensions import Self import bot as bot_module diff --git a/bot/exts/filtering/_settings_types/actions/remove_context.py b/bot/exts/filtering/_settings_types/actions/remove_context.py index a34822542..3f219248c 100644 --- a/bot/exts/filtering/_settings_types/actions/remove_context.py +++ b/bot/exts/filtering/_settings_types/actions/remove_context.py @@ -1,10 +1,10 @@ from collections import defaultdict from typing import ClassVar -from botcore.utils import scheduling -from botcore.utils.logging import get_logger from discord import Message from discord.errors import HTTPException +from pydis_core.utils import scheduling +from pydis_core.utils.logging import get_logger from typing_extensions import Self import bot diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py index 1534aa122..1ef25f17a 100644 --- a/bot/exts/filtering/_ui/filter.py +++ b/bot/exts/filtering/_ui/filter.py @@ -4,10 +4,10 @@ from typing import Any, Callable import discord import discord.ui -from botcore.site_api import ResponseCodeError from discord import Embed, Interaction, User from discord.ext.commands import BadArgument from discord.ui.select import SelectOption +from pydis_core.site_api import ResponseCodeError from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType from bot.exts.filtering._filters.filter import Filter diff --git a/bot/exts/filtering/_ui/filter_list.py b/bot/exts/filtering/_ui/filter_list.py index 5a0b1c44a..a4526f090 100644 --- a/bot/exts/filtering/_ui/filter_list.py +++ b/bot/exts/filtering/_ui/filter_list.py @@ -3,9 +3,9 @@ from __future__ import annotations from typing import Any, Callable import discord -from botcore.site_api import ResponseCodeError from discord import Embed, Interaction, SelectOption, User from discord.ext.commands import BadArgument +from pydis_core.site_api import ResponseCodeError from bot.exts.filtering._filter_lists import FilterList, ListType from bot.exts.filtering._ui.ui import ( diff --git a/bot/exts/filtering/_ui/ui.py b/bot/exts/filtering/_ui/ui.py index 24fb507e3..6d4a08d93 100644 --- a/bot/exts/filtering/_ui/ui.py +++ b/bot/exts/filtering/_ui/ui.py @@ -8,14 +8,14 @@ from functools import partial from typing import Any, Callable, Coroutine, Optional, TypeVar import discord -from botcore.site_api import ResponseCodeError -from botcore.utils import scheduling -from botcore.utils.logging import get_logger -from botcore.utils.members import get_or_fetch_member from discord import Embed, Interaction from discord.ext.commands import Context from discord.ui.select import MISSING as SELECT_MISSING, SelectOption from discord.utils import escape_markdown +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils import scheduling +from pydis_core.utils.logging import get_logger +from pydis_core.utils.members import get_or_fetch_member import bot from bot.constants import Colours diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index 26e0f32d2..c4417e5e0 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -12,11 +12,11 @@ from typing import Literal, Optional, get_type_hints import arrow import discord from async_rediscache import RedisCache -from botcore.site_api import ResponseCodeError -from botcore.utils import scheduling from discord import Colour, Embed, HTTPException, Message, MessageType from discord.ext import commands, tasks from discord.ext.commands import BadArgument, Cog, Context, command, has_any_role +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils import scheduling import bot import bot.exts.filtering._ui.filter as filters_ui diff --git a/bot/exts/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py index 5c5fa1dd5..86be8edae 100644 --- a/bot/exts/fun/off_topic_names.py +++ b/bot/exts/fun/off_topic_names.py @@ -6,11 +6,11 @@ import random 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/__init__.py b/bot/exts/help_channels/__init__.py index b9c940183..643497b9f 100644 --- a/bot/exts/help_channels/__init__.py +++ b/bot/exts/help_channels/__init__.py @@ -1,40 +1,15 @@ -from bot import constants + from bot.bot import Bot -from bot.exts.help_channels._channel import MAX_CHANNELS_PER_CATEGORY +from bot.constants import HelpChannels +from bot.exts.help_channels._cog import HelpForum 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." - ) - - 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.""" + if not HelpChannels.enable: + log.warning("HelpChannel.enabled set to false, not loading help channel cog.") + return + await bot.add_cog(HelpForum(bot)) diff --git a/bot/exts/help_channels/_caches.py b/bot/exts/help_channels/_caches.py index 937c4ab57..3369fc0a6 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") @@ -29,3 +8,7 @@ help_dm = RedisCache(namespace="HelpChannels.help_dm") # 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, 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/_channel.py b/bot/exts/help_channels/_channel.py index cfe774f4c..f64162006 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,195 +1,232 @@ -import re -import typing as t +"""Contains all logic to handle changes to posts in the help forum.""" +import textwrap from datetime import timedelta -from enum import Enum import arrow import discord -from arrow import Arrow +from pydis_core.utils import members, scheduling 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<user_id>\d{17,20})>\.$") +ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" +BRANDING_REPO_RAW_URL = "https://raw.githubusercontent.com/python-discord/branding" +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've got one. +• **Explain** what you expect to happen and what actually happens. -class ClosingReason(Enum): - """All possible closing reasons for help channels.""" +For more tips, check out our guide on [asking good questions]({ASKING_GUIDE_URL}). +""" +NEW_POST_FOOTER = f"Closes after a period of inactivity, or when you send {constants.Bot.prefix}close." +NEW_POST_ICON_URL = f"{BRANDING_REPO_RAW_URL}/main/icons/checkmark/green-checkmark-dist.png" - COMMAND = "command" - LATEST_MESSAGE = "auto.latest_message" - CLAIMANT_TIMEOUT = "auto.claimant_timeout" - OTHER_TIMEOUT = "auto.other_timeout" - DELETED = "auto.deleted" - CLEANUP = "auto.cleanup" +CLOSED_POST_MSG = f""" +This help channel has been closed and it's no longer possible to send messages here. \ +If your question wasn't answered, feel free to create a new post in <#{constants.Channels.help_system_forum}>. \ +To maximize your chances of getting a response, check out this guide on [asking good questions]({ASKING_GUIDE_URL}). +""" +CLOSED_POST_ICON_URL = f"{BRANDING_REPO_RAW_URL}/main/icons/zzz/zzz-dist.png" -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_post(closed_post: discord.Thread, closing_reason: _stats.ClosingReason) -> None: + """Close the help post and record stats.""" + embed = discord.Embed(description=CLOSED_POST_MSG) + embed.set_author(name=f"{POST_TITLE} closed", icon_url=CLOSED_POST_ICON_URL) -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. + await closed_post.send(embed=embed) + await closed_post.edit(archived=True, locked=True, reason="Locked a closed help post") - `init_done` is True if the cog has finished loading and False otherwise. + _stats.report_post_count() + await _stats.report_complete_session(closed_post, closing_reason) - The time is calculated as follows: + poster = closed_post.owner + cooldown_role = closed_post.guild.get_role(constants.Roles.help_cooldown) - * 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}).") + 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 post ({closed_post.id}). " + f"The user is likely no longer on the server." + ) + return + + await members.handle_role_change(poster, poster.remove_roles, cooldown_role) + + +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, + description=NEW_POST_MSG, + ) + embed.set_author(name=f"{POST_TITLE} opened", icon_url=NEW_POST_ICON_URL) + embed.set_footer(text=NEW_POST_FOOTER) + await post.send(embed=embed) + + +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 post opened", + description=f"You opened {post.mention}.", + colour=constants.Colours.bright_green, + timestamp=post.created_at, + ) + embed.set_thumbnail(url=constants.Icons.green_questionmark) + message = post.starter_message + if not message: + try: + message = await post.fetch_message(post.id) + except discord.HTTPException: + log.warning(f"Could not fetch message for post {post.id}") + return + + formatted_message = textwrap.shorten(message.content, width=100, placeholder="...").strip() + if not formatted_message: + # 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", + value=f"[Jump to message!]({message.jump_url})", + inline=False, + ) - 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 + try: + 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 {post.owner} ({post.owner_id}) after posting in help forum: DMs disabled.", + ) - 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 +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") - # 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 + 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 + + await send_opened_post_dm(opened_post) - claimant_time = Arrow.utcfromtimestamp(claimant_time) - others_time = await _caches.non_claimant_last_message_times.get(channel.id) + try: + await opened_post.starter_message.pin() + except (discord.HTTPException, AttributeError) as e: + # Suppress if the message was not found, most likely deleted + # 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 - 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 + await send_opened_post_message(opened_post) - # Offset the cached times by the configured values. - others_time += timedelta(minutes=constants.HelpChannels.idle_minutes_others) - claimant_time += timedelta(minutes=idle_minutes_claimant) + 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) - # 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 help_post_closed(closed_post: discord.Thread) -> None: + """Apply archive logic to a manually closed help forum post.""" + await _close_help_post(closed_post, _stats.ClosingReason.COMMAND) -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}.") +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_post.guild.audit_logs(limit=50, action=discord.AuditLogAction.thread_update): + if thread_update.target.id != archived_post.id: + continue - claimed_timestamp = await _caches.claim_times.get(channel_id) - if claimed_timestamp: - claimed = Arrow.utcfromtimestamp(claimed_timestamp) - return arrow.utcnow() - claimed + # 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_post(archived_post, _stats.ClosingReason.INACTIVE) -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 help_post_deleted(deleted_post_event: discord.RawThreadDeleteEvent) -> None: + """Record appropriate stats when a help post is deleted.""" + _stats.report_post_count() + 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) -async def move_to_bottom(channel: discord.TextChannel, category_id: int, **options) -> None: + +async def get_closing_time(post: discord.Thread) -> tuple[arrow.Arrow, _stats.ClosingReason]: """ - Move the `channel` to the bottom position of `category` and edit channel attributes. + Return the time at which the given help `post` should be closed along with the reason. - 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. + 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. - 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. + Otherwise, use the most recent message's create_at date and add `idle_minutes_claimant`. """ - # 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, - } - ) + try: + starter_message = post.starter_message or await post.fetch_message(post.id) + except discord.NotFound: + starter_message = None - # 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) + last_100_messages = [message async for message in post.history(limit=100, oldest_first=False)] - # Now that the channel is moved, we can edit the other attributes - if options: - await channel.edit(**options) + 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 ensure_cached_claimant(channel: discord.TextChannel) -> None: - """ - Ensure there is a claimant cached for each help channel. - 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. +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 a claimant could not be found, send a warning to #helpers and set the claimant to the bot. + If `has_task` is True and rescheduling is required, the extant task to make the post + dormant will first be cancelled. """ - if await _caches.claimants.get(channel.id): + if post.locked: + log.trace(f"Not closing already closed post #{post} ({post.id}).") return - async for message in channel.history(limit=1000): - if message.author.id != bot.instance.user.id: - # We only care about bot messages - 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) + 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 31a33f8af..bc6bd0303 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -1,666 +1,183 @@ -import asyncio -import random +"""Contains the Cog that receives discord.py events and defers most actions to other files in the module.""" + import typing as t -from datetime import timedelta -from operator import attrgetter -import arrow import discord -import discord.abc -from botcore.utils import members, scheduling from discord.ext import commands +from pydis_core.utils import scheduling 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}" +if t.TYPE_CHECKING: + from bot.exts.filters.filtering import Filtering -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 + self.help_forum_channel: discord.ForumChannel = None 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() - + """Cancel all scheduled tasks on unload.""" 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 + 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!") - log.debug(f"Creating a new dormant channel named {name}.") - return await self.dormant_category.create_text_channel(name, topic=HELP_CHANNEL_TOPIC) + 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: - """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) + 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 post {post.mention} ({post.id}), " + "which triggered the token filter with its name!\n" + f"**Match:** {match.group()}" + ) + + @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: + 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}).") + await _channel.help_post_closed(ctx.channel) + if ctx.channel.id in self.scheduler: + self.scheduler.cancel(ctx.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) - - 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( + @help_forum_group.command(name="dm", root_aliases=("helpdm",)) + async def help_dm_command( self, - channel: discord.TextChannel, - claimant_id: t.Optional[int], - closed_on: _channel.ClosingReason + ctx: commands.Context, + state_bool: bool, ) -> 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. + Allows user to toggle "Helping" DMs. - The new time for the dormant task is configured with `HelpChannels.deleted_idle_minutes`. + 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. """ - if not channel_utils.is_in_category(msg.channel, constants.Categories.help_in_use): - return + state_str = "ON" if state_bool else "OFF" - if not await _message.is_empty(msg.channel): + if state_bool == await _caches.help_dm.get(ctx.author.id, False): + await ctx.send(f"{constants.Emojis.cross_mark} {ctx.author.mention} Help DMs are already {state_str}") 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 + if state_bool: + await _caches.help_dm.set(ctx.author.id, True) + else: + await _caches.help_dm.delete(ctx.author.id) + await ctx.send(f"{constants.Emojis.ok_hand} {ctx.author.mention} Help DMs {state_str}!") - log.trace(f"Channel #{channel} ({channel.id}) finally retrieved from the queue.") - self.queue_tasks.remove(task) + @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 - return channel + if not await commands.has_any_role(constants.Roles.helpers).predicate(ctx): + # Silently fail for non-helpers + return - 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 - ) + await ctx.channel.edit(name=title) - 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. + @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 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 message.id == thread.id: + # Opener messages have the same ID as the thread + return - if not await _caches.help_dm.get(message.author.id): - return # Ignore message if user is opted out of help dms + if thread.parent_id != self.help_forum_channel.id: + return - if (await self.bot.get_context(message)).command == self.close_command: - return # Ignore messages that are closing the channel + await self.post_with_disallowed_title_check(thread) + await _channel.help_post_opened(thread) - session_participants = self._deserialise_session_participants( - await _caches.session_participants.get(message.channel.id) or "" + 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) ) - 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( - self, - ctx: commands.Context, - state_bool: bool - ) -> None: - """ - 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. + @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_post_archived(after) + if before.name != after.name: + await self.post_with_disallowed_title_check(after) - 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" + @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_post_deleted(deleted_thread_event) + + @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 - if state_bool == await _caches.help_dm.get(ctx.author.id, False): - await ctx.send(f"{constants.Emojis.cross_mark} {ctx.author.mention} Help DMs are already {state_str}") - return + await _message.notify_session_participants(message) - if state_bool: - await _caches.help_dm.set(ctx.author.id, True) - else: - await _caches.help_dm.delete(ctx.author.id) - await ctx.send(f"{constants.Emojis.ok_hand} {ctx.author.mention} Help DMs {state_str}!") + 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") diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 00d57ea40..98bfe59b8 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]: [email protected]_arg(NAMESPACE, "message", attrgetter("channel.id")) [email protected]_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 a9d9b2df1..000000000 --- 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 4698c26de..1075b439e 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.exts.help_channels import _caches 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.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}") - 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.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") - else: + if await _caches.posts_with_non_claimant_messages.get(help_session_post.id): bot.instance.stats.incr("help.sessions.answered") + else: + bot.instance.stats.incr("help.sessions.unanswered") diff --git a/bot/exts/info/codeblock/_cog.py b/bot/exts/info/codeblock/_cog.py index 95ea54761..d981b95f5 100644 --- a/bot/exts/info/codeblock/_cog.py +++ b/bot/exts/info/codeblock/_cog.py @@ -2,18 +2,18 @@ import time 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 from bot.exts.filtering._filters.unique.discord_token import DiscordTokenFilter from bot.exts.filtering._filters.unique.webhook 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 @@ class CodeBlockCog(Cog, name="Code Block"): """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/exts/info/doc/_batch_parser.py b/bot/exts/info/doc/_batch_parser.py index 41a15fb6e..53d931830 100644 --- a/bot/exts/info/doc/_batch_parser.py +++ b/bot/exts/info/doc/_batch_parser.py @@ -8,8 +8,8 @@ from operator import attrgetter 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 c35349c3c..2d0f28406 100644 --- a/bot/exts/info/doc/_cog.py +++ b/bot/exts/info/doc/_cog.py @@ -10,9 +10,9 @@ from typing import Dict, Literal, NamedTuple, Optional, Tuple, Union 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 2592e093d..c680da2bc 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -3,13 +3,13 @@ import pprint 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 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 @@ -31,6 +31,11 @@ DEFAULT_RULES_DESCRIPTION = ( " 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.""" @@ -76,20 +81,23 @@ class Information(Cog): ) 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 = "" - 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(await talentpool_cog.api.get_nominations(active=True)) 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" @@ -224,7 +232,7 @@ class Information(Cog): # 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) @@ -523,8 +531,19 @@ class Information(Cog): """Shows information about the raw API response in a copy-pasteable Python format.""" await self.send_raw_content(ctx, message, json=True) + async def _set_rules_command_help(self) -> None: + help_string = f"{self.rules.help}\n\n" + help_string += "__Available keywords per rule__:\n\n" + + full_rules = await self.bot.api_client.get("rules", params={"link_format": "md"}) + + for index, (_, keywords) in enumerate(full_rules, start=1): + help_string += f"**Rule {index}**: {', '.join(keywords)}\n\r" + + self.rules.help = help_string + @command(aliases=("rule",)) - async def rules(self, ctx: Context, *args: Optional[str]) -> Optional[Set[int]]: + async def rules(self, ctx: Context, *, args: Optional[str]) -> Optional[Set[int]]: """ Provides a link to all rules or, if specified, displays specific rule(s). @@ -541,13 +560,15 @@ class Information(Cog): for rule_keyword in rule_keywords: keyword_to_rule_number[rule_keyword] = rule_number - for word in args: - try: - rule_numbers.append(int(word)) - except ValueError: - if (kw := word.lower()) not in keyword_to_rule_number: - break - keywords.append(kw) + if args: + for word in args.split(maxsplit=100): + try: + rule_numbers.append(int(word)) + except ValueError: + # Stop on first invalid keyword/index to allow for normal messaging after + if (kw := word.lower()) not in keyword_to_rule_number: + break + keywords.append(kw) if not rule_numbers and not keywords: # Neither rules nor keywords were submitted. Return the default description. @@ -578,6 +599,10 @@ class Information(Cog): return final_rule_numbers + async def cog_load(self) -> None: + """Carry out cog asynchronous initialisation.""" + await self._set_rules_command_help() + async def setup(bot: Bot) -> None: """Load the Information cog.""" diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index e36ce807c..7f4b4f95a 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -5,14 +5,15 @@ from dataclasses import dataclass 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 from bot.decorators import redirect_output from bot.log import get_logger +from bot.utils.channel import get_or_fetch_channel @dataclass(frozen=True) @@ -48,6 +49,7 @@ class AssignableRole: ASSIGNABLE_ROLES = ( AssignableRole(constants.Roles.announcements, None), AssignableRole(constants.Roles.pyweek_announcements, None), + AssignableRole(constants.Roles.legacy_help_channels_access, None), AssignableRole(constants.Roles.lovefest, (1, 2)), AssignableRole(constants.Roles.advent_of_code, (11, 12)), AssignableRole(constants.Roles.revival_of_code, (7, 8, 9, 10)), @@ -60,11 +62,25 @@ log = get_logger(__name__) class RoleButtonView(discord.ui.View): - """A list of SingleRoleButtons to show to the member.""" + """ + A view that holds the list of SingleRoleButtons to show to the member. + + Attributes + __________ + interaction_owner: discord.Member + The member that initiated the interaction + """ + + interaction_owner: discord.Member - def __init__(self, member: discord.Member): - super().__init__() + def __init__(self, member: discord.Member, assignable_roles: list[AssignableRole]): + super().__init__(timeout=DELETE_MESSAGE_AFTER) self.interaction_owner = member + author_roles = [role.id for role in member.roles] + + for index, role in enumerate(assignable_roles): + row = index // ITEMS_PER_ROW + self.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) async def interaction_check(self, interaction: Interaction) -> bool: """Ensure that the user clicking the button is the member who invoked the command.""" @@ -78,12 +94,12 @@ class RoleButtonView(discord.ui.View): class SingleRoleButton(discord.ui.Button): - """A button that adds or removes a role from the member depending on it's current state.""" + """A button that adds or removes a role from the member depending on its current state.""" ADD_STYLE = discord.ButtonStyle.success REMOVE_STYLE = discord.ButtonStyle.red UNAVAILABLE_STYLE = discord.ButtonStyle.secondary - LABEL_FORMAT = "{action} role {role_name}." + LABEL_FORMAT = "{action} role {role_name}" CUSTOM_ID_FORMAT = "subscribe-{role_id}" def __init__(self, role: AssignableRole, assigned: bool, row: int): @@ -123,7 +139,7 @@ class SingleRoleButton(discord.ui.Button): self.assigned = not self.assigned await self.update_view(interaction) - await interaction.response.send_message( + await interaction.followup.send( self.LABEL_FORMAT.format(action="Added" if self.assigned else "Removed", role_name=self.role.name), ephemeral=True, ) @@ -133,15 +149,45 @@ class SingleRoleButton(discord.ui.Button): self.style = self.REMOVE_STYLE if self.assigned else self.ADD_STYLE self.label = self.LABEL_FORMAT.format(action="Remove" if self.assigned else "Add", role_name=self.role.name) try: - await interaction.message.edit(view=self.view) + await interaction.response.edit_message(view=self.view) except discord.NotFound: log.debug("Subscribe message for %s removed before buttons could be updated", interaction.user) self.view.stop() +class AllSelfAssignableRolesView(discord.ui.View): + """A persistent view that'll hold one button allowing interactors to toggle all available self-assignable roles.""" + + def __init__(self, assignable_roles: list[AssignableRole]): + super().__init__(timeout=None) + self.assignable_roles = assignable_roles + + @discord.ui.button( + style=discord.ButtonStyle.success, + label="Show all self assignable roles", + custom_id="toggle-available-roles-button", + row=1 + ) + async def show_all_self_assignable_roles(self, interaction: Interaction, button: discord.ui.Button) -> None: + """Sends the original subscription view containing the available self assignable roles.""" + view = RoleButtonView(interaction.user, self.assignable_roles) + await interaction.response.send_message( + view=view, + ephemeral=True + ) + + class Subscribe(commands.Cog): """Cog to allow user to self-assign & remove the roles present in ASSIGNABLE_ROLES.""" + GREETING_EMOJI = ":wave:" + + SELF_ASSIGNABLE_ROLES_MESSAGE = ( + f"Hi there {GREETING_EMOJI}," + "\nWe have self-assignable roles for server updates and events!" + "\nClick the button below to toggle them:" + ) + def __init__(self, bot: Bot): self.bot = bot self.assignable_roles: list[AssignableRole] = [] @@ -150,7 +196,6 @@ class Subscribe(commands.Cog): async def cog_load(self) -> None: """Initialise the cog by resolving the role IDs in ASSIGNABLE_ROLES to role names.""" await self.bot.wait_until_guild_available() - self.guild = self.bot.get_guild(constants.Guild.id) for role in ASSIGNABLE_ROLES: @@ -170,6 +215,10 @@ class Subscribe(commands.Cog): self.assignable_roles.sort(key=operator.attrgetter("name")) self.assignable_roles.sort(key=operator.methodcaller("is_currently_available"), reverse=True) + placeholder_message_view_tuple = await self._fetch_or_create_self_assignable_roles_message() + self_assignable_roles_message, self_assignable_roles_view = placeholder_message_view_tuple + self._attach_persistent_roles_view(self_assignable_roles_message, self_assignable_roles_view) + @commands.cooldown(1, 10, commands.BucketType.member) @commands.command(name="subscribe", aliases=("unsubscribe",)) @redirect_output( @@ -178,22 +227,58 @@ class Subscribe(commands.Cog): ) async def subscribe_command(self, ctx: commands.Context, *_) -> None: # We don't actually care about the args """Display the member's current state for each role, and allow them to add/remove the roles.""" - button_view = RoleButtonView(ctx.author) - author_roles = [role.id for role in ctx.author.roles] - for index, role in enumerate(self.assignable_roles): - row = index // ITEMS_PER_ROW - button_view.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) - + view = RoleButtonView(ctx.author, self.assignable_roles) await ctx.send( "Click the buttons below to add or remove your roles!", - view=button_view, - delete_after=DELETE_MESSAGE_AFTER, + view=view, + delete_after=DELETE_MESSAGE_AFTER ) + async def _fetch_or_create_self_assignable_roles_message(self) -> tuple[discord.Message, discord.ui.View | None]: + """ + Fetches the message that holds the self assignable roles view. + + If the initial message isn't found, a new one will be created. + This message will always be needed to attach the persistent view to it + """ + roles_channel: discord.TextChannel = await get_or_fetch_channel(constants.Channels.roles) + + async for message in roles_channel.history(limit=30): + if message.content == self.SELF_ASSIGNABLE_ROLES_MESSAGE: + log.debug(f"Found self assignable roles view message: {message.id}") + return message, None + + log.debug("Self assignable roles view message hasn't been found, creating a new one.") + view = AllSelfAssignableRolesView(self.assignable_roles) + placeholder_message = await roles_channel.send(self.SELF_ASSIGNABLE_ROLES_MESSAGE, view=view) + return placeholder_message, view + + def _attach_persistent_roles_view( + self, + placeholder_message: discord.Message, + persistent_roles_view: discord.ui.View | None = None + ) -> None: + """ + Attaches the persistent view that toggles self assignable roles to its placeholder message. + + The message is searched for/created upon loading the Cog. + + Parameters + __________ + :param placeholder_message: The message that will hold the persistent view allowing + users to toggle the RoleButtonView + :param persistent_roles_view: The view attached to the placeholder_message + If none, a new view will be created + """ + if not persistent_roles_view: + persistent_roles_view = AllSelfAssignableRolesView(self.assignable_roles) + + self.bot.add_view(persistent_roles_view, message_id=placeholder_message.id) + async def setup(bot: Bot) -> None: - """Load the Subscribe cog.""" - if len(ASSIGNABLE_ROLES) > ITEMS_PER_ROW*5: # Discord limits views to 5 rows of buttons. + """Load the 'Subscribe' cog.""" + if len(ASSIGNABLE_ROLES) > ITEMS_PER_ROW * 5: # Discord limits views to 5 rows of buttons. log.error("Too many roles for 5 rows, not loading the Subscribe cog.") else: await bot.add_cog(Subscribe(bot)) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 83d3a9d93..309f22cad 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -4,12 +4,12 @@ import enum import re import time from pathlib import Path -from typing import Callable, Iterable, Literal, NamedTuple, Optional, Union +from typing import Literal, NamedTuple, Optional, Union import discord import frontmatter -from discord import Embed, Member -from discord.ext.commands import Cog, Context, group +from discord import Embed, Interaction, Member, app_commands +from discord.ext.commands import Cog, Context from bot import constants from bot.bot import Bot @@ -91,7 +91,7 @@ class Tag: embed.description = self.content return embed - def accessible_by(self, member: discord.Member) -> bool: + def accessible_by(self, member: Member) -> bool: """Check whether `member` can access the tag.""" return bool( not self._restricted_to @@ -182,101 +182,22 @@ class Tags(Cog): return suggestions - def _get_tags_via_content( - self, - check: Callable[[Iterable], bool], - keywords: str, - user: Member, - ) -> list[tuple[TagIdentifier, Tag]]: - """ - Search for tags via contents. - - `predicate` will be the built-in any, all, or a custom callable. Must return a bool. - """ - keywords_processed = [] - for keyword in keywords.split(","): - keyword_sanitized = keyword.strip().casefold() - if not keyword_sanitized: - # this happens when there are leading / trailing / consecutive comma. - continue - keywords_processed.append(keyword_sanitized) - - if not keywords_processed: - # after sanitizing, we can end up with an empty list, for example when keywords is "," - # in that case, we simply want to search for such keywords directly instead. - keywords_processed = [keywords] - - matching_tags = [] - for identifier, tag in self.tags.items(): - matches = (query in tag.content.casefold() for query in keywords_processed) - if tag.accessible_by(user) and check(matches): - matching_tags.append((identifier, tag)) - - return matching_tags - - async def _send_matching_tags( - self, - ctx: Context, - keywords: str, - matching_tags: list[tuple[TagIdentifier, Tag]], - ) -> None: - """Send the result of matching tags to user.""" - if len(matching_tags) == 1: - await ctx.send(embed=matching_tags[0][1].embed) - elif matching_tags: - is_plural = keywords.strip().count(" ") > 0 or keywords.strip().count(",") > 0 - embed = Embed( - title=f"Here are the tags containing the given keyword{'s' * is_plural}:", - ) - await LinePaginator.paginate( - sorted( - f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier.name}" - for identifier, _ in matching_tags - ), - ctx, - embed, - **self.PAGINATOR_DEFAULTS, - ) - - @group(name="tags", aliases=("tag", "t"), invoke_without_command=True, usage="[tag_group] [tag_name]") - async def tags_group(self, ctx: Context, *, argument_string: Optional[str]) -> None: - """Show all known tags, a single tag, or run a subcommand.""" - await self.get_command(ctx, argument_string=argument_string) - - @tags_group.group(name="search", invoke_without_command=True) - async def search_tag_content(self, ctx: Context, *, keywords: str) -> None: - """ - Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma. - - Only search for tags that has ALL the keywords. - """ - matching_tags = self._get_tags_via_content(all, keywords, ctx.author) - await self._send_matching_tags(ctx, keywords, matching_tags) - - @search_tag_content.command(name="any") - async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = "any") -> None: - """ - Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma. - - Search for tags that has ANY of the keywords. - """ - matching_tags = self._get_tags_via_content(any, keywords or "any", ctx.author) - await self._send_matching_tags(ctx, keywords, matching_tags) - async def get_tag_embed( self, - ctx: Context, + member: Member, + channel: discord.abc.Messageable, tag_identifier: TagIdentifier, ) -> Optional[Union[Embed, Literal[COOLDOWN.obj]]]: """ - Generate an embed of the requested tag or of suggestions if the tag doesn't exist/isn't accessible by the user. + Generate an embed of the requested tag or of suggestions if the tag doesn't exist + or isn't accessible by the member. If the requested tag is on cooldown return `COOLDOWN.obj`, otherwise if no suggestions were found return None. - """ + """ # noqa: D205, D415 filtered_tags = [ (ident, tag) for ident, tag in self.get_fuzzy_matches(tag_identifier)[:10] - if tag.accessible_by(ctx.author) + if tag.accessible_by(member) ] # Try exact match, includes checking through alt names @@ -295,10 +216,10 @@ class Tags(Cog): tag = filtered_tags[0][1] if tag is not None: - if tag.on_cooldown_in(ctx.channel): + if tag.on_cooldown_in(channel): log.debug(f"Tag {str(tag_identifier)!r} is on cooldown.") return COOLDOWN.obj - tag.set_cooldown_for(ctx.channel) + tag.set_cooldown_for(channel) self.bot.stats.incr( f"tags.usages" @@ -313,15 +234,15 @@ class Tags(Cog): suggested_tags_text = "\n".join( f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" for identifier, tag in filtered_tags - if not tag.on_cooldown_in(ctx.channel) + if not tag.on_cooldown_in(channel) ) return Embed( title="Did you mean ...", description=suggested_tags_text ) - def accessible_tags(self, user: Member) -> list[str]: - """Return a formatted list of tags that are accessible by `user`; groups first, and alphabetically sorted.""" + def accessible_tags(self, member: Member) -> list[str]: + """Return a formatted list of tags that are accessible by `member`; groups first, and alphabetically sorted.""" def tag_sort_key(tag_item: tuple[TagIdentifier, Tag]) -> str: group, name = tag_item[0] if group is None: @@ -338,7 +259,7 @@ class Tags(Cog): if identifier.group != current_group: if not group_accessible: - # Remove group separator line if no tags in the previous group were accessible by the user. + # Remove group separator line if no tags in the previous group were accessible by the member. result_lines.pop() # A new group began, add a separator with the group name. current_group = identifier.group @@ -348,22 +269,55 @@ class Tags(Cog): else: result_lines.append("\n\N{BULLET}") - if tag.accessible_by(user): + if tag.accessible_by(member): result_lines.append(f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier.name}") group_accessible = True return result_lines - def accessible_tags_in_group(self, group: str, user: discord.Member) -> list[str]: - """Return a formatted list of tags in `group`, that are accessible by `user`.""" + def accessible_tags_in_group(self, group: str, member: Member) -> list[str]: + """Return a formatted list of tags in `group`, that are accessible by `member`.""" return sorted( f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" for identifier, tag in self.tags.items() - if identifier.group == group and tag.accessible_by(user) + if identifier.group == group and tag.accessible_by(member) ) - @tags_group.command(name="get", aliases=("show", "g"), usage="[tag_group] [tag_name]") - async def get_command(self, ctx: Context, *, argument_string: Optional[str]) -> bool: + async def get_command_ctx( + self, + ctx: Context, + name: str + ) -> bool: + """ + Made specifically for `ErrorHandler().try_get_tag` to handle sending tags through ctx. + + See `get_command` for more info, but here name is not optional unlike `get_command`. + """ + identifier = TagIdentifier.from_string(name) + + if identifier.group is None: + # Try to find accessible tags from a group matching the identifier's name. + if group_tags := self.accessible_tags_in_group(identifier.name, ctx.author): + await LinePaginator.paginate( + group_tags, ctx, Embed(title=f"Tags under *{identifier.name}*"), **self.PAGINATOR_DEFAULTS + ) + return True + + embed = await self.get_tag_embed(ctx.author, ctx.channel, identifier) + if embed is None: + return False + + if embed is not COOLDOWN.obj: + + await wait_for_deletion( + await ctx.send(embed=embed), + (ctx.author.id,) + ) + # A valid tag was found and was either sent, or is on cooldown + return True + + @app_commands.command(name="tag") + async def get_command(self, interaction: Interaction, *, name: Optional[str]) -> bool: """ If a single argument matching a group name is given, list all accessible tags from that group Otherwise display the tag if one was found for the given arguments, or try to display suggestions for that name. @@ -373,37 +327,62 @@ class Tags(Cog): Returns True if a message was sent, or if the tag is on cooldown. Returns False if no message was sent. """ # noqa: D205, D415 - if not argument_string: + if not name: if self.tags: await LinePaginator.paginate( - self.accessible_tags(ctx.author), ctx, Embed(title="Available tags"), **self.PAGINATOR_DEFAULTS + self.accessible_tags(interaction.user), + interaction, Embed(title="Available tags"), + **self.PAGINATOR_DEFAULTS, ) else: - await ctx.send(embed=Embed(description="**There are no tags!**")) + await interaction.response.send_message(embed=Embed(description="**There are no tags!**")) return True - identifier = TagIdentifier.from_string(argument_string) + identifier = TagIdentifier.from_string(name) if identifier.group is None: # Try to find accessible tags from a group matching the identifier's name. - if group_tags := self.accessible_tags_in_group(identifier.name, ctx.author): + if group_tags := self.accessible_tags_in_group(identifier.name, interaction.user): await LinePaginator.paginate( - group_tags, ctx, Embed(title=f"Tags under *{identifier.name}*"), **self.PAGINATOR_DEFAULTS + group_tags, interaction, Embed(title=f"Tags under *{identifier.name}*"), **self.PAGINATOR_DEFAULTS ) return True - embed = await self.get_tag_embed(ctx, identifier) + embed = await self.get_tag_embed(interaction.user, interaction.channel, identifier) + ephemeral = False if embed is None: - return False - - if embed is not COOLDOWN.obj: + description = f"**There are no tags matching the name {name!r}!**" + embed = Embed(description=description) + ephemeral = True + elif embed is COOLDOWN.obj: + description = f"Tag {name!r} is on cooldown." + embed = Embed(description=description) + ephemeral = True + + await interaction.response.send_message(embed=embed, ephemeral=ephemeral) + if not ephemeral: await wait_for_deletion( - await ctx.send(embed=embed), - (ctx.author.id,) + await interaction.original_response(), + (interaction.user.id,) ) + # A valid tag was found and was either sent, or is on cooldown return True + @get_command.autocomplete("name") + async def name_autocomplete( + self, + interaction: Interaction, + current: str + ) -> list[app_commands.Choice[str]]: + """Autocompleter for `/tag get` command.""" + names = [tag.name for tag in self.tags.keys()] + choices = [ + app_commands.Choice(name=tag, value=tag) + for tag in names if current.lower() in tag + ] + return choices[:25] if len(choices) > 25 else choices + async def setup(bot: Bot) -> None: """Load the Tags cog.""" diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 7c924ff14..ee870ea57 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -7,12 +7,12 @@ from typing import Optional, Union 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 1ddbe9857..ce83ca3fe 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -6,8 +6,8 @@ from typing import Optional 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 4c275a1f0..9b8e67ec5 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -7,9 +7,9 @@ from gettext import ngettext 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 c03081b07..c2ef80461 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -2,8 +2,8 @@ import typing as t 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 @@ -31,12 +31,13 @@ RULES_URL = "https://pythondiscord.com/pages/rules" Infraction = t.Dict[str, t.Union[str, int, bool]] APPEAL_SERVER_INVITE = "https://discord.gg/WXrCJxWBnm" +MODMAIL_ACCOUNT_ID = "683001325440860340" INFRACTION_TITLE = "Please review our rules" INFRACTION_APPEAL_SERVER_FOOTER = f"\nTo appeal this infraction, join our [appeals server]({APPEAL_SERVER_INVITE})." INFRACTION_APPEAL_MODMAIL_FOOTER = ( '\nIf you would like to discuss or appeal this infraction, ' - 'send a message to the ModMail bot.' + f'send a message to the ModMail bot (<@{MODMAIL_ACCOUNT_ID}>).' ) INFRACTION_AUTHOR_NAME = "Infraction information" diff --git a/bot/exts/moderation/metabase.py b/bot/exts/moderation/metabase.py index c63019882..aeb589b5b 100644 --- a/bot/exts/moderation/metabase.py +++ b/bot/exts/moderation/metabase.py @@ -8,8 +8,8 @@ import arrow 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 511f05c50..47a21753c 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -13,7 +13,7 @@ from discord.ext.commands import Cog, Context from discord.utils import escape_markdown, format_dt, snowflake_time from bot.bot import Bot -from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles +from bot.constants import Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles from bot.log import get_logger from bot.utils import time from bot.utils.messages import format_user, upload_log @@ -164,12 +164,6 @@ class ModLog(Cog, name="ModLog"): 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 = [] @@ -495,7 +489,7 @@ class ModLog(Cog, name="ModLog"): return self.is_channel_ignored(message.channel.id) - def is_channel_ignored(self, channel_id: int) -> bool: + def is_channel_ignored(self, channel: int | GuildChannel | Thread) -> bool: """ Return true if the channel, or parent channel in the case of threads, passed should be ignored by modlog. @@ -504,7 +498,8 @@ class ModLog(Cog, name="ModLog"): 2. Channels that mods do not have view permissions to 3. Channels in constants.Guild.modlog_blacklist """ - channel = self.bot.get_channel(channel_id) + if isinstance(channel, int): + channel = self.bot.get_channel(channel) # Ignore not found channels, DMs, and messages outside of the main guild. if not channel or channel.guild is None or channel.guild.id != GuildConstant.id: @@ -787,13 +782,14 @@ class ModLog(Cog, name="ModLog"): ( 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() async def on_thread_delete(self, thread: Thread) -> None: """Log thread deletion.""" - if self.is_channel_ignored(thread.id): + if self.is_channel_ignored(thread): log.trace("Ignoring deletion of thread %s (%d)", thread.mention, thread.id) return @@ -804,24 +800,8 @@ class ModLog(Cog, name="ModLog"): ( f"Thread {thread.mention} ({thread.name}, `{thread.id}`) from {thread.parent.mention} " f"(`{thread.parent.id}`) deleted" - ) - ) - - @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" - ) + ), + channel_id=Channels.message_log, ) @Cog.listener() diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 7c8e4ac13..16423b3d0 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -3,10 +3,10 @@ import datetime 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 578551d24..682791593 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -5,11 +5,11 @@ from datetime import datetime, timedelta, timezone 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 a96e96511..f0d8c23b8 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -5,8 +5,8 @@ import arrow 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 90f88d040..1901d1c57 100644 --- a/bot/exts/moderation/voice_gate.py +++ b/bot/exts/moderation/voice_gate.py @@ -5,9 +5,9 @@ from datetime import timedelta 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 8701320de..5cb696575 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -7,10 +7,10 @@ from dataclasses import dataclass 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 new file mode 100644 index 000000000..c00c8c09c --- /dev/null +++ b/bot/exts/recruitment/talentpool/_api.py @@ -0,0 +1,114 @@ +from datetime import datetime + +from pydantic import BaseModel, Field, parse_obj_as +from pydis_core.site_api import APIClient + + +class NominationEntry(BaseModel): + """Pydantic model representing a nomination entry.""" + + actor_id: int = Field(alias="actor") + reason: str + inserted_at: datetime + + +class Nomination(BaseModel): + """Pydantic model representing a nomination.""" + + id: int + active: bool + user_id: int = Field(alias="user") + inserted_at: datetime + end_reason: str + ended_at: datetime | None + entries: list[NominationEntry] + reviewed: bool + thread_id: int | None + + +class NominationAPI: + """Abstraction of site API interaction for talentpool.""" + + def __init__(self, site_api: APIClient): + self.site_api = site_api + + async def get_nominations( + self, + user_id: int | None = None, + active: bool | None = None, + ordering: str = "-inserted_at" + ) -> list[Nomination]: + """ + Fetch a list of nominations. + + Passing a value of `None` indicates it shouldn't filtered by. + """ + params = {"ordering": ordering} + if active is not None: + params["active"] = str(active) + if user_id is not None: + params["user__id"] = str(user_id) + + data = await self.site_api.get("bot/nominations", params=params) + nominations = parse_obj_as(list[Nomination], data) + return nominations + + async def get_nomination(self, nomination_id: int) -> Nomination: + """Fetch a nomination by ID.""" + data = await self.site_api.get(f"bot/nominations/{nomination_id}") + nomination = Nomination.parse_obj(data) + return nomination + + async def edit_nomination( + self, + nomination_id: int, + *, + end_reason: str | None = None, + active: bool | None = None, + reviewed: bool | None = None, + thread_id: int | None = None, + ) -> Nomination: + """ + Edit a nomination. + + Passing a value of `None` indicates it shouldn't be updated. + """ + data = {} + if end_reason is not None: + data["end_reason"] = end_reason + if active is not None: + 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) + + async def edit_nomination_entry( + self, + nomination_id: int, + *, + actor_id: int, + reason: str, + ) -> Nomination: + """Edit a nomination entry.""" + data = {"actor": actor_id, "reason": reason} + result = await self.site_api.patch(f"bot/nominations/{nomination_id}", json=data) + return Nomination.parse_obj(result) + + async def post_nomination( + self, + user_id: int, + actor_id: int, + reason: str, + ) -> Nomination: + """Post a nomination to site.""" + data = { + "actor": actor_id, + "reason": reason, + "user": user_id, + } + result = await self.site_api.post("bot/nominations", json=data) + return Nomination.parse_obj(result) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index f69e5a647..a41d9e8c5 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -1,15 +1,14 @@ import asyncio import textwrap -from collections import ChainMap, defaultdict from io import StringIO from typing import Optional, Union 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 @@ -18,8 +17,11 @@ from bot.exts.recruitment.talentpool._review import Reviewer 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 + AUTOREVIEW_ENABLED_KEY = "autoreview_enabled" REASON_MAX_CHARS = 1000 @@ -35,17 +37,13 @@ class TalentPool(Cog, name="Talentpool"): def __init__(self, bot: Bot) -> None: self.bot = bot - self.reviewer = Reviewer(bot, self) - self.cache: Optional[defaultdict[dict]] = None - self.api_default_params = {'active': 'true', 'ordering': '-inserted_at'} - + self.api = NominationAPI(bot.api_client) + self.reviewer = Reviewer(bot, self.api) # This lock lets us avoid cancelling the reviewer loop while the review code is running. self.autoreview_lock = asyncio.Lock() async def cog_load(self) -> None: - """Load user cache and maybe start autoreview loop.""" - await self.refresh_cache() - + """Start autoreview loop if enabled.""" if await self.autoreview_enabled(): self.autoreview_loop.start() @@ -53,27 +51,6 @@ class TalentPool(Cog, name="Talentpool"): """Return whether automatic posting of nomination reviews is enabled.""" return await self.talentpool_settings.get(AUTOREVIEW_ENABLED_KEY, True) - async def refresh_cache(self) -> bool: - """Updates TalentPool users cache.""" - # Wait until logged in to ensure bot api client exists - await self.bot.wait_until_guild_available() - try: - data = await self.bot.api_client.get( - 'bot/nominations', - params=self.api_default_params - ) - except ResponseCodeError as err: - log.exception("Failed to fetch the currently nominated users from the API", exc_info=err) - return False - - self.cache = defaultdict(dict) - - for entry in data: - user_id = entry.pop('user') - self.cache[user_id] = entry - - return True - @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) @has_any_role(*STAFF_ROLES) async def nomination_group(self, ctx: Context) -> None: @@ -155,23 +132,18 @@ class TalentPool(Cog, name="Talentpool"): self, ctx: Context, oldest_first: bool = False, - update_cache: bool = True ) -> None: """ Shows the users that are currently in the talent pool. The optional kwarg `oldest_first` can be used to order the list by oldest nomination. - - The optional kwarg `update_cache` can be used to update the user - cache using the API before listing the users. """ - await self.list_nominated_users(ctx, oldest_first=oldest_first, update_cache=update_cache) + await self.list_nominated_users(ctx, oldest_first=oldest_first) async def list_nominated_users( self, ctx: Context, oldest_first: bool = False, - update_cache: bool = True ) -> None: """ Gives an overview of the nominated users list. @@ -180,31 +152,22 @@ class TalentPool(Cog, name="Talentpool"): review was posted. The optional kwarg `oldest_first` orders the list by oldest entry. - - The optional kwarg `update_cache` specifies whether the cache should - be refreshed by polling the API. """ - successful_update = False - if update_cache: - if not (successful_update := await self.refresh_cache()): - await ctx.send(":warning: Unable to update cache. Data may be inaccurate.") - - nominations = self.cache.items() + nominations = await self.api.get_nominations(active=True) if oldest_first: nominations = reversed(nominations) lines = [] - for user_id, user_data in nominations: - member = await get_or_fetch_member(ctx.guild, user_id) - line = f"• `{user_id}`" + for nomination in nominations: + member = await get_or_fetch_member(ctx.guild, nomination.user_id) + line = f"• `{nomination.user_id}`" if member: line += f" ({member.name}#{member.discriminator})" - inserted_at = user_data['inserted_at'] - line += f", added {time.format_relative(inserted_at)}" + line += f", added {time.format_relative(nomination.inserted_at)}" if not member: # Cross off users who left the server. line = f"~~{line}~~" - if user_data['reviewed']: + if nomination.reviewed: line += " *(reviewed)*" lines.append(line) @@ -212,21 +175,16 @@ class TalentPool(Cog, name="Talentpool"): lines = ("There's nothing here yet.",) embed = Embed( - title=f"Talent Pool active nominations ({'updated' if update_cache and successful_update else 'cached'})", + title="Talent Pool active nominations", color=Color.blue() ) await LinePaginator.paginate(lines, ctx, embed, empty=False) @nomination_group.command(name='oldest') @has_any_role(*MODERATION_ROLES) - async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None: - """ - Shows talent pool users ordered by oldest nomination. - - The optional kwarg `update_cache` can be used to update the user - cache using the API before listing the users. - """ - await ctx.invoke(self.list_command, oldest_first=True, update_cache=update_cache) + async def oldest_command(self, ctx: Context) -> None: + """Shows the users that are currently in the talent pool, ordered by oldest nomination.""" + await self.list_nominated_users(ctx, oldest_first=True) @nomination_group.command( name="forcenominate", @@ -273,39 +231,21 @@ class TalentPool(Cog, name="Talentpool"): await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:") return - if not await self.refresh_cache(): - await ctx.send(f":x: Failed to update the cache; can't add {user.mention}.") - return - if len(reason) > REASON_MAX_CHARS: await ctx.send(f":x: The reason's length must not exceed {REASON_MAX_CHARS} characters.") return - # Manual request with `raise_for_status` as False because we want the actual response - session = self.bot.api_client.session - url = self.bot.api_client._url_for('bot/nominations') - kwargs = { - 'json': { - 'actor': ctx.author.id, - 'reason': reason, - 'user': user.id - }, - 'raise_for_status': False, - } - async with session.post(url, **kwargs) as resp: - response_data = await resp.json() - - if resp.status == 400: - if response_data.get('user', False): + try: + await self.api.post_nomination(user.id, ctx.author.id, reason) + except ResponseCodeError as e: + match (e.status, e.response_json): + case (400, {"user": _}): await ctx.send(f":x: {user.mention} can't be found in the database tables.") - elif response_data.get('actor', False): + return + case (400, {"actor": _}): await ctx.send(f":x: You have already nominated {user.mention}.") - - return - else: - resp.raise_for_status() - - self.cache[user.id] = response_data + return + raise await ctx.send(f"✅ The nomination for {user.mention} has been added to the talent pool.") @@ -313,13 +253,8 @@ class TalentPool(Cog, name="Talentpool"): @has_any_role(*MODERATION_ROLES) async def history_command(self, ctx: Context, user: MemberOrUser) -> None: """Shows the specified user's nomination history.""" - result = await self.bot.api_client.get( - 'bot/nominations', - params={ - 'user__id': str(user.id), - 'ordering': "-active,-inserted_at" - } - ) + result = await self.api.get_nominations(user.id, ordering="-active,-inserted_at") + if not result: await ctx.send(f":warning: {user.mention} has never been nominated.") return @@ -417,41 +352,32 @@ class TalentPool(Cog, name="Talentpool"): if len(reason) > REASON_MAX_CHARS: await ctx.send(f":x: The reason's length must not exceed {REASON_MAX_CHARS} characters.") return + if isinstance(target, int): nomination_id = target else: - if nomination := self.cache.get(target.id): - nomination_id = nomination["id"] + active_nominations = await self.api.get_nominations(user_id=target.id, active=True) + if active_nominations: + nomination_id = active_nominations[0].id else: await ctx.send(f":x: {target.mention} doesn't have an active nomination.") return + log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}") + try: - nomination = await self.bot.api_client.get(f"bot/nominations/{nomination_id}") + nomination = await self.api.edit_nomination_entry(nomination_id, actor_id=actor.id, reason=reason) except ResponseCodeError as e: - if e.response.status == 404: - log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") - await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`.") - return - else: - raise + match (e.status, e.response_json): + case (400, {"actor": _}): + await ctx.send(f":x: {actor.mention} doesn't have an entry in this nomination.") + return + case (404, _): + await ctx.send(f":x: Can't find a nomination with id `{target}`.") + return + raise - if not nomination["active"]: - await ctx.send(f":x: <@{nomination['user']}> doesn't have an active nomination.") - return - - if not any(entry["actor"] == actor.id for entry in nomination["entries"]): - await ctx.send(f":x: {actor.mention} doesn't have an entry in this nomination.") - return - - log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}") - - await self.bot.api_client.patch( - f"bot/nominations/{nomination_id}", - json={"actor": actor.id, "reason": reason} - ) - await self.refresh_cache() # Update cache - await ctx.send(f":white_check_mark: Updated the nomination reason for <@{nomination['user']}>.") + await ctx.send(f":white_check_mark: Updated the nomination reason for <@{nomination.user_id}>.") @nomination_edit_group.command(name='end_reason') @has_any_role(*MODERATION_ROLES) @@ -461,44 +387,31 @@ class TalentPool(Cog, name="Talentpool"): await ctx.send(f":x: The reason's length must not exceed {REASON_MAX_CHARS} characters.") return + log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}") try: - nomination = await self.bot.api_client.get(f"bot/nominations/{nomination_id}") + nomination = await self.api.edit_nomination(nomination_id, end_reason=reason) except ResponseCodeError as e: - if e.response.status == 404: - log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") - await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`.") - return - else: - raise - - if nomination["active"]: - await ctx.send( - f":x: Can't edit the nomination end reason for <@{nomination['user']}> because it's still active." - ) - return - - log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}") + match (e.status, e.response_json): + case (400, {"end_reason": _}): + await ctx.send(f":x: Can't edit nomination with id `{nomination_id}` because it's still active.") + return + case (404, _): + await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`.") + return + raise - await self.bot.api_client.patch( - f"bot/nominations/{nomination_id}", - json={"end_reason": reason} - ) - await self.refresh_cache() # Update cache. - await ctx.send(f":white_check_mark: Updated the nomination end reason for <@{nomination['user']}>.") - - @nomination_group.command(aliases=('mr',)) - @has_any_role(*MODERATION_ROLES) - async def mark_reviewed(self, ctx: Context, user_id: int) -> None: - """Mark a user's nomination as reviewed and cancel the review task.""" - if not await self.reviewer.mark_reviewed(ctx, user_id): - return - await ctx.send(f"{Emojis.check_mark} The user with ID `{user_id}` was marked as reviewed.") + await ctx.send(f":white_check_mark: Updated the nomination end reason for <@{nomination.user_id}>.") @nomination_group.command(aliases=('gr',)) @has_any_role(*MODERATION_ROLES) async def get_review(self, ctx: Context, user_id: int) -> None: """Get the user's review as a markdown file.""" - review, _, _ = await self.reviewer.make_review(user_id) + nominations = await self.api.get_nominations(user_id, active=True) + if not nominations: + await ctx.send(f":x: There doesn't appear to be an active nomination for {user_id}") + return + + review, _, _ = await self.reviewer.make_review(nominations[0]) file = discord.File(StringIO(review), f"{user_id}_review.md") await ctx.send(file=file) @@ -506,10 +419,17 @@ class TalentPool(Cog, name="Talentpool"): @has_any_role(*MODERATION_ROLES) async def post_review(self, ctx: Context, user_id: int) -> None: """Post the automatic review for the user ahead of time.""" - if not await self.reviewer.mark_reviewed(ctx, user_id): + nominations = await self.api.get_nominations(user_id, active=True) + if not nominations: + await ctx.send(f":x: There doesn't appear to be an active nomination for {user_id}") + return + + nomination = nominations[0] + if nomination.reviewed: + await ctx.send(":x: This nomination was already reviewed, but here's a cookie :cookie:") return - await self.reviewer.post_review(user_id, update_database=False) + await self.reviewer.post_review(nomination) await ctx.message.add_reaction(Emojis.check_mark) @Cog.listener() @@ -542,73 +462,72 @@ class TalentPool(Cog, name="Talentpool"): async def end_nomination(self, user_id: int, reason: str) -> bool: """End the active nomination of a user with the given reason and return True on success.""" - active_nomination = await self.bot.api_client.get( - 'bot/nominations', - params=ChainMap( - {"user__id": str(user_id)}, - self.api_default_params, - ) - ) + active_nominations = await self.api.get_nominations(user_id, active=True) - if not active_nomination: - log.debug(f"No active nominate exists for {user_id=}") + if not active_nominations: + log.debug(f"No active nomination exists for {user_id=}") return False log.info(f"Ending nomination: {user_id=} {reason=}") - nomination = active_nomination[0] - await self.bot.api_client.patch( - f"bot/nominations/{nomination['id']}", - json={'end_reason': reason, 'active': False} - ) - - self.cache.pop(user_id) + nomination = active_nominations[0] + await self.api.edit_nomination(nomination.id, end_reason=reason, active=False) return True - async def _nomination_to_string(self, nomination_object: dict) -> str: + async def _nomination_to_string(self, nomination: Nomination) -> str: """Creates a string representation of a nomination.""" guild = self.bot.get_guild(Guild.id) entries = [] - for site_entry in nomination_object["entries"]: - actor_id = site_entry["actor"] - actor = await get_or_fetch_member(guild, actor_id) + for entry in nomination.entries: + actor = await get_or_fetch_member(guild, entry.actor_id) - reason = site_entry["reason"] or "*None*" - created = time.discord_timestamp(site_entry["inserted_at"]) + reason = entry.reason or "*None*" + created = time.discord_timestamp(entry.inserted_at) entries.append( - f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" + f"Actor: {actor.mention if actor else entry.actor_id}\nCreated: {created}\nReason: {reason}" ) entries_string = "\n\n".join(entries) - active = nomination_object["active"] + start_date = time.discord_timestamp(nomination.inserted_at) + + thread_jump_url = "*Not created*" - start_date = time.discord_timestamp(nomination_object["inserted_at"]) - if active: + 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""" =============== Status: **Active** Date: {start_date} - Nomination ID: `{nomination_object["id"]}` + Nomination ID: `{nomination.id}` + Nomination vote thread: {thread_jump_url} {entries_string} =============== """ ) else: - end_date = time.discord_timestamp(nomination_object["ended_at"]) + end_date = time.discord_timestamp(nomination.ended_at) lines = textwrap.dedent( f""" =============== Status: Inactive Date: {start_date} - Nomination ID: `{nomination_object["id"]}` + Nomination ID: `{nomination.id}` + Nomination vote thread: {thread_jump_url} {entries_string} End date: {end_date} - Unnomination reason: {nomination_object["end_reason"]} + Unnomination reason: {nomination.end_reason} =============== """ ) @@ -619,4 +538,4 @@ class TalentPool(Cog, name="Talentpool"): """Cancels the autoreview loop on cog unload.""" # Only cancel the loop task when the autoreview code is not running async with self.autoreview_lock: - self.autoreview_loop_lock.cancel() + self.autoreview_loop.cancel() diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index e3ac1086d..f41e08fe1 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -8,20 +8,20 @@ from collections import Counter from datetime import datetime, timedelta, timezone from typing import List, Optional, Union -from botcore.site_api import ResponseCodeError -from dateutil.parser import isoparse +import discord from discord import Embed, Emoji, Member, Message, NotFound, PartialMessage, TextChannel -from discord.ext.commands import Context +from pydis_core.site_api import ResponseCodeError from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Guild, Roles +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 if typing.TYPE_CHECKING: - from bot.exts.recruitment.talentpool._cog import TalentPool from bot.exts.utils.thread_bumper import ThreadBumper log = get_logger(__name__) @@ -52,9 +52,9 @@ NOMINATION_MESSAGE_REGEX = re.compile( class Reviewer: """Manages, formats, and publishes reviews of helper nominees.""" - def __init__(self, bot: Bot, pool: 'TalentPool'): + def __init__(self, bot: Bot, nomination_api: NominationAPI): self.bot = bot - self._pool = pool + self.api = nomination_api async def maybe_review_user(self) -> bool: """ @@ -65,11 +65,11 @@ class Reviewer: if not await self.is_ready_for_review(): return False - user = await self.get_user_for_review() - if not user: + nomination = await self.get_nomination_to_review() + if not nomination: return False - await self.post_review(user, True) + await self.post_review(nomination) return True async def is_ready_for_review(self) -> bool: @@ -107,9 +107,9 @@ class Reviewer: return True - async def get_user_for_review(self) -> Optional[int]: + async def get_nomination_to_review(self) -> Optional[Nomination]: """ - Returns the user ID of the next user to review, or None if there are no users ready. + Returns the Nomination of the next user to review, or None if there are no users ready. Users will only be selected for review if: - They have not already been reviewed. @@ -121,45 +121,46 @@ class Reviewer: """ now = datetime.now(timezone.utc) - possible = [] - for user_id, user_data in self._pool.cache.items(): - time_since_nomination = now - isoparse(user_data["inserted_at"]) + possible_nominations: list[Nomination] = [] + nominations = await self.api.get_nominations(active=True) + for nomination in nominations: + time_since_nomination = now - nomination.inserted_at if ( - not user_data["reviewed"] + not nomination.reviewed and time_since_nomination > MIN_NOMINATION_TIME ): - possible.append((user_id, user_data)) + possible_nominations.append(nomination) - if not possible: + if not possible_nominations: log.debug("No users ready to review.") return None - oldest_date = min(isoparse(x[1]["inserted_at"]) for x in possible) - max_entries = max(len(x[1]["entries"]) for x in possible) + oldest_date = min(nomination.inserted_at for nomination in possible_nominations) + max_entries = max(len(nomination.entries) for nomination in possible_nominations) - def sort_key(nomination: dict) -> float: - return self.score_nomination(nomination[1], oldest_date, now, max_entries) + def sort_key(nomination: Nomination) -> float: + return self.score_nomination(nomination, oldest_date, now, max_entries) - return max(possible, key=sort_key)[0] + return max(possible_nominations, key=sort_key) @staticmethod - def score_nomination(nomination: dict, oldest_date: datetime, now: datetime, max_entries: int) -> float: + def score_nomination(nomination: Nomination, oldest_date: datetime, now: datetime, max_entries: int) -> float: """ Scores a nomination based on age and number of nomination entries. The higher the score, the higher the priority for being put up for review should be. """ - num_entries = len(nomination["entries"]) + num_entries = len(nomination.entries) entries_score = num_entries / max_entries - nomination_date = isoparse(nomination["inserted_at"]) + nomination_date = nomination.inserted_at age_score = (nomination_date - now) / (oldest_date - now) return entries_score * REVIEW_SCORE_WEIGHT + age_score - async def post_review(self, user_id: int, update_database: bool) -> None: + async def post_review(self, nomination: Nomination) -> None: """Format the review of a user and post it to the nomination voting channel.""" - review, reviewed_emoji, nominee = await self.make_review(user_id) + review, reviewed_emoji, nominee = await self.make_review(nomination) if not nominee: return @@ -181,41 +182,31 @@ class Reviewer: ) message = await thread.send(f"<@&{Roles.mod_team}> <@&{Roles.admins}>") - if update_database: - nomination = self._pool.cache.get(user_id) - await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"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: context = await self.bot.get_context(message) await bump_cog.add_thread_to_bump_list(context, thread) - async def make_review(self, user_id: int) -> typing.Tuple[str, Optional[Emoji], Optional[Member]]: + async def make_review(self, nomination: Nomination) -> typing.Tuple[str, Optional[Emoji], Optional[Member]]: """Format a generic review of a user and return it with the reviewed emoji and the user themselves.""" - log.trace(f"Formatting the review of {user_id}") - - # Since `cache` is a defaultdict, we should take care - # not to accidentally insert the IDs of users that have no - # active nominated by using the `cache.get(user_id)` - # instead of `cache[user_id]`. - nomination = self._pool.cache.get(user_id) - if not nomination: - log.trace(f"There doesn't appear to be an active nomination for {user_id}") - return f"There doesn't appear to be an active nomination for {user_id}", None, None + log.trace(f"Formatting the review of {nomination.user_id}") guild = self.bot.get_guild(Guild.id) - nominee = await get_or_fetch_member(guild, user_id) + nominee = await get_or_fetch_member(guild, nomination.user_id) if not nominee: return ( - f"I tried to review the user with ID `{user_id}`, but they don't appear to be on the server :pensive:" + f"I tried to review the user with ID `{nomination.user_id}`," + " but they don't appear to be on the server :pensive:" ), None, None opening = f"{nominee.mention} ({nominee}) for Helper!" current_nominations = "\n\n".join( - f"**<@{entry['actor']}>:** {entry['reason'] or '*no reason given*'}" - for entry in nomination['entries'][::-1] + f"**<@{entry.actor_id}>:** {entry.reason or '*no reason given*'}" + for entry in nomination.entries[::-1] ) current_nominations = f"**Nominated by:**\n{current_nominations}" @@ -434,29 +425,41 @@ class Reviewer: The number of previous nominations and unnominations are shown, as well as the reason the last one ended. """ log.trace(f"Fetching the nomination history data for {member.id}'s review") - history = await self.bot.api_client.get( - "bot/nominations", - params={ - "user__id": str(member.id), - "active": "false", - "ordering": "-inserted_at" - } - ) + history = await self.api.get_nominations(user_id=member.id, active=False) log.trace(f"{len(history)} previous nominations found for {member.id}, formatting review.") if not history: return - num_entries = sum(len(nomination["entries"]) for nomination in history) + num_entries = sum(len(nomination.entries) for nomination in history) nomination_times = f"{num_entries} times" if num_entries > 1 else "once" rejection_times = f"{len(history)} times" if len(history) > 1 else "once" - end_time = time.format_relative(history[0]['ended_at']) + 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"\nThe last one ended {end_time} with the reason: {history[0]['end_reason']}" + 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}" ) return review @@ -485,25 +488,3 @@ class Reviewer: results.append(await channel.send(message)) return results - - async def mark_reviewed(self, ctx: Context, user_id: int) -> bool: - """ - Mark an active nomination as reviewed, updating the database and canceling the review task. - - Returns True if the user was successfully marked as reviewed, False otherwise. - """ - log.trace(f"Updating user {user_id} as reviewed") - await self._pool.refresh_cache() - if user_id not in self._pool.cache: - log.trace(f"Can't find a nominated user with id {user_id}") - await ctx.send(f":x: Can't find a currently nominated user with id `{user_id}`") - return False - - nomination = self._pool.cache.get(user_id) - if nomination["reviewed"]: - await ctx.send(":x: This nomination was already reviewed, but here's a cookie :cookie:") - return False - - await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"reviewed": True}) - - return True diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 803e2ea52..368f08510 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -5,15 +5,15 @@ from datetime import datetime, timezone 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 ( - 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 @@ -218,7 +218,7 @@ class Reminders(Cog): """ 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: @@ -280,7 +280,8 @@ class Reminders(Cog): # 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 diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 8e961b67c..8a2e68b28 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -7,14 +7,15 @@ from signal import Signals 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 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 @@ -125,7 +126,8 @@ class PythonVersionSwitcherButton(ui.Button): version_to_switch_to: Literal["3.10", "3.11"], snekbox_cog: "Snekbox", ctx: Context, - code: str + code: str, + args: Optional[list[str]] = None ) -> None: self.version_to_switch_to = version_to_switch_to super().__init__(label=f"Run in {self.version_to_switch_to}", style=enums.ButtonStyle.primary) @@ -134,6 +136,7 @@ class PythonVersionSwitcherButton(ui.Button): self.ctx = ctx self.job_name = job_name self.code = code + self.args = args async def callback(self, interaction: Interaction) -> None: """ @@ -150,7 +153,9 @@ class PythonVersionSwitcherButton(ui.Button): # The log arg on send_job will stop the actual job from running. await interaction.message.delete() - await self.snekbox_cog.run_job(self.job_name, self.ctx, self.version_to_switch_to, self.code) + await self.snekbox_cog.run_job( + self.job_name, self.ctx, self.version_to_switch_to, self.code, args=self.args + ) class Snekbox(Cog): @@ -165,7 +170,8 @@ class Snekbox(Cog): job_name: str, current_python_version: Literal["3.10", "3.11"], ctx: Context, - code: str + code: str, + args: Optional[list[str]] = None ) -> None: """Return a view that allows the user to change what version of Python their code is run on.""" if current_python_version == "3.10": @@ -177,7 +183,7 @@ class Snekbox(Cog): allowed_users=(ctx.author.id,), allowed_roles=MODERATION_ROLES, ) - view.add_item(PythonVersionSwitcherButton(job_name, alt_python_version, self, ctx, code)) + view.add_item(PythonVersionSwitcherButton(job_name, alt_python_version, self, ctx, code, args)) view.add_item(interactions.DeleteMessageButton()) return view @@ -357,7 +363,7 @@ class Snekbox(Cog): response = await ctx.send("Attempt to circumvent filter detected. Moderator team has been alerted.") else: allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author]) - view = self.build_python_version_switcher_view(job_name, python_version, ctx, code) + view = self.build_python_version_switcher_view(job_name, python_version, ctx, code, args) response = await ctx.send(msg, allowed_mentions=allowed_mentions, view=view) view.message = response @@ -396,15 +402,16 @@ class Snekbox(Cog): 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) @@ -451,7 +458,7 @@ class Snekbox(Cog): 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") diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index a2f208484..0384119f5 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/pagination.py b/bot/pagination.py index 92fa781ee..679108933 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -190,8 +190,8 @@ class LinePaginator(Paginator): @classmethod async def paginate( cls, - lines: t.List[str], - ctx: Context, + lines: list[str], + ctx: Context | discord.Interaction, embed: discord.Embed, prefix: str = "", suffix: str = "", @@ -229,7 +229,10 @@ class LinePaginator(Paginator): current_page = 0 if not restrict_to_user: - restrict_to_user = ctx.author + if isinstance(ctx, discord.Interaction): + restrict_to_user = ctx.user + else: + restrict_to_user = ctx.author if not lines: if exception_on_empty_embed: @@ -264,6 +267,9 @@ class LinePaginator(Paginator): log.trace(f"Setting embed url to '{url}'") log.debug("There's less than two pages, so we won't paginate - sending single page on its own") + + if isinstance(ctx, discord.Interaction): + return await ctx.response.send_message(embed=embed) return await ctx.send(embed=embed, reference=reference) else: if footer_text: @@ -277,7 +283,12 @@ class LinePaginator(Paginator): log.trace(f"Setting embed url to '{url}'") log.debug("Sending first page to channel...") - message = await ctx.send(embed=embed, reference=reference) + + if isinstance(ctx, discord.Interaction): + await ctx.response.send_message(embed=embed) + message = await ctx.original_response() + else: + message = await ctx.send(embed=embed, reference=reference) log.debug("Adding emoji reactions to message...") @@ -295,7 +306,10 @@ class LinePaginator(Paginator): while True: try: - reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=check) + if isinstance(ctx, discord.Interaction): + reaction, user = await ctx.client.wait_for("reaction_add", timeout=timeout, check=check) + else: + reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=check) log.trace(f"Got reaction: {reaction}") except asyncio.TimeoutError: log.debug("Timed out waiting for a reaction") @@ -304,61 +318,35 @@ class LinePaginator(Paginator): 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: - 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)}") - - 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 == 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)}") + elif reaction.emoji in PAGINATION_EMOJI: + total_pages = len(paginator.pages) + 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 + 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] @@ -367,8 +355,20 @@ class LinePaginator(Paginator): 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: + # Suppress if trying to act on an archived thread. + if e.code != 50083: + raise e diff --git a/bot/resources/tags/args-kwargs.md b/bot/resources/tags/args-kwargs.md index b440a2346..67d93fc40 100644 --- a/bot/resources/tags/args-kwargs.md +++ b/bot/resources/tags/args-kwargs.md @@ -1,5 +1,7 @@ -`*args` and `**kwargs` - +--- +embed: + title: "The `*args` and `**kwargs` parameters" +--- These special parameters allow functions to take arbitrary amounts of positional and keyword arguments. The names `args` and `kwargs` are purely convention, and could be named any other valid variable name. The special functionality comes from the single and double asterisks (`*`). If both are used in a function signature, `*args` **must** appear before `**kwargs`. **Single asterisk** @@ -9,9 +11,9 @@ These special parameters allow functions to take arbitrary amounts of positional `**kwargs` will ingest an arbitrary amount of **keyword arguments**, and store it in a dictionary. There can be **no** additional parameters **after** `**kwargs` in the parameter list. **Use cases** -• **Decorators** (see `!tags decorators`) +• **Decorators** (see `/tag decorators`) • **Inheritance** (overriding methods) • **Future proofing** (in the case of the first two bullet points, if the parameters change, your code won't break) • **Flexibility** (writing functions that behave like `dict()` or `print()`) -*See* `!tags positional-keyword` *for information about positional and keyword arguments* +*See* `/tag positional-keyword` *for information about positional and keyword arguments* diff --git a/bot/resources/tags/async-await.md b/bot/resources/tags/async-await.md index 01ab28fe3..a3d8bde84 100644 --- a/bot/resources/tags/async-await.md +++ b/bot/resources/tags/async-await.md @@ -1,5 +1,7 @@ -**Concurrency in Python** - +--- +embed: + title: "Concurrency in Python" +--- Python provides the ability to run multiple tasks and coroutines simultaneously with the use of the `asyncio` library, which is included in the Python standard library. This works by running these coroutines in an event loop, where the context of the running coroutine switches periodically to allow all other coroutines to run, thus giving the appearance of running at the same time. This is different to using threads or processes in that all code runs in the main process and thread, although it is possible to run coroutines in other threads. diff --git a/bot/resources/tags/blocking.md b/bot/resources/tags/blocking.md index 5554d7eba..1999e2421 100644 --- a/bot/resources/tags/blocking.md +++ b/bot/resources/tags/blocking.md @@ -1,4 +1,7 @@ -**Why do we need asynchronous programming?** +--- +embed: + title: "Asynchronous programming" +--- Imagine that you're coding a Discord bot and every time somebody uses a command, you need to get some information from a database. But there's a catch: the database servers are acting up today and take a whole 10 seconds to respond. If you do **not** use asynchronous methods, your whole bot will stop running until it gets a response from the database. How do you fix this? Asynchronous programming. **What is asynchronous programming?** diff --git a/bot/resources/tags/botvar.md b/bot/resources/tags/botvar.md index 3db6ae7ac..e4ea8b87d 100644 --- a/bot/resources/tags/botvar.md +++ b/bot/resources/tags/botvar.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Bot variables" +--- Python allows you to set custom attributes to most objects, like your bot! By storing things as attributes of the bot object, you can access them anywhere you access your bot. In the discord.py library, these custom attributes are commonly known as "bot variables" and can be a lifesaver if your bot is divided into many different files. An example on how to use custom attributes on your bot is shown below: ```py diff --git a/bot/resources/tags/class.md b/bot/resources/tags/class.md index 4f73fc974..5fbe43c18 100644 --- a/bot/resources/tags/class.md +++ b/bot/resources/tags/class.md @@ -1,5 +1,7 @@ -**Classes** - +--- +embed: + title: "Classes" +--- Classes are used to create objects that have specific behavior. Every object in python has a class, including `list`s, `dict`ionaries and even numbers. Using a class to group code and data like this is the foundation of Object Oriented Programming. Classes allow you to expose a simple, consistent interface while hiding the more complicated details. This simplifies the rest of your program and makes it easier to separately maintain and debug each component. diff --git a/bot/resources/tags/classmethod.md b/bot/resources/tags/classmethod.md index a4e803093..2452de8e8 100644 --- a/bot/resources/tags/classmethod.md +++ b/bot/resources/tags/classmethod.md @@ -1,3 +1,7 @@ +--- +embed: + title: "The `@classmethod` decorator" +--- Although most methods are tied to an _object instance_, it can sometimes be useful to create a method that does something with _the class itself_. To achieve this in Python, you can use the `@classmethod` decorator. This is often used to provide alternative constructors for a class. For example, you may be writing a class that takes some magic token (like an API key) as a constructor argument, but you sometimes read this token from a configuration file. You could make use of a `@classmethod` to create an alternate constructor for when you want to read from the configuration file. diff --git a/bot/resources/tags/codeblock.md b/bot/resources/tags/codeblock.md index ac64656e5..c2b77637e 100644 --- a/bot/resources/tags/codeblock.md +++ b/bot/resources/tags/codeblock.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Formatting code on discord" +--- Here's how to format Python code on Discord: \`\`\`py @@ -5,3 +9,5 @@ print('Hello world!') \`\`\` **These are backticks, not quotes.** Check [this](https://superuser.com/questions/254076/how-do-i-type-the-tick-and-backtick-characters-on-windows/254077#254077) out if you can't find the backtick key. + +**For long code samples,** you can use our [pastebin](https://paste.pythondiscord.com/). diff --git a/bot/resources/tags/comparison.md b/bot/resources/tags/comparison.md index 12844bd2f..5db17cc28 100644 --- a/bot/resources/tags/comparison.md +++ b/bot/resources/tags/comparison.md @@ -1,5 +1,7 @@ -**Assignment vs. Comparison** - +--- +embed: + title: "Assignment vs comparison`" +--- The assignment operator (`=`) is used to assign variables. ```python x = 5 diff --git a/bot/resources/tags/contribute.md b/bot/resources/tags/contribute.md index 50c5cd11f..57eae0a7e 100644 --- a/bot/resources/tags/contribute.md +++ b/bot/resources/tags/contribute.md @@ -1,4 +1,7 @@ -**Contribute to Python Discord's Open Source Projects** +--- +embed: + title: "Contribute to Python Discord's open source projects" +--- Looking to contribute to Open Source Projects for the first time? Want to add a feature or fix a bug on the bots on this server? We have on-going projects that people can contribute to, even if you've never contributed to open source before! **Projects to Contribute to** diff --git a/bot/resources/tags/customchecks.md b/bot/resources/tags/customchecks.md index 23ff7a66f..46e4c8f3e 100644 --- a/bot/resources/tags/customchecks.md +++ b/bot/resources/tags/customchecks.md @@ -1,5 +1,7 @@ -**Custom Command Checks in discord.py** - +--- +embed: + title: "Custom command checks in discord.py" +--- Often you may find the need to use checks that don't exist by default in discord.py. Fortunately, discord.py provides `discord.ext.commands.check` which allows you to create you own checks like this: ```py from discord.ext.commands import check, Context diff --git a/bot/resources/tags/customcooldown.md b/bot/resources/tags/customcooldown.md index ac7e70aee..78d1d253e 100644 --- a/bot/resources/tags/customcooldown.md +++ b/bot/resources/tags/customcooldown.md @@ -1,5 +1,7 @@ -**Cooldowns in discord.py** - +--- +embed: + title: "Cooldowns in discord.py" +--- Cooldowns can be used in discord.py to rate-limit. In this example, we're using it in an on_message. ```python @@ -17,4 +19,4 @@ async def on_message(message): await message.channel.send("Not ratelimited!") ``` -`from_cooldown` takes the amount of `update_rate_limit()`s needed to trigger the cooldown, the time in which the cooldown is triggered, and a [`BucketType`](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.discord.ext.commands.BucketType). +`from_cooldown` takes the amount of `update_rate_limit()`s needed to trigger the cooldown, the time in which the cooldown is triggered, and a [`BucketType`](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.BucketType). diff --git a/bot/resources/tags/customhelp.md b/bot/resources/tags/customhelp.md index 6f0b17642..5dc1a0541 100644 --- a/bot/resources/tags/customhelp.md +++ b/bot/resources/tags/customhelp.md @@ -1,3 +1,5 @@ -**Custom help commands in discord.py** - +--- +embed: + title: "Custom help commands in discord.py" +--- To learn more about how to create custom help commands in discord.py by subclassing the help command, please see [this tutorial](https://gist.github.com/InterStella0/b78488fb28cadf279dfd3164b9f0cf96#embed-minimalhelpcommand) by Stella#2000 diff --git a/bot/resources/tags/decorators.md b/bot/resources/tags/decorators.md index 39c943f0a..65d221d6e 100644 --- a/bot/resources/tags/decorators.md +++ b/bot/resources/tags/decorators.md @@ -1,5 +1,7 @@ -**Decorators** - +--- +embed: + title: "Decorators" +--- A decorator is a function that modifies another function. Consider the following example of a timer decorator: diff --git a/bot/resources/tags/defaultdict.md b/bot/resources/tags/defaultdict.md index b6c3175fc..82555b7a1 100644 --- a/bot/resources/tags/defaultdict.md +++ b/bot/resources/tags/defaultdict.md @@ -1,5 +1,7 @@ -**[`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict)** - +--- +embed: + title: "The `collections.defaultdict` class" +--- The Python `defaultdict` type behaves almost exactly like a regular Python dictionary, but if you try to access or modify a missing key, the `defaultdict` will automatically insert the key and generate a default value for it. While instantiating a `defaultdict`, we pass in a function that tells it how to create a default value for missing keys. @@ -19,3 +21,4 @@ In this example, we've used the `int` class which returns 0 when called like a f >>> my_dict defaultdict(<class 'int'>, {'foo': 0, 'bar': 5}) ``` +Check out the [`docs`](https://docs.python.org/3/library/collections.html#collections.defaultdict) to learn even more! diff --git a/bot/resources/tags/dict-get.md b/bot/resources/tags/dict-get.md index e02df03ab..57d5349e1 100644 --- a/bot/resources/tags/dict-get.md +++ b/bot/resources/tags/dict-get.md @@ -1,7 +1,9 @@ +--- +embed: + title: "The `dict.get` method" +--- Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary. Python gives you some neat ways to handle them. -**The `dict.get` method** - The [`dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return the value for the key if it exists, and None (or a default value that you specify) if the key doesn't exist. Hence it will _never raise_ a KeyError. ```py >>> my_dict = {"foo": 1, "bar": 2} diff --git a/bot/resources/tags/dictcomps.md b/bot/resources/tags/dictcomps.md index 75fbe0f8a..75f0f7b3a 100644 --- a/bot/resources/tags/dictcomps.md +++ b/bot/resources/tags/dictcomps.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Dictionary comprehensions" +--- Dictionary comprehensions (*dict comps*) provide a convenient way to make dictionaries, just like list comps: ```py >>> {word.lower(): len(word) for word in ('I', 'love', 'Python')} diff --git a/bot/resources/tags/docstring.md b/bot/resources/tags/docstring.md index 6e9d9aa09..ee07ff5f7 100644 --- a/bot/resources/tags/docstring.md +++ b/bot/resources/tags/docstring.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Docstrings" +--- A [`docstring`](https://docs.python.org/3/glossary.html#term-docstring) is a string - always using triple quotes - that's placed at the top of files, classes and functions. A docstring should contain a clear explanation of what it's describing. You can also include descriptions of the subject's parameter(s) and what it returns, as shown below: ```py def greet(name: str, age: int) -> str: diff --git a/bot/resources/tags/dotenv.md b/bot/resources/tags/dotenv.md index acb9a216e..14fff3458 100644 --- a/bot/resources/tags/dotenv.md +++ b/bot/resources/tags/dotenv.md @@ -1,5 +1,7 @@ -**Using .env files in Python** - +--- +embed: + title: "Using .env files in Python" +--- `.env` (dotenv) files are a type of file commonly used for storing application secrets and variables, for example API tokens and URLs, although they may also be used for storing other configurable values. While they are commonly used for storing secrets, at a high level their purpose is to load environment variables into a program. Dotenv files are especially suited for storing secrets as they are a key-value store in a file, which can be easily loaded in most programming languages and ignored by version control systems like Git with a single entry in a `.gitignore` file. diff --git a/bot/resources/tags/dunder-methods.md b/bot/resources/tags/dunder-methods.md index be2b97b7b..e3f753802 100644 --- a/bot/resources/tags/dunder-methods.md +++ b/bot/resources/tags/dunder-methods.md @@ -1,5 +1,7 @@ -**Dunder methods** - +--- +embed: + title: "Dunder methods" +--- Double-underscore methods, or "dunder" methods, are special methods defined in a class that are invoked implicitly. Like the name suggests, they are prefixed and suffixed with dunders. You've probably already seen some, such as the `__init__` dunder method, also known as the "constructor" of a class, which is implicitly invoked when you instantiate an instance of a class. When you create a new class, there will be default dunder methods inherited from the `object` class. However, we can override them by redefining these methods within the new class. For example, the default `__init__` method from `object` doesn't take any arguments, so we almost always override that to fit our needs. diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index 935544bb7..889297671 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Empty JSON error" +--- When using JSON, you might run into the following error: ``` JSONDecodeError: Expecting value: line 1 column 1 (char 0) diff --git a/bot/resources/tags/enumerate.md b/bot/resources/tags/enumerate.md index da9c86a36..c84ad323a 100644 --- a/bot/resources/tags/enumerate.md +++ b/bot/resources/tags/enumerate.md @@ -1,3 +1,7 @@ +--- +embed: + title: "The `enumerate` function" +--- Ever find yourself in need of the current iteration number of your `for` loop? You should use **enumerate**! Using `enumerate`, you can turn code that looks like this: ```py index = 0 diff --git a/bot/resources/tags/environments.md b/bot/resources/tags/environments.md index 7bc69bde4..d6ed06448 100644 --- a/bot/resources/tags/environments.md +++ b/bot/resources/tags/environments.md @@ -1,12 +1,15 @@ -**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/except.md b/bot/resources/tags/except.md index 8f0abf156..934efdd76 100644 --- a/bot/resources/tags/except.md +++ b/bot/resources/tags/except.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Error handling" +--- A key part of the Python philosophy is to ask for forgiveness, not permission. This means that it's okay to write code that may produce an error, as long as you specify how that error should be handled. Code written this way is readable and resilient. ```py try: diff --git a/bot/resources/tags/exit().md b/bot/resources/tags/exit().md index 27da9f866..57cfaaaab 100644 --- a/bot/resources/tags/exit().md +++ b/bot/resources/tags/exit().md @@ -1,5 +1,7 @@ -**Exiting Programmatically** - +--- +embed: + title: "Exiting programmatically" +--- If you want to exit your code programmatically, you might think to use the functions `exit()` or `quit()`, however this is bad practice. These functions are constants added by the [`site`](https://docs.python.org/3/library/site.html#module-site) module as a convenient method for exiting the interactive interpreter shell, and should not be used in programs. You should use either [`SystemExit`](https://docs.python.org/3/library/exceptions.html#SystemExit) or [`sys.exit()`](https://docs.python.org/3/library/sys.html#sys.exit) instead. diff --git a/bot/resources/tags/f-strings.md b/bot/resources/tags/f-strings.md index ab6ec75c9..92d7c62e2 100644 --- a/bot/resources/tags/f-strings.md +++ b/bot/resources/tags/f-strings.md @@ -1,5 +1,7 @@ --- aliases: ["fstrings", "fstring", "f-string"] +embed: + title: "Format-strings" --- Creating a Python string with your variables using the `+` operator can be difficult to write and read. F-strings (*format-strings*) make it easy to insert values into a string. If you put an `f` in front of the first quote, you can then put Python expressions between curly braces in the string. diff --git a/bot/resources/tags/faq.md b/bot/resources/tags/faq.md index e1c57b3a0..6299240fc 100644 --- a/bot/resources/tags/faq.md +++ b/bot/resources/tags/faq.md @@ -2,5 +2,4 @@ embed: title: "Frequently asked questions" --- - As the largest Python community on Discord, we get hundreds of questions every day. Many of these questions have been asked before. We've compiled a list of the most frequently asked questions along with their answers, which can be found on our [FAQ page](https://www.pythondiscord.com/pages/frequently-asked-questions/). diff --git a/bot/resources/tags/floats.md b/bot/resources/tags/floats.md index 03fcd7268..d9d537377 100644 --- a/bot/resources/tags/floats.md +++ b/bot/resources/tags/floats.md @@ -1,4 +1,7 @@ -**Floating Point Arithmetic** +--- +embed: + title: "Floating point arithmetic" +--- You may have noticed that when doing arithmetic with floats in Python you sometimes get strange results, like this: ```python >>> 0.1 + 0.2 diff --git a/bot/resources/tags/foo.md b/bot/resources/tags/foo.md index 98529bfc0..0a40eb991 100644 --- a/bot/resources/tags/foo.md +++ b/bot/resources/tags/foo.md @@ -1,5 +1,7 @@ -**Metasyntactic variables** - +--- +embed: + title: "Metasyntactic variables" +--- A specific word or set of words identified as a placeholder used in programming. They are used to name entities such as variables, functions, etc, whose exact identity is unimportant and serve only to demonstrate a concept, which is useful for teaching programming. Common examples include `foobar`, `foo`, `bar`, `baz`, and `qux`. diff --git a/bot/resources/tags/for-else.md b/bot/resources/tags/for-else.md index e102e4e75..a6e79add0 100644 --- a/bot/resources/tags/for-else.md +++ b/bot/resources/tags/for-else.md @@ -1,5 +1,7 @@ -**for-else** - +--- +embed: + title: "The for-else block" +--- In Python it's possible to attach an `else` clause to a for loop. The code under the `else` block will be run when the iterable is exhausted (there are no more items to iterate over). Code within the else block will **not** run if the loop is broken out using `break`. Here's an example of its usage: diff --git a/bot/resources/tags/functions-are-objects.md b/bot/resources/tags/functions-are-objects.md index 01af7a721..88f3ddeb4 100644 --- a/bot/resources/tags/functions-are-objects.md +++ b/bot/resources/tags/functions-are-objects.md @@ -1,5 +1,7 @@ -**Calling vs. Referencing functions** - +--- +embed: + title: "Calling vs. referencing functions" +--- When assigning a new name to a function, storing it in a container, or passing it as an argument, a common mistake made is to call the function. Instead of getting the actual function, you'll get its return value. In Python you can treat function names just like any other variable. Assume there was a function called `now` that returns the current time. If you did `x = now()`, the current time would be assigned to `x`, but if you did `x = now`, the function `now` itself would be assigned to `x`. `x` and `now` would both equally reference the function. diff --git a/bot/resources/tags/global.md b/bot/resources/tags/global.md index 64c316b62..c463121ab 100644 --- a/bot/resources/tags/global.md +++ b/bot/resources/tags/global.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Globals" +--- When adding functions or classes to a program, it can be tempting to reference inaccessible variables by declaring them as global. Doing this can result in code that is harder to read, debug and test. Instead of using globals, pass variables or objects as parameters and receive return values. Instead of writing diff --git a/bot/resources/tags/guilds.md b/bot/resources/tags/guilds.md index 571abb99b..565b01cef 100644 --- a/bot/resources/tags/guilds.md +++ b/bot/resources/tags/guilds.md @@ -1,3 +1,5 @@ -**Communities** - +--- +embed: + title: "Communities" +--- The [communities page](https://pythondiscord.com/pages/resources/communities/) on our website contains a number of communities we have partnered with as well as a [curated list](https://github.com/mhxion/awesome-discord-communities) of other communities relating to programming and technology. diff --git a/bot/resources/tags/identity.md b/bot/resources/tags/identity.md index fb2010759..aa8974a29 100644 --- a/bot/resources/tags/identity.md +++ b/bot/resources/tags/identity.md @@ -1,5 +1,7 @@ -**Identity vs. Equality** - +--- +embed: + title: "Identity vs. equality" +--- Should I be using `is` or `==`? To check if two objects are equal, use the equality operator (`==`). @@ -13,12 +15,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/if-name-main.md b/bot/resources/tags/if-name-main.md index 9d88bb897..c2a7eb289 100644 --- a/bot/resources/tags/if-name-main.md +++ b/bot/resources/tags/if-name-main.md @@ -1,5 +1,7 @@ -`if __name__ == '__main__'` - +--- +embed: + title: "`if __name__ == '__main__'`" +--- This is a statement that is only true if the module (your source code) it appears in is being run directly, as opposed to being imported into another module. When you run your module, the `__name__` special variable is automatically set to the string `'__main__'`. Conversely, when you import that same module into a different one, and run that, `__name__` is instead set to the filename of your module minus the `.py` extension. **Example** diff --git a/bot/resources/tags/indent.md b/bot/resources/tags/indent.md index 4c3cdd126..96ef53310 100644 --- a/bot/resources/tags/indent.md +++ b/bot/resources/tags/indent.md @@ -1,5 +1,7 @@ -**Indentation** - +--- +embed: + title: "Indentation" +--- Indentation is leading whitespace (spaces and tabs) at the beginning of a line of code. In the case of Python, they are used to determine the grouping of statements. Spaces should be preferred over tabs. To be clear, this is in reference to the character itself, not the keys on a keyboard. Your editor/IDE should be configured to insert spaces when the TAB key is pressed. The amount of spaces should be a multiple of 4, except optionally in the case of continuation lines. diff --git a/bot/resources/tags/inline.md b/bot/resources/tags/inline.md index 4ece74ef7..f50243460 100644 --- a/bot/resources/tags/inline.md +++ b/bot/resources/tags/inline.md @@ -1,7 +1,11 @@ -**Inline codeblocks** - +--- +embed: + title: "Inline codeblocks" +--- Inline codeblocks look `like this`. To create them you surround text with single backticks, so \`hello\` would become `hello`. 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/intents.md b/bot/resources/tags/intents.md index aa49d59ae..9cb17b0d9 100644 --- a/bot/resources/tags/intents.md +++ b/bot/resources/tags/intents.md @@ -1,19 +1,21 @@ -**Using intents in discord.py** +--- +embed: + title: "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, 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. -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. - -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* 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.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). +For more info about using intents, see [discord.py's related guide](https://discordpy.readthedocs.io/en/stable/intents.html), and for general information about them, see the [Discord developer documentation on intents](https://discord.com/developers/docs/topics/gateway#gateway-intents). diff --git a/bot/resources/tags/iterate-dict.md b/bot/resources/tags/iterate-dict.md index 78c067b20..40477444f 100644 --- a/bot/resources/tags/iterate-dict.md +++ b/bot/resources/tags/iterate-dict.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Iteration over dictionaries" +--- There are two common ways to iterate over a dictionary in Python. To iterate over the keys: ```py for key in my_dict: diff --git a/bot/resources/tags/kindling-projects.md b/bot/resources/tags/kindling-projects.md index 54ed8c961..00ab95513 100644 --- a/bot/resources/tags/kindling-projects.md +++ b/bot/resources/tags/kindling-projects.md @@ -1,3 +1,5 @@ -**Kindling Projects** - +--- +embed: + title: "Kindling Projects" +--- The [Kindling projects page](https://nedbatchelder.com/text/kindling.html) on Ned Batchelder's website contains a list of projects and ideas programmers can tackle to build their skills and knowledge. diff --git a/bot/resources/tags/listcomps.md b/bot/resources/tags/listcomps.md index ba00a4bf7..1b51883ee 100644 --- a/bot/resources/tags/listcomps.md +++ b/bot/resources/tags/listcomps.md @@ -1,3 +1,7 @@ +--- +embed: + title: "List comprehensions" +--- Do you ever find yourself writing something like this? ```py >>> squares = [] @@ -10,8 +14,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 ae41d589c..40e0a2380 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -1,4 +1,8 @@ -Thanks to discord.py, sending local files as embed images is simple. You have to create an instance of [`discord.File`](https://discordpy.readthedocs.io/en/latest/api.html#discord.File) class: +--- +embed: + title: "Sending images in embeds using discord.py" +--- +Thanks to discord.py, sending local files as embed images is simple. You have to create an instance of [`discord.File`](https://discordpy.readthedocs.io/en/stable/api.html#discord.File) class: ```py # When you know the file exact path, you can pass it. file = discord.File("/this/is/path/to/my/file.png", filename="file.png") @@ -7,10 +11,10 @@ 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/latest/api.html#discord.Embed) instances have a [`set_image`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed.set_image) method which can be used to set an attachment as an image: +[`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 embed = discord.Embed() # Set other fields @@ -20,4 +24,4 @@ After this, you can send an embed with an attachment to Discord: ```py await channel.send(file=file, embed=embed) ``` -This example uses [`discord.TextChannel`](https://discordpy.readthedocs.io/en/latest/api.html#discord.TextChannel) for sending, but any instance of [`discord.abc.Messageable`](https://discordpy.readthedocs.io/en/latest/api.html#discord.abc.Messageable) can be used for sending. +This example uses [`discord.TextChannel`](https://discordpy.readthedocs.io/en/stable/api.html#discord.TextChannel) for sending, but any instance of [`discord.abc.Messageable`](https://discordpy.readthedocs.io/en/stable/api.html#discord.abc.Messageable) can be used for sending. diff --git a/bot/resources/tags/microsoft-build-tools.md b/bot/resources/tags/microsoft-build-tools.md index 7c702e296..367a1e0f8 100644 --- a/bot/resources/tags/microsoft-build-tools.md +++ b/bot/resources/tags/microsoft-build-tools.md @@ -1,5 +1,7 @@ -**Microsoft Visual C++ Build Tools** - +--- +embed: + title: "Microsoft Visual C++ Build Tools" +--- When you install a library through `pip` on Windows, sometimes you may encounter this error: ``` diff --git a/bot/resources/tags/modmail.md b/bot/resources/tags/modmail.md index 8ac19c8a7..15a7404e1 100644 --- a/bot/resources/tags/modmail.md +++ b/bot/resources/tags/modmail.md @@ -1,5 +1,7 @@ -**Contacting the moderation team via ModMail** - +--- +embed: + title: "Contacting the moderation team via ModMail" +--- <@!683001325440860340> is a bot that will relay your messages to our moderation team, so that you can start a conversation with the moderation team. Your messages will be relayed to the entire moderator team, who will be able to respond to you via the bot. It supports attachments, codeblocks, and reactions. As communication happens over direct messages, the conversation will stay between you and the mod team. diff --git a/bot/resources/tags/mutability.md b/bot/resources/tags/mutability.md index bde9b5e7e..a1d2c8ee0 100644 --- a/bot/resources/tags/mutability.md +++ b/bot/resources/tags/mutability.md @@ -1,5 +1,7 @@ -**Mutable vs immutable objects** - +--- +embed: + title: "Mutable vs immutable objects" +--- Imagine that you want to make all letters in a string upper case. Conveniently, strings have an `.upper()` method. You might think that this would work: diff --git a/bot/resources/tags/mutable-default-args.md b/bot/resources/tags/mutable-default-args.md index a8f0c38b3..e314e6c43 100644 --- a/bot/resources/tags/mutable-default-args.md +++ b/bot/resources/tags/mutable-default-args.md @@ -1,5 +1,7 @@ -**Mutable Default Arguments** - +--- +embed: + title: "Mutable default arguments" +--- Default arguments in python are evaluated *once* when the function is **defined**, *not* each time the function is **called**. This means that if you have a mutable default argument and mutate it, you will have diff --git a/bot/resources/tags/names.md b/bot/resources/tags/names.md index 3e76269f7..f949c861a 100644 --- a/bot/resources/tags/names.md +++ b/bot/resources/tags/names.md @@ -1,5 +1,7 @@ -**Naming and Binding** - +--- +embed: + title: "Naming and binding" +--- A name is a piece of text that is bound to an object. They are a **reference** to an object. Examples are function names, class names, module names, variables, etc. **Note:** Names **cannot** reference other names, and assignment **never** creates a copy. diff --git a/bot/resources/tags/nomodule.md b/bot/resources/tags/nomodule.md new file mode 100644 index 000000000..2f420e4bc --- /dev/null +++ b/bot/resources/tags/nomodule.md @@ -0,0 +1,15 @@ +--- +embed: + title: "The `ModuleNotFoundError` error" +--- +If you've installed a package but you're getting a ModuleNotFoundError when you try to import it, it's likely that the environment where your code is running is different from the one where you did the installation. + +You can read about Python environments at `/tag environments` and `/tag venv`. + +Common causes of this problem include: + +• You installed your package using `pip install ...`. It could be that the `pip` command is not pointing to the environment where your code runs. For greater control, you could instead run pip as a module within the python environment you specify: +``` +python -m pip install <your_package> +``` +• Your editor/ide is configured to create virtual environments automatically (PyCharm is configured this way by default). diff --git a/bot/resources/tags/off-topic-names.md b/bot/resources/tags/off-topic-names.md index 5d0614aaa..44a16d0c2 100644 --- a/bot/resources/tags/off-topic-names.md +++ b/bot/resources/tags/off-topic-names.md @@ -1,5 +1,7 @@ -**Off-topic channels** - +--- +embed: + title: "Off-topic channels" +--- There are three off-topic channels: • <#291284109232308226> • <#463035241142026251> diff --git a/bot/resources/tags/open.md b/bot/resources/tags/open.md index 13b4555b9..3947cb88d 100644 --- a/bot/resources/tags/open.md +++ b/bot/resources/tags/open.md @@ -1,5 +1,7 @@ -**Opening files** - +--- +embed: + title: "Opening files" +--- The built-in function `open()` is one of several ways to open files on your computer. It accepts many different parameters, so this tag will only go over two of them (`file` and `mode`). For more extensive documentation on all these parameters, consult the [official documentation](https://docs.python.org/3/library/functions.html#open). The object returned from this function is a [file object or stream](https://docs.python.org/3/glossary.html#term-file-object), for which the full documentation can be found [here](https://docs.python.org/3/library/io.html#io.TextIOBase). See also: diff --git a/bot/resources/tags/or-gotcha.md b/bot/resources/tags/or-gotcha.md index 25ade8620..88944f3a8 100644 --- a/bot/resources/tags/or-gotcha.md +++ b/bot/resources/tags/or-gotcha.md @@ -1,3 +1,7 @@ +--- +embed: + title: "The or-gotcha" +--- When checking if something is equal to one thing or another, you might think that this is possible: ```py # Incorrect... diff --git a/bot/resources/tags/ot.md b/bot/resources/tags/ot.md index 636e59110..46d33d615 100644 --- a/bot/resources/tags/ot.md +++ b/bot/resources/tags/ot.md @@ -1,3 +1,7 @@ -**Off-topic channel:** <#463035268514185226> +--- +embed: + title: "Off-topic channel" +--- +<#463035268514185226> Please read our [off-topic etiquette](https://pythondiscord.com/pages/resources/guides/off-topic-etiquette/) before participating in conversations. diff --git a/bot/resources/tags/param-arg.md b/bot/resources/tags/param-arg.md index 88069d8bd..852114921 100644 --- a/bot/resources/tags/param-arg.md +++ b/bot/resources/tags/param-arg.md @@ -1,5 +1,7 @@ -**Parameters vs. Arguments** - +--- +embed: + title: "Parameters vs. arguments" +--- A parameter is a variable defined in a function signature (the line with `def` in it), while arguments are objects passed to a function call. ```py diff --git a/bot/resources/tags/paste.md b/bot/resources/tags/paste.md index d2d54d48e..f4c004291 100644 --- a/bot/resources/tags/paste.md +++ b/bot/resources/tags/paste.md @@ -1,5 +1,7 @@ -**Pasting large amounts of code** - +--- +embed: + title: "Pasting large amounts of code" +--- If your code is too long to fit in a codeblock in Discord, you can paste your code here: https://paste.pythondiscord.com/ diff --git a/bot/resources/tags/pathlib.md b/bot/resources/tags/pathlib.md index 24ca895d8..db6c1f3eb 100644 --- a/bot/resources/tags/pathlib.md +++ b/bot/resources/tags/pathlib.md @@ -1,5 +1,7 @@ -**Pathlib** - +--- +embed: + title: "The `pathlib` module" +--- Python 3 comes with a new module named `Pathlib`. Since Python 3.6, `pathlib.Path` objects work nearly everywhere that `os.path` can be used, meaning you can integrate your new code directly into legacy code without having to rewrite anything. Pathlib makes working with paths way simpler than `os.path` does. **Feature spotlight**: @@ -18,4 +20,4 @@ Python 3 comes with a new module named `Pathlib`. Since Python 3.6, `pathlib.Pat • [**Why you should use pathlib** - Trey Hunner](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) • [**Answering concerns about pathlib** - Trey Hunner](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) • [**Official Documentation**](https://docs.python.org/3/library/pathlib.html) -• [**PEP 519** - Adding a file system path protocol](https://peps.python.org/pep-0519/) +• [**PEP 519** - Adding a file system path protocol](https://peps.python.org/pep-0519/) diff --git a/bot/resources/tags/pep8.md b/bot/resources/tags/pep8.md index a2510d697..98c739e5e 100644 --- a/bot/resources/tags/pep8.md +++ b/bot/resources/tags/pep8.md @@ -1,3 +1,7 @@ +--- +embed: + title: "PEP 8" +--- **PEP 8** is the official style guide for Python. It includes comprehensive guidelines for code formatting, variable naming, and making your code easy to read. Professional Python developers are usually required to follow the guidelines, and will often use code-linters like flake8 to verify that the code they're writing complies with the style guide. More information: diff --git a/bot/resources/tags/positional-keyword.md b/bot/resources/tags/positional-keyword.md index d6b4e0cd4..8b4cf5611 100644 --- a/bot/resources/tags/positional-keyword.md +++ b/bot/resources/tags/positional-keyword.md @@ -1,5 +1,7 @@ -**Positional vs. Keyword arguments** - +--- +embed: + title: "Positional vs. keyword arguments" +--- Functions can take two different kinds of arguments. A positional argument is just the object itself. A keyword argument is a name assigned to an object. **Example** @@ -35,4 +37,4 @@ The reverse is also true: **More info** • [Keyword only arguments](https://peps.python.org/pep-3102/) • [Positional only arguments](https://peps.python.org/pep-0570/) -• `!tags param-arg` (Parameters vs. Arguments) +• `/tag param-arg` (Parameters vs. Arguments) diff --git a/bot/resources/tags/precedence.md b/bot/resources/tags/precedence.md index ed399143c..a9b3c4070 100644 --- a/bot/resources/tags/precedence.md +++ b/bot/resources/tags/precedence.md @@ -1,5 +1,7 @@ -**Operator Precedence** - +--- +embed: + title: "Operator precedence" +--- Operator precedence is essentially like an order of operations for python's operators. **Example 1** (arithmetic) diff --git a/bot/resources/tags/quotes.md b/bot/resources/tags/quotes.md index 99ce93f61..cea6d0147 100644 --- a/bot/resources/tags/quotes.md +++ b/bot/resources/tags/quotes.md @@ -1,5 +1,7 @@ -**String Quotes** - +--- +embed: + title: "String quotes" +--- Single and Double quoted strings are the **same** in Python. The choice of which one to use is up to you, just make sure that you **stick to that choice**. With that said, there are exceptions to this that are more important than consistency. If a single or double quote is needed *inside* the string, using the opposite quotation is better than using escape characters. diff --git a/bot/resources/tags/range-len.md b/bot/resources/tags/range-len.md index 65665eccf..4bd377d59 100644 --- a/bot/resources/tags/range-len.md +++ b/bot/resources/tags/range-len.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Pythonic way of iterating over ordered collections" +--- Iterating over `range(len(...))` is a common approach to accessing each item in an ordered collection. ```py for i in range(len(my_list)): diff --git a/bot/resources/tags/regex.md b/bot/resources/tags/regex.md index ae7960b37..d8bbb38cd 100644 --- a/bot/resources/tags/regex.md +++ b/bot/resources/tags/regex.md @@ -1,4 +1,7 @@ -**Regular expressions** +--- +embed: + title: "Regular expressions" +--- Regular expressions (regex) are a tool for finding patterns in strings. The standard library's `re` module defines functions for using regex patterns. **Example** diff --git a/bot/resources/tags/relative-path.md b/bot/resources/tags/relative-path.md index 6e97b78af..c9feb2e52 100644 --- a/bot/resources/tags/relative-path.md +++ b/bot/resources/tags/relative-path.md @@ -1,5 +1,7 @@ -**Relative Path** - +--- +embed: + title: "Relative path" +--- A relative path is a partial path that is relative to your current working directory. A common misconception is that your current working directory is the location of the module you're executing, **but this is not the case**. Your current working directory is actually the **directory you were in when you ran the python interpreter**. The reason for this misconception is because a common way to run your code is to navigate to the directory your module is stored, and run `python <module>.py`. Thus, in this case your current working directory will be the same as the location of the module. However, if we instead did `python path/to/<module>.py`, our current working directory would no longer be the same as the location of the module we're executing. **Why is this important?** diff --git a/bot/resources/tags/repl.md b/bot/resources/tags/repl.md index 875b4ec47..7d56457d3 100644 --- a/bot/resources/tags/repl.md +++ b/bot/resources/tags/repl.md @@ -1,5 +1,7 @@ -**Read-Eval-Print Loop** - +--- +embed: + title: "Read-Eval-Print Loop (REPL)" +--- A REPL is an interactive language shell environment. It first **reads** one or more expressions entered by the user, **evaluates** it, yields the result, and **prints** it out to the user. It will then **loop** back to the **read** step. To use python's REPL, execute the interpreter with no arguments. This will drop you into the interactive interpreter shell, print out some relevant information, and then prompt you with the primary prompt `>>>`. At this point it is waiting for your input. diff --git a/bot/resources/tags/print-return.md b/bot/resources/tags/return-gif.md index 89d37053f..57b6afefb 100644 --- a/bot/resources/tags/print-return.md +++ b/bot/resources/tags/return-gif.md @@ -1,9 +1,10 @@ --- +aliases: ["print-return", "return-jif"] embed: - title: Print and Return + title: Print and return image: url: https://raw.githubusercontent.com/python-discord/bot/main/bot/resources/media/print-return.gif --- Here's a handy animation demonstrating how `print` and `return` differ in behavior. -See also: `!tags return` +See also: `/tag return` diff --git a/bot/resources/tags/return.md b/bot/resources/tags/return.md index e37f0eebc..805a17ee1 100644 --- a/bot/resources/tags/return.md +++ b/bot/resources/tags/return.md @@ -1,27 +1,27 @@ -**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`* +--- +embed: + title: "Return statement" +--- +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 +29,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))`. diff --git a/bot/resources/tags/round.md b/bot/resources/tags/round.md index 0392bb41b..fc67ca2f9 100644 --- a/bot/resources/tags/round.md +++ b/bot/resources/tags/round.md @@ -1,5 +1,7 @@ -**Round half to even** - +--- +embed: + title: "Round half to even*" +--- Python 3 uses bankers' rounding (also known by other names), where if the fractional part of a number is `.5`, it's rounded to the nearest **even** result instead of away from zero. Example: @@ -21,4 +23,4 @@ It should be noted that round half to even distorts the distribution by increasi • [Wikipedia article about rounding](https://en.wikipedia.org/wiki/Rounding#Round_half_to_even) • [Documentation on `round` function](https://docs.python.org/3/library/functions.html#round) • [`round` in what's new in python 3](https://docs.python.org/3/whatsnew/3.0.html#builtins) (4th bullet down) -• [How to force rounding technique](https://stackoverflow.com/a/10826537/4607272) +• [How to force rounding technique](https://stackoverflow.com/a/10826537/4607272) diff --git a/bot/resources/tags/scope.md b/bot/resources/tags/scope.md index 5c1e64e1c..cd55acf8c 100644 --- a/bot/resources/tags/scope.md +++ b/bot/resources/tags/scope.md @@ -1,8 +1,10 @@ -**Scoping Rules** - +--- +embed: + title: "Scoping rules" +--- A *scope* defines the visibility of a name within a block, where a block is a piece of python code executed as a unit. For simplicity, this would be a module, a function body, and a class definition. A name refers to text bound to an object. -*For more information about names, see `!tags names`* +*For more information about names, see `/tag names`* A module is the source code file itself, and encompasses all blocks defined within it. Therefore if a variable is defined at the module level (top-level code block), it is a global variable and can be accessed anywhere in the module as long as the block in which it's referenced is executed after it was defined. diff --git a/bot/resources/tags/seek.md b/bot/resources/tags/seek.md index bc013fe03..53949506e 100644 --- a/bot/resources/tags/seek.md +++ b/bot/resources/tags/seek.md @@ -1,5 +1,7 @@ -**Seek** - +--- +embed: + title: "Seek" +--- In the context of a [file object](https://docs.python.org/3/glossary.html#term-file-object), the `seek` function changes the stream position to a given byte offset, with an optional argument of where to offset from. While you can find the official documentation [here](https://docs.python.org/3/library/io.html#io.IOBase.seek), it can be unclear how to actually use this feature, so keep reading to see examples on how to use it. File named `example`: diff --git a/bot/resources/tags/self.md b/bot/resources/tags/self.md index d20154fd5..a90e0ffc5 100644 --- a/bot/resources/tags/self.md +++ b/bot/resources/tags/self.md @@ -1,5 +1,7 @@ -**Class instance** - +--- +embed: + title: "Class instance" +--- When calling a method from a class instance (ie. `instance.method()`), the instance itself will automatically be passed as the first argument implicitly. By convention, we call this `self`, but it could technically be called any valid variable name. ```py diff --git a/bot/resources/tags/site.md b/bot/resources/tags/site.md index 376f84742..d5ffc5e1f 100644 --- a/bot/resources/tags/site.md +++ b/bot/resources/tags/site.md @@ -1,6 +1,5 @@ --- embed: - title: "Python Discord Website" + title: "Python Discord website" --- - [Our official website](https://www.pythondiscord.com/) is an open-source community project created with Python and Django. It contains information about the server itself, lets you sign up for upcoming events, has its own wiki, contains a list of valuable learning resources, and much more. diff --git a/bot/resources/tags/slicing.md b/bot/resources/tags/slicing.md new file mode 100644 index 000000000..0d82c642c --- /dev/null +++ b/bot/resources/tags/slicing.md @@ -0,0 +1,24 @@ +--- +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. + +**Examples** +```py +>>> letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] +>>> letters[2:] # from element 2 to the end +['c', 'd', 'e', 'f', 'g'] +>>> letters[:4] # up to element 4 +['a', 'b', 'c', 'd'] +>>> letters[3:5] # elements 3 and 4 -- the right bound is not included +['d', 'e'] +>>> letters[2:-1:2] # Every other element between 2 and the last +['c', 'e'] +>>> letters[::-1] # The whole list in reverse +['g', 'f', 'e', 'd', 'c', 'b', 'a'] +>>> words = "Hello world!" +>>> words[2:7] # Strings are also sequences +"llo w" +``` diff --git a/bot/resources/tags/sql-fstring.md b/bot/resources/tags/sql-fstring.md index 538a0aa87..b57d6cb3a 100644 --- a/bot/resources/tags/sql-fstring.md +++ b/bot/resources/tags/sql-fstring.md @@ -1,4 +1,7 @@ -**SQL & f-strings** +--- +embed: + title: "SQL & f-strings" +--- Don't use f-strings (`f""`) or other forms of "string interpolation" (`%`, `+`, `.format`) to inject data into a SQL query. It is an endless source of bugs and syntax errors. Additionally, in user-facing applications, it presents a major security risk via SQL injection. Your database library should support "query parameters". A query parameter is a placeholder that you put in the SQL query. When the query is executed, you provide data to the database library, and the library inserts the data into the query for you, **safely**. @@ -12,5 +15,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 diff --git a/bot/resources/tags/star-imports.md b/bot/resources/tags/star-imports.md index 6e20e2b09..43e29fea5 100644 --- a/bot/resources/tags/star-imports.md +++ b/bot/resources/tags/star-imports.md @@ -1,4 +1,7 @@ -**Star / Wildcard imports** +--- +embed: + title: "Star / Wildcard imports" +--- Wildcard imports are import statements in the form `from <module_name> import *`. What imports like these do is that they import everything **[1]** from the module into the current module's namespace **[2]**. This allows you to use names defined in the imported module without prefixing the module's name. diff --git a/bot/resources/tags/str-join.md b/bot/resources/tags/str-join.md index c835f9313..67fe58321 100644 --- a/bot/resources/tags/str-join.md +++ b/bot/resources/tags/str-join.md @@ -1,5 +1,7 @@ -**Joining Iterables** - +--- +embed: + title: "Joining iterables" +--- If you want to display a list (or some other iterable), you can write: ```py colors = ['red', 'green', 'blue', 'yellow'] diff --git a/bot/resources/tags/string-formatting.md b/bot/resources/tags/string-formatting.md index 707d19c90..281107e4a 100644 --- a/bot/resources/tags/string-formatting.md +++ b/bot/resources/tags/string-formatting.md @@ -1,4 +1,7 @@ -**String Formatting Mini-Language** +--- +embed: + title: "String formatting mini-language" +--- The String Formatting Language in Python is a powerful way to tailor the display of strings and other data structures. This string formatting mini language works for f-strings and `.format()`. Take a look at some of these examples! diff --git a/bot/resources/tags/strip-gotcha.md b/bot/resources/tags/strip-gotcha.md index 9ad495cd2..934b10f1a 100644 --- a/bot/resources/tags/strip-gotcha.md +++ b/bot/resources/tags/strip-gotcha.md @@ -1,3 +1,7 @@ +--- +embed: + title: "The strip-gotcha" +--- When working with `strip`, `lstrip`, or `rstrip`, you might think that this would be the case: ```py >>> "Monty Python".rstrip(" Python") diff --git a/bot/resources/tags/traceback.md b/bot/resources/tags/traceback.md index e21fa6c6e..005f23b1c 100644 --- a/bot/resources/tags/traceback.md +++ b/bot/resources/tags/traceback.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Traceback" +--- Please provide the full traceback for your exception in order to help us identify your issue. While the last line of the error message tells us what kind of error you got, the full traceback will tell us which line, and other critical information to solve your problem. @@ -10,6 +14,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/type-hint.md b/bot/resources/tags/type-hint.md index f4a12f125..315257349 100644 --- a/bot/resources/tags/type-hint.md +++ b/bot/resources/tags/type-hint.md @@ -1,5 +1,7 @@ -**Type Hints** - +--- +embed: + title: "Type hints" +--- A type hint indicates what type a variable is expected to be. ```python def add(a: int, b: int) -> int: diff --git a/bot/resources/tags/underscore.md b/bot/resources/tags/underscore.md new file mode 100644 index 000000000..06c525538 --- /dev/null +++ b/bot/resources/tags/underscore.md @@ -0,0 +1,27 @@ +--- +aliases: ["under"] +embed: + title: "Meanings of underscores in identifier names" +--- + +• `__name__`: Used to implement special behaviour, such as the `+` operator for classes with the `__add__` method. [More info](https://dbader.org/blog/python-dunder-methods) +• `_name`: Indicates that a variable is "private" and should only be used by the class or module that defines it +• `name_`: Used to avoid naming conflicts. For example, as `class` is a keyword, you could call a variable `class_` instead +• `__name`: Causes the name to be "mangled" if defined inside a class. [More info](https://docs.python.org/3/tutorial/classes.html#private-variables) + +A single underscore, **`_`**, has multiple uses: +• To indicate an unused variable, e.g. in a for loop if you don't care which iteration you are on +```python +for _ in range(10): + print("Hello World") +``` +• In the REPL, where the previous result is assigned to the variable `_` +```python +>>> 1 + 1 # Evaluated and stored in `_` + 2 +>>> _ + 3 # Take the previous result and add 3 + 5 +``` +• In integer literals, e.g. `x = 1_500_000` can be written instead of `x = 1500000` to improve readability + +See also ["Reserved classes of identifiers"](https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers) in the Python docs, and [this more detailed guide](https://dbader.org/blog/meaning-of-underscores-in-python). diff --git a/bot/resources/tags/venv.md b/bot/resources/tags/venv.md index a4fc62151..bf0158fde 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 +``` diff --git a/bot/resources/tags/voice-verification.md b/bot/resources/tags/voice-verification.md index 3d88b0c71..5ec3ec2b3 100644 --- a/bot/resources/tags/voice-verification.md +++ b/bot/resources/tags/voice-verification.md @@ -1,3 +1,5 @@ -**Voice verification** - +--- +embed: + title: "Voice verification" +--- Can’t talk in voice chat? Check out <#764802555427029012> to get access. The criteria for verifying are specified there. diff --git a/bot/resources/tags/windows-path.md b/bot/resources/tags/windows-path.md index b2b0da029..af28c3eb0 100644 --- a/bot/resources/tags/windows-path.md +++ b/bot/resources/tags/windows-path.md @@ -1,5 +1,7 @@ -**PATH on Windows** - +--- +embed: + title: "PATH on Windows" +--- If you have installed Python but forgot to check the `Add Python to PATH` option during the installation, you may still be able to access your installation with ease. If you did not uncheck the option to install the `py launcher`, then you'll instead have a `py` command which can be used in the same way. If you want to be able to access your Python installation via the `python` command, then your best option is to re-install Python (remembering to tick the `Add Python to PATH` checkbox). diff --git a/bot/resources/tags/with.md b/bot/resources/tags/with.md index 83f160b4f..b42916512 100644 --- a/bot/resources/tags/with.md +++ b/bot/resources/tags/with.md @@ -1,3 +1,7 @@ +--- +embed: + title: "The `with` keyword" +--- The `with` keyword triggers a context manager. Context managers automatically set up and take down data connections, or any other kind of object that implements the magic methods `__enter__` and `__exit__`. ```py with open("test.txt", "r") as file: diff --git a/bot/resources/tags/xy-problem.md b/bot/resources/tags/xy-problem.md index 8c508f18c..f831eaf1d 100644 --- a/bot/resources/tags/xy-problem.md +++ b/bot/resources/tags/xy-problem.md @@ -1,7 +1,9 @@ -**xy-problem** - +--- +embed: + title: "xy-problem" +--- The XY problem can be summarised as asking about your attempted solution, rather than your actual problem. Often programmers will get distracted with a potential solution they've come up with, and will try asking for help getting it to work. However, it's possible this solution either wouldn't work as they expect, or there's a much better solution instead. -For more information and examples, see http://xyproblem.info/ +For more information and examples, see http://xyproblem.info/. diff --git a/bot/resources/tags/ytdl.md b/bot/resources/tags/ytdl.md index 68a0a0cdb..611b2c48c 100644 --- a/bot/resources/tags/ytdl.md +++ b/bot/resources/tags/ytdl.md @@ -1,3 +1,7 @@ +--- +embed: + title: "Our youtube-dl, or equivalents, policy" +--- Per [Python Discord's Rule 5](https://pythondiscord.com/pages/rules), we are unable to assist with questions related to youtube-dl, pytube, or other YouTube video downloaders, as their usage violates YouTube's Terms of Service. For reference, this usage is covered by the following clauses in [YouTube's TOS](https://www.youtube.com/static?gl=GB&template=terms), as of 2021-03-17: diff --git a/bot/resources/tags/zip.md b/bot/resources/tags/zip.md index 6f3157f71..6d8245694 100644 --- a/bot/resources/tags/zip.md +++ b/bot/resources/tags/zip.md @@ -1,3 +1,7 @@ +--- +embed: + title: "The `zip` function" +--- The zip function allows you to iterate through multiple iterables simultaneously. It joins the iterables together, almost like a zipper, so that each new element is a tuple with one element from each iterable. ```py diff --git a/bot/utils/channel.py b/bot/utils/channel.py index 954a10e56..20f433a3f 100644 --- a/bot/utils/channel.py +++ b/bot/utils/channel.py @@ -4,20 +4,11 @@ import discord 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): @@ -57,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}.") diff --git a/bot/utils/messages.py b/bot/utils/messages.py index c5f6dc41a..8d765ebfc 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -8,10 +8,10 @@ from io import BytesIO from typing import Callable, List, Optional, Sequence, Union import discord -from botcore.site_api import ResponseCodeError -from botcore.utils import scheduling from discord import Message from discord.ext.commands import Context +from pydis_core.site_api import ResponseCodeError +from pydis_core.utils import scheduling from sentry_sdk import add_breadcrumb import bot @@ -63,7 +63,7 @@ def reaction_check( async def wait_for_deletion( - message: discord.Message, + message: discord.Message | discord.InteractionMessage, user_ids: Sequence[int], deletion_emojis: Sequence[str] = (Emojis.trashcan,), timeout: float = 60 * 5, @@ -106,9 +106,15 @@ async def wait_for_deletion( 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, diff --git a/config-default.yml b/config-default.yml index f0e217d6c..3a1c3f612 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 @@ -239,6 +235,9 @@ guild: # Watch big_brother_logs: &BB_LOGS 468507907357409333 + # Information + roles: 851270062434156586 + moderation_categories: - *MODS_CATEGORY - *MODMAIL @@ -272,6 +271,7 @@ guild: lovefest: 542431903886606399 pyweek_announcements: 897568414044938310 revival_of_code: 988801794668908655 + legacy_help_channels_access: 1074780483776417964 contributors: 295488872404484098 help_cooldown: 699189276025421825 @@ -400,42 +400,17 @@ free: help_channels: enable: true - # Roles which are allowed to use the command which makes channels dormant - cmd_whitelist: - - *HELPERS_ROLE + # Allowed duration of inactivity before archiving a help post + idle_minutes: 30 - # 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 + # Allowed duration of inactivity when post is empty (due to deleted messages) + # before archiving a help post 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 + # Roles which are allowed to use the command which makes channels dormant + cmd_whitelist: - *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 diff --git a/docker-compose.yml b/docker-compose.yml index be7370d6b..bc53c482b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: postgres: << : *logging << : *restart_policy - image: postgres:13-alpine + image: postgres:15-alpine environment: POSTGRES_DB: pysite POSTGRES_PASSWORD: pysite diff --git a/poetry.lock b/poetry.lock index a077d5c3c..72859e4ac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -31,11 +31,11 @@ speedups = ["aiodns", "brotli", "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" @@ -76,17 +76,19 @@ python-versions = ">=3.6" [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs"] +docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["attrs", "zope.interface"] +tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] +tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] [[package]] name = "beautifulsoup4" @@ -104,28 +106,8 @@ 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" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -163,11 +145,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" @@ -199,35 +181,21 @@ toml = ["tomli"] [[package]] name = "deepdiff" -version = "5.8.1" -description = "Deep Difference and Search of any Python object/data." +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 = ">=3.6" - -[package.dependencies] -ordered-set = ">=4.1.0,<4.2.0" - -[package.extras] -cli = ["click (==8.0.3)", "pyyaml (==5.4.1)", "toml (==0.10.2)", "clevercsv (==0.7.1)"] - -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -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 = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] +cli = ["click (==8.1.3)", "pyyaml (==6.0)", "toml (==0.10.2)", "clevercsv (==0.7.4)"] [[package]] name = "discord.py" -version = "2.0.1" +version = "2.2.0" description = "A Python wrapper for the Discord API" category = "main" optional = false @@ -237,10 +205,10 @@ python-versions = ">=3.8.0" aiohttp = ">=3.7.4,<4" [package.extras] +docs = ["sphinx (==4.4.0)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport", "typing-extensions (>=4.3,<5)"] +speed = ["orjson (>=3.5.4)", "aiodns (>=1.1)", "brotli", "cchardet (==2.1.7)"] +test = ["coverage", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)"] voice = ["PyNaCl (>=1.3.0,<1.6)"] -test = ["typing-extensions (>=4.3,<5)", "pytest-mock", "pytest-cov", "pytest-asyncio", "pytest", "coverage"] -speed = ["cchardet (==2.1.7)", "brotli", "aiodns (>=1.1)", "orjson (>=3.5.4)"] -docs = ["typing-extensions (>=4.3,<5)", "sphinxcontrib-websupport", "sphinxcontrib-trio (==1.1.2)", "sphinx (==4.4.0)"] [[package]] name = "distlib" @@ -252,7 +220,7 @@ python-versions = "*" [[package]] name = "emoji" -version = "2.1.0" +version = "2.2.0" description = "Emoji for Python" category = "main" optional = false @@ -262,6 +230,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" dev = ["pytest", "coverage", "coveralls"] [[package]] +name = "exceptiongroup" +version = "1.1.0" +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" description = "execnet: rapid multi-Python deployment" @@ -274,7 +253,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 @@ -282,7 +261,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] @@ -302,28 +281,28 @@ sgmllib3k = "*" [[package]] name = "filelock" -version = "3.8.0" +version = "3.9.0" description = "A platform independent file lock." category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] -testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2022.12.7)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx (>=5.3)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)", "pytest (>=7.2)"] [[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" @@ -339,18 +318,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 = ["tox", "coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] [[package]] name = "flake8-docstrings" @@ -366,7 +345,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 @@ -414,7 +393,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 @@ -433,7 +412,7 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.5.6" +version = "2.5.18" description = "File identification library for Python" category = "dev" optional = false @@ -452,29 +431,29 @@ python-versions = ">=3.5" [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [[package]] name = "isort" -version = "5.10.1" +version = "5.12.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.8.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] +colors = ["colorama (>=0.4.3)"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] plugins = ["setuptools"] [[package]] name = "lupa" -version = "1.13" +version = "1.14.1" description = "Python wrapper around Lua and LuaJIT" category = "main" optional = false @@ -516,11 +495,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" @@ -532,7 +511,7 @@ python-versions = ">=3.5" [[package]] name = "multidict" -version = "6.0.2" +version = "6.0.4" description = "multidict implementation" category = "main" optional = false @@ -559,14 +538,11 @@ dev = ["pytest", "black", "mypy"] [[package]] name = "packaging" -version = "21.3" +version = "23.0" description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" [[package]] name = "pep8-naming" @@ -581,29 +557,29 @@ 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"] [[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 = "3.0.0" +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-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx (>=6.1.3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2.1)"] [[package]] name = "pluggy" @@ -634,35 +610,33 @@ toml = "*" virtualenv = ">=20.0.8" [[package]] -name = "psutil" -version = "5.9.3" -description = "Cross-platform lib for process and system monitoring in Python." +name = "prettytable" +version = "3.6.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 = ["ipaddress", "mock", "enum34", "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 = ["ipaddress", "mock", "enum34", "pywin32", "wmi"] [[package]] name = "pycares" -version = "4.2.2" +version = "4.3.0" description = "Python interface for c-ares" category = "main" optional = false @@ -676,7 +650,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 @@ -706,39 +680,45 @@ dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] [[package]] +name = "pydis-core" +version = "9.5.0" +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.12.0" + +[package.dependencies] +aiodns = "3.0.0" +async-rediscache = {version = "1.0.0rc2", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""} +"discord.py" = "2.2.0" +statsd = "4.0.1" + +[package.extras] +async-rediscache = ["async-rediscache[fakeredis] (==1.0.0rc2)"] + +[[package]] name = "pydocstyle" -version = "6.1.1" +version = "6.3.0" description = "Python docstring style checker" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -snowballstemmer = "*" +snowballstemmer = ">=2.2.0" [package.extras] -toml = ["toml"] +toml = ["tomli (>=1.2.3)"] [[package]] name = "pyflakes" -version = "2.5.0" +version = "3.0.1" description = "passive checker of Python programs" category = "dev" optional = false python-versions = ">=3.6" [[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] - -[[package]] name = "pyreadline3" version = "3.4.1" description = "A python implementation of GNU readline." @@ -748,7 +728,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 +737,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"] @@ -782,31 +762,19 @@ pytest = ">=4.6" testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "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 +783,6 @@ python-versions = ">=3.6" [package.dependencies] execnet = ">=1.1" pytest = ">=6.2.0" -pytest-forked = "*" [package.extras] psutil = ["psutil (>=3.0)"] @@ -869,18 +836,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 +855,6 @@ python-versions = ">=3.6" [package.dependencies] async-timeout = ">=4.0.2" -deprecated = ">=1.2.3" packaging = ">=20.4" [package.extras] @@ -897,7 +863,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 @@ -905,7 +871,7 @@ python-versions = ">=3.6" [[package]] name = "requests" -version = "2.28.1" +version = "2.28.2" description = "Python HTTP for Humans." category = "main" optional = false @@ -913,7 +879,7 @@ python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" @@ -935,7 +901,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 +923,7 @@ fastapi = ["fastapi (>=0.79.0)"] flask = ["flask (>=0.11)", "blinker (>=1.1)"] httpx = ["httpx (>=0.16.0)"] pure_eval = ["pure-eval", "executing", "asttokens"] +pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["quart (>=0.16.1)", "blinker (>=1.1)"] rq = ["rq (>=0.6)"] @@ -999,15 +966,15 @@ python-versions = "*" [[package]] name = "soupsieve" -version = "2.3.2.post1" +version = "2.4" description = "A modern CSS selector implementation for Beautiful Soup." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "statsd" -version = "3.3.0" +version = "4.0.1" description = "A simple statsd client." category = "main" optional = false @@ -1059,7 +1026,7 @@ python-versions = ">=3.7" [[package]] name = "typing-extensions" -version = "4.4.0" +version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -1067,11 +1034,11 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.14" 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 = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] @@ -1080,32 +1047,32 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.5" +version = "20.20.0" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<3" +platformdirs = ">=2.4,<4" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] -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)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-argparse (>=0.4)", "sphinx (>=6.1.3)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.2.2)", "coverage-enable-subprocess (>=1)", "coverage (>=7.1)", "flaky (>=3.7)", "packaging (>=23)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest (>=7.2.1)"] [[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" +name = "wcwidth" +version = "0.2.6" +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" -version = "1.8.1" +version = "1.8.2" description = "Yet another URL library" category = "main" optional = false @@ -1118,7 +1085,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.10.*" -content-hash = "1aabd5f7b0da4d799e63e5a7f84d2257727aaeaf24fdc3e7e53054374248083a" +content-hash = "7e51e1e6e1b060e332aaec2c14bdefc6dcb572320daafccd46881c227c58f519" [metadata.files] aiodns = [ @@ -1215,8 +1182,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"}, @@ -1231,17 +1198,16 @@ async-timeout = [ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, ] 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"}, + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] cffi = [] cfgv = [ @@ -1253,8 +1219,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"}, @@ -1313,59 +1279,59 @@ 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.0.1-py3-none-any.whl", hash = "sha256:aeb186348bf011708b085b2715cf92bbb72c692eb4f59c4c0b488130cc4c4b7e"}, - {file = "discord.py-2.0.1.tar.gz", hash = "sha256:309146476e986cb8faf038cd5d604d4b3834ef15c2d34df697ce5064bf5cd779"}, + {file = "discord.py-2.2.0-py3-none-any.whl", hash = "sha256:012e98571af6847467e81f9501bbe4c6ebfe292c842f5ef8e951908839ee1cd0"}, + {file = "discord.py-2.2.0.tar.gz", hash = "sha256:a92d69ab6f982998693d0c371ea19235fa0f9900b50068fc461086d02c33e6bb"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {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.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, ] 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"}, {file = "feedparser-6.0.10.tar.gz", hash = "sha256:27da485f4637ce7163cdeab13a80312b93b7d0c1b775bef4a47629a3110bca51"}, ] filelock = [ - {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, - {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, ] 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"}, @@ -1379,153 +1345,177 @@ 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.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"}, + {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, ] 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 = [] markdownify = [ @@ -1537,73 +1527,88 @@ 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"}, {file = "mslex-0.3.0.tar.gz", hash = "sha256:4a1ac3f25025cad78ad2fe499dd16d42759f7a3801645399cce5c404415daa97"}, ] multidict = [ - {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, - {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, - {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, - {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, - {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, - {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, - {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, - {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, - {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, - {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, - {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, - {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, - {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, ] nodeenv = [] ordered-set = [ @@ -1611,20 +1616,20 @@ ordered-set = [ {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, ] packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] pep8-naming = [ {file = "pep8-naming-0.13.2.tar.gz", hash = "sha256:93eef62f525fd12a6f8c98f4dcc17fa70baae2f37fa1f73bec00e3e44392fa48"}, {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.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, + {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1634,105 +1639,83 @@ 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.6.0-py3-none-any.whl", hash = "sha256:3b767129491767a3a5108e6f305cbaa650f8020a7db5dfe994a2df7ef7bad0fe"}, + {file = "prettytable-3.6.0.tar.gz", hash = "sha256:2e0026af955b4ea67b22122f310b90eae890738c08cb0458693a49b6221530ac"}, +] psutil = [ - {file = "psutil-5.9.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b4a247cd3feaae39bb6085fcebf35b3b8ecd9b022db796d89c8f05067ca28e71"}, - {file = "psutil-5.9.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5fa88e3d5d0b480602553d362c4b33a63e0c40bfea7312a7bf78799e01e0810b"}, - {file = "psutil-5.9.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:767ef4fa33acda16703725c0473a91e1832d296c37c63896c7153ba81698f1ab"}, - {file = "psutil-5.9.3-cp27-cp27m-win32.whl", hash = "sha256:9a4af6ed1094f867834f5f07acd1250605a0874169a5fcadbcec864aec2496a6"}, - {file = "psutil-5.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:fa5e32c7d9b60b2528108ade2929b115167fe98d59f89555574715054f50fa31"}, - {file = "psutil-5.9.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:fe79b4ad4836e3da6c4650cb85a663b3a51aef22e1a829c384e18fae87e5e727"}, - {file = "psutil-5.9.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:db8e62016add2235cc87fb7ea000ede9e4ca0aa1f221b40cef049d02d5d2593d"}, - {file = "psutil-5.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:941a6c2c591da455d760121b44097781bc970be40e0e43081b9139da485ad5b7"}, - {file = "psutil-5.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71b1206e7909792d16933a0d2c1c7f04ae196186c51ba8567abae1d041f06dcb"}, - {file = "psutil-5.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d63a2b5beaf797b87024d018772439f9d3103a395627b77d17a8d72009543"}, - {file = "psutil-5.9.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7507f6c7b0262d3e7b0eeda15045bf5881f4ada70473b87bc7b7c93b992a7d7"}, - {file = "psutil-5.9.3-cp310-cp310-win32.whl", hash = "sha256:1b540599481c73408f6b392cdffef5b01e8ff7a2ac8caae0a91b8222e88e8f1e"}, - {file = "psutil-5.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:547ebb02031fdada635452250ff39942db8310b5c4a8102dfe9384ee5791e650"}, - {file = "psutil-5.9.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d8c3cc6bb76492133474e130a12351a325336c01c96a24aae731abf5a47fe088"}, - {file = "psutil-5.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d880053c6461c9b89cd5d4808f3b8336665fa3acdefd6777662c5ed73a851a"}, - {file = "psutil-5.9.3-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e8b50241dd3c2ed498507f87a6602825073c07f3b7e9560c58411c14fe1e1c9"}, - {file = "psutil-5.9.3-cp36-cp36m-win32.whl", hash = "sha256:828c9dc9478b34ab96be75c81942d8df0c2bb49edbb481f597314d92b6441d89"}, - {file = "psutil-5.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:ed15edb14f52925869250b1375f0ff58ca5c4fa8adefe4883cfb0737d32f5c02"}, - {file = "psutil-5.9.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d266cd05bd4a95ca1c2b9b5aac50d249cf7c94a542f47e0b22928ddf8b80d1ef"}, - {file = "psutil-5.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e4939ff75149b67aef77980409f156f0082fa36accc475d45c705bb00c6c16a"}, - {file = "psutil-5.9.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68fa227c32240c52982cb931801c5707a7f96dd8927f9102d6c7771ea1ff5698"}, - {file = "psutil-5.9.3-cp37-cp37m-win32.whl", hash = "sha256:beb57d8a1ca0ae0eb3d08ccaceb77e1a6d93606f0e1754f0d60a6ebd5c288837"}, - {file = "psutil-5.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:12500d761ac091f2426567f19f95fd3f15a197d96befb44a5c1e3cbe6db5752c"}, - {file = "psutil-5.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba38cf9984d5462b506e239cf4bc24e84ead4b1d71a3be35e66dad0d13ded7c1"}, - {file = "psutil-5.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46907fa62acaac364fff0b8a9da7b360265d217e4fdeaca0a2397a6883dffba2"}, - {file = "psutil-5.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a04a1836894c8279e5e0a0127c0db8e198ca133d28be8a2a72b4db16f6cf99c1"}, - {file = "psutil-5.9.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a4e07611997acf178ad13b842377e3d8e9d0a5bac43ece9bfc22a96735d9a4f"}, - {file = "psutil-5.9.3-cp38-cp38-win32.whl", hash = "sha256:6ced1ad823ecfa7d3ce26fe8aa4996e2e53fb49b7fed8ad81c80958501ec0619"}, - {file = "psutil-5.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35feafe232d1aaf35d51bd42790cbccb882456f9f18cdc411532902370d660df"}, - {file = "psutil-5.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:538fcf6ae856b5e12d13d7da25ad67f02113c96f5989e6ad44422cb5994ca7fc"}, - {file = "psutil-5.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3d81165b8474087bb90ec4f333a638ccfd1d69d34a9b4a1a7eaac06648f9fbe"}, - {file = "psutil-5.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a7826e68b0cf4ce2c1ee385d64eab7d70e3133171376cac53d7c1790357ec8f"}, - {file = "psutil-5.9.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ec296f565191f89c48f33d9544d8d82b0d2af7dd7d2d4e6319f27a818f8d1cc"}, - {file = "psutil-5.9.3-cp39-cp39-win32.whl", hash = "sha256:9ec95df684583b5596c82bb380c53a603bb051cf019d5c849c47e117c5064395"}, - {file = "psutil-5.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4bd4854f0c83aa84a5a40d3b5d0eb1f3c128f4146371e03baed4589fe4f3c931"}, - {file = "psutil-5.9.3.tar.gz", hash = "sha256:7ccfcdfea4fc4b0a02ca2c31de7fcd186beb9cff8207800e14ab66f79c773af6"}, -] -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"}, + {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"}, ] 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"}, - {file = "pycares-4.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05e029e594c27a0066cdb89dfc5bba28ba94e2b27b0ca7aceb94f9aea06812cd"}, - {file = "pycares-4.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eb203ceedcf7f9865ed3abb6128dfbb3498c5e76342e3c820c4274cc0c8e873"}, - {file = "pycares-4.2.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4a01ba75e8a2947fc0b954850f8db9d52166634a206056febef2f833c8cfa1e"}, - {file = "pycares-4.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:064543e222e3587a92bccae704fcc5f4ce1ba1ce66aac96483c9cf504d554a67"}, - {file = "pycares-4.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a5a28f1d041aa2102bd2512e7361671e4ef46bc927e95b6932ed95cc45273480"}, - {file = "pycares-4.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:650b16f025bd3dad6a331e63bb8c9d55994c1b5d0d289ebe03c0bc16edad114f"}, - {file = "pycares-4.2.2-cp310-cp310-win32.whl", hash = "sha256:f8b76c13275b319b850e28bb9b3f5815de7521b1e0a581453d1acf10011bafef"}, - {file = "pycares-4.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:bcfcafbb376376c9cca6d37a8497dfd6dbd82333bf37627067b34dcaf5039612"}, - {file = "pycares-4.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ae5accd693c6910bbd1a99d1f4551a9e99decd65d792a80f10c27b8fcc32b497"}, - {file = "pycares-4.2.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f1901b309cb5cf7ade5822d74b904f55c49369e4ff9328818e554d4c34b4714"}, - {file = "pycares-4.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bc61edb98aff9cb4b2e07c25383100b81459a676ca0b0bd5fe77226eb1f850e"}, - {file = "pycares-4.2.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:241155687db7b45cb4ef84a18755ebc78c3ad624fd2578b48ea52ac16a4c8d9f"}, - {file = "pycares-4.2.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:27a21184ba35fff12eec36375d5b064516a0c3401dbf66a7eded7da34c5ca282"}, - {file = "pycares-4.2.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8a376e637ecd79db62761ca40cda080b9383a07d6dedbc799dd1a31e053862d9"}, - {file = "pycares-4.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6c411610be8de17cd5257845ebba5104b8e6356c62e66768728985a2ac0e9d1c"}, - {file = "pycares-4.2.2-cp36-cp36m-win32.whl", hash = "sha256:6a5af6443a1cefb36ddca47af37e29cae94a734c6c7cea3eb94e5de5cc2a4f1a"}, - {file = "pycares-4.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a01ab41405dd4dd8449f9323b2dac25e1d856ef02d85c8aedea0130b65275b2a"}, - {file = "pycares-4.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9a2053b34163d13d6d135248c65e71cefce3f25b3611677a1428ec7a57bae856"}, - {file = "pycares-4.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8064eaae5084e5155008b8f9d089055a432ff2115960273fc570f55dccedf666"}, - {file = "pycares-4.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc045040c094068d5de28e61a6fd0babe8522e8f61829839b893f7aff928173b"}, - {file = "pycares-4.2.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:135a356d52773f02d7babd2b38ad64493418363274972cc786fdce847875ca03"}, - {file = "pycares-4.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:512fb2c04c28e0e5a7de0b61624ab9c15d2df52db113f63a0aba6c6f1174b92f"}, - {file = "pycares-4.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eb374525c6231920509612f197ca47bdaa6ec9a0728aa199ba536dc0c25bb55"}, - {file = "pycares-4.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:47c6e18bbe6f2f4ce42fbdfa4ab2602268590f76110f06af60d02f964b72fada"}, - {file = "pycares-4.2.2-cp37-cp37m-win32.whl", hash = "sha256:a2c7fb5d3cb633e3f23344194da9b5caa54eb40da34dbe4465f0ebcede2e1e1a"}, - {file = "pycares-4.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:90f374fae2af5eb728841b4c2a0c8038a6889ba2a5a421e4c4e4e0f15bcc5912"}, - {file = "pycares-4.2.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c0a7e0f9371c47cf028e2389f11385e906ba2797900419509adfa86587a2ac"}, - {file = "pycares-4.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0fb3944af9492cfde6e1167780c9b8a701a56cf7d3fb29086cfb906b8261648f"}, - {file = "pycares-4.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7466315e76ab0ca4dc1354f3d7cb53f6d99d365b3778d9849e52643270beb6f2"}, - {file = "pycares-4.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f58398bd9fa99cc2dd79f7fecddc85837ccb452d673168037ea603b15aa11b"}, - {file = "pycares-4.2.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47eae9809826cea5c0eb08eec9da584dd6330e51c075c2f6963ca2067555cd07"}, - {file = "pycares-4.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6cbd4df536d2c32d2d74b854db25f1d15cc61cdd182b03206afbd7ccbe7b8f11"}, - {file = "pycares-4.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3e4519bc51b744331c968eef0bd0071ca9c3e5863b8b8c1d99540ab8bfb04235"}, - {file = "pycares-4.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e2af8ca3bc49894a87d2b5629b993c22b0e602ecb7fd2fad660ebb9be584829"}, - {file = "pycares-4.2.2-cp38-cp38-win32.whl", hash = "sha256:f6b5360e2278fae1e79479a4b56198fc7faf46ab350da18756c4de789835dbcd"}, - {file = "pycares-4.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:4304e5f0c10281abcee3c2547140a6b280c70866f2828956c9bcb2de6cffa211"}, - {file = "pycares-4.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9155e95cbe26b4b57ca691e9d8bfb5a002c7ce14ac02ddfcfe7849d4d349badb"}, - {file = "pycares-4.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:612a20514685a3d999dd0a99eede9da851be11171d599b211fac287eee452ff1"}, - {file = "pycares-4.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075d4bdde10590a2d0456eab20028aab997207e45469d30dd01a4a65caa7f8da"}, - {file = "pycares-4.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6eebdf34477c9bfb00497f8e58a674fd22b348bd928d19d29c84e8923554e1"}, - {file = "pycares-4.2.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55d39f2c38d1285d1ae248b9d2d965b161dcf51a4b6eacf97ff056da6f09dd30"}, - {file = "pycares-4.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:64261640fd52910e7960f30888abeca4e6a7a91371d351ccebc70ac1625ca74e"}, - {file = "pycares-4.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:72184b1510866c9bc97a6daca7d8218a6954c4a78640197f0840e604ba1182f9"}, - {file = "pycares-4.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:02fdf5ce48b21da6eafc5cb4508d344a0d48ac1a31e8df178f7c2fb548fcbc14"}, - {file = "pycares-4.2.2-cp39-cp39-win32.whl", hash = "sha256:fe8e0f8ed7fd795868bfc2211e345963174a9f4d1e2125753e1715a60441c8a0"}, - {file = "pycares-4.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:bb09c084de909206e6db1f014d4c6d662c7df04194df31f4831088d426afe8f1"}, - {file = "pycares-4.2.2.tar.gz", hash = "sha256:e1f57a8004370080694bd6fb969a1ffc9171a59c6824d54f791c1b2e4d298385"}, + {file = "pycares-4.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:19c9cdd3322d422931982939773e453e491dfc5c0b2e23d7266959315c7a0824"}, + {file = "pycares-4.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e56e9cdf46a092970dc4b75bbabddea9f480be5eeadc3fcae3eb5c6807c4136"}, + {file = "pycares-4.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c75a6241c79b935048272cb77df498da64b8defc8c4b29fdf9870e43ba4cbb4"}, + {file = "pycares-4.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d8654fac3742791b8bef59d1fbb3e19ae6a5c48876a6d98659f7c66ee546c4"}, + {file = "pycares-4.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebf50b049a245880f1aa16a6f72c4408e0a65b49ea1d3bf13383a44a2cabd2bf"}, + {file = "pycares-4.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:84daf560962763c0359fd79c750ef480f0fda40c08b57765088dbe362e8dc452"}, + {file = "pycares-4.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:978d10da7ee74b9979c494afa8b646411119ad0186a29c7f13c72bb4295630c6"}, + {file = "pycares-4.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c5b9d7fe52eb3d243f5ead58d5c0011884226d961df8360a34618c38c7515"}, + {file = "pycares-4.3.0-cp310-cp310-win32.whl", hash = "sha256:da7c7089ae617317d2cbe38baefd3821387b3bfef7b3ee5b797b871cb1257974"}, + {file = "pycares-4.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7106dc683db30e1d851283b7b9df7a5ea4964d6bdd000d918d91d4b1f9bed329"}, + {file = "pycares-4.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4e7a24ecef0b1933f2a3fdbf328d1b529a76cda113f8364fa0742e5b3bd76566"}, + {file = "pycares-4.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7abccc2aa4771c06994e4d9ed596453061e2b8846f887d9c98a64ccdaf4790a"}, + {file = "pycares-4.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531fed46c5ed798a914c3207be4ae7b297c4d09e4183d3cf8fd9ee59a55d5080"}, + {file = "pycares-4.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c9335175af0c64a1e0ba67bdd349eb62d4eea0ad02c235ccdf0d535fd20f323"}, + {file = "pycares-4.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5f0e95535027d2dcd51e780410632b0d3ed7e9e5ceb25dc0fe937f2c2960079"}, + {file = "pycares-4.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3692179ce5fb96908ba342e1e5303608d0c976f0d5d4619fa9d3d6d9d5a9a1b4"}, + {file = "pycares-4.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c4cb6cc7fe8e0606d30b60367f59fe26d1472e88555d61e202db70dea5c8edb"}, + {file = "pycares-4.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3215445396c74103e2054e6b349d9e85883ceda2006d0039fc2d58c9b11818a2"}, + {file = "pycares-4.3.0-cp311-cp311-win32.whl", hash = "sha256:6a0c0c3a0adf490bba9dbb37dbd07ec81e4a6584f095036ac34f06a633710ffe"}, + {file = "pycares-4.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:995cb37cc39bd40ca87bb16555a0f7724f3be30d9f9059a4caab2fde45b1b903"}, + {file = "pycares-4.3.0-cp36-cp36m-win32.whl", hash = "sha256:4c9187be72449c975c11daa1d94d7ddcc494f8a4c37a6c18f977cd7024a531d9"}, + {file = "pycares-4.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d7405ba10a2903a58b8b0faedcb54994c9ee002ad01963587fabf93e7e479783"}, + {file = "pycares-4.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40aaa12081495f879f11f4cfc95edfec1ea14711188563102f9e33fe98728fac"}, + {file = "pycares-4.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4972cac24b66c5997f3a3e2cb608e408066d80103d443e36d626a88a287b9ae7"}, + {file = "pycares-4.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35886dba7aa5b73affca8729aeb5a1f5e94d3d9a764adb1b7e75bafca44eeca5"}, + {file = "pycares-4.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cea6e1f3be016f155d60f27f16c1074d58b4d6e123228fdbc3326d076016af8"}, + {file = "pycares-4.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3a9fd2665b053afb39226ac6f8137a60910ca7729358456df2fb94866f4297de"}, + {file = "pycares-4.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e8e9195f869120e44e0aa0a6098bb5c19947f4753054365891f592e6f9eab3ef"}, + {file = "pycares-4.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:674486ecf2afb25ee219171b07cdaba481a1aaa2dabb155779c7be9ded03eaa9"}, + {file = "pycares-4.3.0-cp37-cp37m-win32.whl", hash = "sha256:1b6cd3161851499b6894d1e23bfd633e7b775472f5af35ae35409c4a47a2d45e"}, + {file = "pycares-4.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:710120c97b9afdba443564350c3f5f72fd9aae74d95b73dc062ca8ac3d7f36d7"}, + {file = "pycares-4.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9103649bd29d84bc6bcfaf09def9c0592bbc766018fad19d76d09989608b915d"}, + {file = "pycares-4.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c072dbaf73cb5434279578dc35322867d8d5df053e14fdcdcc589994ba4804ae"}, + {file = "pycares-4.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008531733f9c7a976b59c7760a3672b191159fd69ae76c01ca051f20b5e44164"}, + {file = "pycares-4.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2aae02d97d77dcff840ab55f86cb8b99bf644acbca17e1edb7048408b9782088"}, + {file = "pycares-4.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:257953ae6d400a934fd9193aeb20990ac84a78648bdf5978e998bd007a4045cd"}, + {file = "pycares-4.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c28d481efae26936ec08cb6beea305f4b145503b152cf2c4dc68cc4ad9644f0e"}, + {file = "pycares-4.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:976249b39037dbfb709ccf7e1c40d2785905a0065536385d501b94570cfed96d"}, + {file = "pycares-4.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:98568c30cfab6b327d94ae1acdf85bbba4cffd415980804985d34ca07e6f4791"}, + {file = "pycares-4.3.0-cp38-cp38-win32.whl", hash = "sha256:a2f3c4f49f43162f7e684419d9834c2c8ec165e54cb8dc47aa9dc0c2132701c0"}, + {file = "pycares-4.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:1730ef93e33e4682fbbf0e7fb19df2ed9822779d17de8ea6e20d5b0d71c1d2be"}, + {file = "pycares-4.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a26b3f1684557025da26ce65d076619890c82b95e38cc7284ce51c3539a1ce8"}, + {file = "pycares-4.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86112cce01655b9f63c5e53b74722084e88e784a7a8ad138d373440337c591c9"}, + {file = "pycares-4.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01465a191dc78e923884bb45cd63c7e012623e520cf7ed67e542413ee334804"}, + {file = "pycares-4.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9fd5d6012f3ee8c8038cbfe16e988bbd17b2f21eea86650874bf63757ee6161"}, + {file = "pycares-4.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa36b8ea91eae20b5c7205f3e6654423f066af24a1df02b274770a96cbcafaa7"}, + {file = "pycares-4.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:61019151130557c1788cae52e4f2f388a7520c9d92574f3a0d61c974c6740db0"}, + {file = "pycares-4.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:231962bb46274c52632469a1e686fab065dbd106dbef586de4f7fb101e297587"}, + {file = "pycares-4.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6c979512fa51c7ccef5204fe10ed4e5c44c2bce5f335fe98a3e423f1672bd7d4"}, + {file = "pycares-4.3.0-cp39-cp39-win32.whl", hash = "sha256:655cf0df862ce3847a60e1a106dafa2ba2c14e6636bac49e874347acdc7312dc"}, + {file = "pycares-4.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:36f2251ad0f99a5ce13df45c94c3161d9734c9e9fa2b9b4cc163b853ca170dc5"}, + {file = "pycares-4.3.0.tar.gz", hash = "sha256:c542696f6dac978e9d99192384745a65f80a7d9450501151e4a7563e06010d45"}, ] 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"}, @@ -1776,38 +1759,37 @@ 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.5.0-py3-none-any.whl", hash = "sha256:35834274a80b86a5426f27cb546b3fada8a5711bbf01bbcf1b0a8860a2afee94"}, + {file = "pydis_core-9.5.0.tar.gz", hash = "sha256:1cf9c223af9b5377e08cc0eb046a12130518e4743afbdd4f052d5556b7dae805"}, +] pydocstyle = [ - {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, - {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, ] 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 = [] pyreadline3 = [ {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, {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"}, @@ -1857,211 +1839,201 @@ 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"}, -] -requests = [] + {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.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] requests-file = [ {file = "requests-file-1.5.1.tar.gz", hash = "sha256:07d74208d3389d01c38ab89ef403af0cfec63957d53a0081d8eca738d0247d8e"}, {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"}, ] sgmllib3k = [ {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, @@ -2079,12 +2051,12 @@ sortedcontainers = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] soupsieve = [ - {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, - {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, + {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"}, + {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"}, ] 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"}, @@ -2103,76 +2075,94 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] 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.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, ] 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.20.0-py3-none-any.whl", hash = "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45"}, + {file = "virtualenv-20.20.0.tar.gz", hash = "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4"}, +] +wcwidth = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, ] -wrapt = [] yarl = [ - {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, - {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"}, - {file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"}, - {file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"}, - {file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"}, - {file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"}, - {file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"}, - {file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"}, - {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"}, - {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"}, - {file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"}, - {file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"}, - {file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"}, - {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"}, - {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"}, - {file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"}, - {file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"}, - {file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"}, - {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"}, + {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"}, + {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"}, + {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"}, + {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"}, + {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"}, + {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"}, + {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"}, + {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"}, + {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"}, + {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"}, + {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, + {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, ] diff --git a/pyproject.toml b/pyproject.toml index e85cfe532..9e1d1d5d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,52 +9,51 @@ 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"] } -redis = "4.3.4" -fakeredis = { version = "1.9.3", extras = ["lua"] } +pydis_core = { version = "9.5.0", extras = ["async-rediscache"] } +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" # Must be kept on this version unless doc command output is fixed # See https://github.com/python-discord/bot/pull/2156 markdownify = "0.6.1" -more_itertools = "8.14.0" +more_itertools = "9.0.0" pydantic = "1.10.2" 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" -statsd = "3.3.0" +rapidfuzz = "2.13.2" +regex = "2022.10.31" +sentry-sdk = "1.11.1" tldextract = "3.4.0" [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" diff --git a/tests/bot/exts/backend/sync/test_base.py b/tests/bot/exts/backend/sync/test_base.py index a17c1fa10..4dacfda17 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 87b76c6b4..2ce950965 100644 --- a/tests/bot/exts/backend/sync/test_cog.py +++ b/tests/bot/exts/backend/sync/test_cog.py @@ -2,7 +2,7 @@ import unittest 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 562c827b9..0ba2fcf11 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 @@ -47,7 +47,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): ) self.cog.try_silence = AsyncMock() self.cog.try_get_tag = AsyncMock() - self.cog.try_run_eval = AsyncMock(return_value=False) + self.cog.try_run_fixed_codeblock = AsyncMock(return_value=False) for case in test_cases: with self.subTest(try_silence_return=case["try_silence_return"], try_get_tag=case["called_try_get_tag"]): @@ -75,7 +75,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): self.cog.try_silence = AsyncMock() self.cog.try_get_tag = AsyncMock() - self.cog.try_run_eval = AsyncMock() + self.cog.try_run_fixed_codeblock = AsyncMock() error = errors.CommandNotFound() @@ -83,7 +83,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): self.cog.try_silence.assert_not_awaited() self.cog.try_get_tag.assert_not_awaited() - self.cog.try_run_eval.assert_not_awaited() + self.cog.try_run_fixed_codeblock.assert_not_awaited() self.ctx.send.assert_not_awaited() async def test_error_handler_user_input_error(self): @@ -334,13 +334,13 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext() self.tag = Tags(self.bot) self.cog = error_handler.ErrorHandler(self.bot) - self.bot.get_command.return_value = self.tag.get_command + self.bot.get_cog.return_value = self.tag async def test_try_get_tag_get_command(self): """Should call `Bot.get_command` with `tags get` argument.""" - self.bot.get_command.reset_mock() + self.bot.get_cog.reset_mock() await self.cog.try_get_tag(self.ctx) - self.bot.get_command.assert_called_once_with("tags get") + self.bot.get_cog.assert_called_once_with("Tags") async def test_try_get_tag_invoked_from_error_handler(self): """`self.ctx` should have `invoked_from_error_handler` `True`.""" @@ -350,14 +350,14 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): async def test_try_get_tag_no_permissions(self): """Test how to handle checks failing.""" - self.tag.get_command.can_run = AsyncMock(return_value=False) + self.bot.can_run = AsyncMock(return_value=False) self.ctx.invoked_with = "foo" self.assertIsNone(await self.cog.try_get_tag(self.ctx)) async def test_try_get_tag_command_error(self): """Should call `on_command_error` when `CommandError` raised.""" err = errors.CommandError() - self.tag.get_command.can_run = AsyncMock(side_effect=err) + self.bot.can_run = AsyncMock(side_effect=err) self.cog.on_command_error = AsyncMock() self.assertIsNone(await self.cog.try_get_tag(self.ctx)) self.cog.on_command_error.assert_awaited_once_with(self.ctx, err) @@ -365,7 +365,7 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): async def test_dont_call_suggestion_tag_sent(self): """Should never call command suggestion if tag is already sent.""" self.ctx.message = MagicMock(content="foo") - self.ctx.invoke = AsyncMock(return_value=True) + self.tag.get_command_ctx = AsyncMock(return_value=True) self.cog.send_command_suggestion = AsyncMock() await self.cog.try_get_tag(self.ctx) @@ -385,7 +385,7 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): async def test_call_suggestion(self): """Should call command suggestion if user is not a mod.""" self.ctx.invoked_with = "foo" - self.ctx.invoke = AsyncMock(return_value=False) + self.tag.get_command_ctx = AsyncMock(return_value=False) self.cog.send_command_suggestion = AsyncMock() await self.cog.try_get_tag(self.ctx) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 9f5143c01..65595e959 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -603,9 +603,9 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): async def test_return_none_if_one_rule_number_is_invalid(self): test_cases = [ - (('1', '6', '7', '8'), (6, 7, 8)), - (('10', "first"), (10, )), - (("first", 10), (10, )) + ("1 6 7 8", (6, 7, 8)), + ("10 first", (10,)), + ("first 10", (10,)) ] for raw_user_input, extracted_rule_numbers in test_cases: @@ -614,7 +614,7 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): str(rule_number) for rule_number in extracted_rule_numbers if rule_number < 1 or rule_number > len(self.full_rules)) - final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) + final_rule_numbers = await self.cog.rules(self.cog, self.ctx, args=raw_user_input) self.assertEqual( self.ctx.send.call_args, @@ -624,26 +624,26 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): async def test_return_correct_rule_numbers(self): test_cases = [ - (("1", "2", "first"), {1, 2}), - (("1", "hello", "2", "second"), {1}), - (("second", "third", "unknown", "999"), {2, 3}) + ("1 2 first", {1, 2}), + ("1 hello 2 second", {1}), + ("second third unknown 999", {2, 3}), ] for raw_user_input, expected_matched_rule_numbers in test_cases: with self.subTest(identifier=raw_user_input): - final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) + final_rule_numbers = await self.cog.rules(self.cog, self.ctx, args=raw_user_input) self.assertEqual(expected_matched_rule_numbers, final_rule_numbers) async def test_return_default_rules_when_no_input_or_no_match_are_found(self): test_cases = [ - ((), None), - (("hello", "2", "second"), None), - (("hello", "999"), None), + ("", None), + ("hello 2 second", None), + ("hello 999", None), ] for raw_user_input, expected_matched_rule_numbers in test_cases: with self.subTest(identifier=raw_user_input): - final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) + final_rule_numbers = await self.cog.rules(self.cog, self.ctx, args=raw_user_input) embed = self.ctx.send.call_args.kwargs['embed'] self.assertEqual(information.DEFAULT_RULES_DESCRIPTION, embed.description) self.assertEqual(expected_matched_rule_numbers, final_rule_numbers) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 29dadf372..122935e37 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -3,8 +3,8 @@ from collections import namedtuple 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/bot/exts/recruitment/talentpool/test_review.py b/tests/bot/exts/recruitment/talentpool/test_review.py index ed9b66e12..03c78ab08 100644 --- a/tests/bot/exts/recruitment/talentpool/test_review.py +++ b/tests/bot/exts/recruitment/talentpool/test_review.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime, timedelta, timezone -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from bot.exts.recruitment.talentpool import _review from tests.helpers import MockBot, MockMember, MockMessage, MockTextChannel @@ -31,10 +31,12 @@ def nomination( num_entries: int, reviewed: bool = False, id: int | None = None -) -> tuple[int, dict]: - return ( - id or MockMember().id, - {"inserted_at": inserted_at.isoformat(), "entries": [Mock() for _ in range(num_entries)], "reviewed": reviewed}, +) -> Mock: + return Mock( + id=id or MockMember().id, + inserted_at=inserted_at, + entries=[Mock() for _ in range(num_entries)], + reviewed=reviewed ) @@ -48,8 +50,8 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase): self.voting_channel = MockTextChannel() self.bot.get_channel = Mock(return_value=self.voting_channel) - self.pool = Mock(name="MockTalentPool") - self.reviewer = _review.Reviewer(self.bot, self.pool) + self.nomination_api = Mock(name="MockNominationAPI") + self.reviewer = _review.Reviewer(self.bot, self.nomination_api) @patch("bot.exts.recruitment.talentpool._review.MAX_ONGOING_REVIEWS", 3) @patch("bot.exts.recruitment.talentpool._review.MIN_REVIEW_INTERVAL", timedelta(days=1)) @@ -108,8 +110,8 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase): self.assertIs(res, expected) @patch("bot.exts.recruitment.talentpool._review.MIN_NOMINATION_TIME", timedelta(days=7)) - async def test_get_user_for_review(self): - """Test get_user_for_review function.""" + async def test_get_nomination_to_review(self): + """Test get_nomination_to_review function.""" now = datetime.now(timezone.utc) # Each case contains a list of nominations, followed by the index in that list @@ -146,19 +148,19 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase): ] for (case_num, (nominations, expected)) in enumerate(cases, 1): - nomination_dict = dict(nominations) - with self.subTest(case_num=case_num): - self.pool.cache = nomination_dict - res = await self.reviewer.get_user_for_review() + get_nominations_mock = AsyncMock(return_value=nominations) + self.nomination_api.get_nominations = get_nominations_mock + res = await self.reviewer.get_nomination_to_review() if expected is None: self.assertIsNone(res) else: - self.assertEqual(res, nominations[expected][0]) + self.assertEqual(res, nominations[expected]) + get_nominations_mock.assert_called_once_with(active=True) @patch("bot.exts.recruitment.talentpool._review.MIN_NOMINATION_TIME", timedelta(days=0)) - async def test_get_user_for_review_order(self): + async def test_get_nomination_to_review_order(self): now = datetime.now(timezone.utc) # Each case in cases is a list of nominations in the order they should be chosen from first to last @@ -196,8 +198,9 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase): with self.subTest(case_num=case_num): for i in range(len(case)): with self.subTest(nomination_num=i+1): - sub_case = dict(case[i:]) - self.pool.cache = sub_case + get_nominations_mock = AsyncMock(return_value=case[i:]) + self.nomination_api.get_nominations = get_nominations_mock - res = await self.reviewer.get_user_for_review() - self.assertEqual(res, case[i][0]) + res = await self.reviewer.get_nomination_to_review() + self.assertEqual(res, case[i]) + get_nominations_mock.assert_called_once_with(active=True) diff --git a/tests/helpers.py b/tests/helpers.py index 35a8a71f7..020f1aee5 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -9,9 +9,9 @@ from typing import Iterable, Optional 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 @@ -222,7 +222,7 @@ class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): # Create a Member instance to get a realistic Mock of `discord.Member` -member_data = {'user': 'lemon', 'roles': [1]} +member_data = {'user': 'lemon', 'roles': [1], 'flags': 2} state_mock = unittest.mock.MagicMock() member_instance = discord.Member(data=member_data, guild=guild_instance, state=state_mock) @@ -479,6 +479,25 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock): self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False) +class MockInteraction(CustomMockMixin, unittest.mock.MagicMock): + """ + A MagicMock subclass to mock Interaction objects. + + Instances of this class will follow the specifications of `discord.Interaction` + instances. For more information, see the `MockGuild` docstring. + """ + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.me = kwargs.get('me', MockMember()) + self.client = kwargs.get('client', MockBot()) + self.guild = kwargs.get('guild', MockGuild()) + self.user = kwargs.get('user', MockMember()) + self.channel = kwargs.get('channel', MockTextChannel()) + self.message = kwargs.get('message', MockMessage()) + self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False) + + attachment_instance = discord.Attachment(data=unittest.mock.MagicMock(id=1), state=unittest.mock.MagicMock()) @@ -530,6 +549,16 @@ class MockMessage(CustomMockMixin, unittest.mock.MagicMock): self.channel = kwargs.get('channel', MockTextChannel()) +class MockInteractionMessage(MockMessage): + """ + A MagicMock subclass to mock InteractionMessage objects. + + Instances of this class will follow the specifications of `discord.InteractionMessage` instances. For more + information, see the `MockGuild` docstring. + """ + pass + + emoji_data = {'require_colons': True, 'managed': True, 'id': 1, 'name': 'hyperlemon'} emoji_instance = discord.Emoji(guild=MockGuild(), state=unittest.mock.MagicMock(), data=emoji_data) |