From 8c46657aefeea118e90ada3ec9a61ba9c4734162 Mon Sep 17 00:00:00 2001 From: Izan Date: Wed, 29 Dec 2021 15:08:58 +0000 Subject: Incident archive improvements --- bot/exts/moderation/incidents.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index 77dfad255..bd9e5b88e 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -1,6 +1,5 @@ import asyncio import re -from datetime import datetime from enum import Enum from typing import Optional @@ -13,6 +12,7 @@ from bot.constants import Channels, Colours, Emojis, Guild, Roles, Webhooks from bot.log import get_logger from bot.utils import scheduling from bot.utils.messages import format_user, sub_clyde +from bot.utils.time import TimestampFormats, discord_timestamp log = get_logger(__name__) @@ -25,9 +25,9 @@ CRAWL_LIMIT = 50 CRAWL_SLEEP = 2 DISCORD_MESSAGE_LINK_RE = re.compile( - r"(https?:\/\/(?:(ptb|canary|www)\.)?discord(?:app)?\.com\/channels\/" + r"(https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/" r"[0-9]{15,20}" - r"\/[0-9]{15,20}\/[0-9]{15,20})" + r"/[0-9]{15,20}/[0-9]{15,20})" ) @@ -97,9 +97,19 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di colour = Colours.soft_red footer = f"Rejected by {actioned_by}" + day_timestamp = discord_timestamp(incident.created_at, TimestampFormats.DATE) + time_timestamp = discord_timestamp(incident.created_at, TimestampFormats.TIME) + relative_timestamp = discord_timestamp(incident.created_at, TimestampFormats.RELATIVE) + reported_on_msg = f"__*Reported {day_timestamp} at {time_timestamp} ({relative_timestamp}).*__" + + # If the description will be too long (>4096 total characters), truncate the incident content + if len(incident.content) > (allowed_content_chars := 4096-len(reported_on_msg)-2): # -2 for the newlines + description = incident.content[:allowed_content_chars-3] + f"...\n\n{reported_on_msg}" + else: + description = incident.content + f"\n\n{reported_on_msg}" + embed = discord.Embed( - description=incident.content, - timestamp=datetime.utcnow(), + description=description, colour=colour, ) embed.set_footer(text=footer, icon_url=actioned_by.display_avatar.url) @@ -380,7 +390,7 @@ class Incidents(Cog): webhook = await self.bot.fetch_webhook(Webhooks.incidents_archive) await webhook.send( embed=embed, - username=sub_clyde(incident.author.name), + username=sub_clyde(incident.author.display_name), avatar_url=incident.author.display_avatar.url, file=attachment_file, ) -- cgit v1.2.3 From 1e0c0cfe37eb9a868454508fcb813d7cf19e12cc Mon Sep 17 00:00:00 2001 From: Izan Date: Wed, 29 Dec 2021 15:09:22 +0000 Subject: Fix tests --- tests/bot/exts/moderation/test_incidents.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index cfe0c4b03..ef33aa62b 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -1,4 +1,5 @@ import asyncio +import datetime import enum import logging import typing as t @@ -13,6 +14,7 @@ from async_rediscache import RedisSession from bot.constants import Colours from bot.exts.moderation import incidents from bot.utils.messages import format_user +from bot.utils.time import TimestampFormats, discord_timestamp from tests.helpers import ( MockAsyncWebhook, MockAttachment, MockBot, MockMember, MockMessage, MockReaction, MockRole, MockTextChannel, MockUser @@ -114,10 +116,19 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase): async def test_make_embed_content(self): """Incident content appears as embed description.""" - incident = MockMessage(content="this is an incident") + current_time = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) + incident = MockMessage(content="this is an incident", created_at=current_time) + + day_timestamp = discord_timestamp(current_time, TimestampFormats.DATE) + time_timestamp = discord_timestamp(current_time, TimestampFormats.TIME) + relative_timestamp = discord_timestamp(current_time, TimestampFormats.RELATIVE) + embed, file = await incidents.make_embed(incident, incidents.Signal.ACTIONED, MockMember()) - self.assertEqual(incident.content, embed.description) + self.assertEqual( + f"{incident.content}\n\n__*Reported {day_timestamp} at {time_timestamp} ({relative_timestamp}).*__", + embed.description + ) async def test_make_embed_with_attachment_succeeds(self): """Incident's attachment is downloaded and displayed in the embed's image field.""" @@ -391,7 +402,7 @@ class TestArchive(TestIncidents): # Define our own `incident` to be archived incident = MockMessage( content="this is an incident", - author=MockUser(name="author_name", display_avatar=Mock(url="author_avatar")), + author=MockUser(display_name="author_name", display_avatar=Mock(url="author_avatar")), id=123, ) built_embed = MagicMock(discord.Embed, id=123) # We patch `make_embed` to return this @@ -422,7 +433,7 @@ class TestArchive(TestIncidents): webhook = MockAsyncWebhook() self.cog_instance.bot.fetch_webhook = AsyncMock(return_value=webhook) - message_from_clyde = MockMessage(author=MockUser(name="clyde the great")) + message_from_clyde = MockMessage(author=MockUser(display_name="clyde the great")) await self.cog_instance.archive(message_from_clyde, MagicMock(incidents.Signal), MockMember()) self.assertNotIn("clyde", webhook.send.call_args.kwargs["username"]) @@ -521,12 +532,13 @@ class TestProcessEvent(TestIncidents): async def test_process_event_confirmation_task_is_awaited(self): """Task given by `Incidents.make_confirmation_task` is awaited before method exits.""" mock_task = AsyncMock() + mock_member = MockMember(display_name="Bobby Johnson", roles=[MockRole(id=1)]) with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task): await self.cog_instance.process_event( reaction=incidents.Signal.ACTIONED.value, - incident=MockMessage(id=123), - member=MockMember(roles=[MockRole(id=1)]) + incident=MockMessage(author=mock_member, id=123), + member=mock_member ) mock_task.assert_awaited() -- cgit v1.2.3 From b7e03616ac3fc0b5e8a5a77a352df593983d187a Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 14 Jul 2022 22:21:34 +0100 Subject: Address Reviews - Use the more concise DATETIME timestamp instead of both a DATE and a TIME timestamp. - Remove underline from the "Reported ..." section at the bottom of the embed. - Re-add time of action/rejection timestamp to footer of embed. --- bot/exts/moderation/incidents.py | 7 ++++--- tests/bot/exts/moderation/test_incidents.py | 5 ++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index bd9e5b88e..f29cfcdd6 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -1,5 +1,6 @@ import asyncio import re +from datetime import datetime, timezone from enum import Enum from typing import Optional @@ -97,10 +98,9 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di colour = Colours.soft_red footer = f"Rejected by {actioned_by}" - day_timestamp = discord_timestamp(incident.created_at, TimestampFormats.DATE) - time_timestamp = discord_timestamp(incident.created_at, TimestampFormats.TIME) + reported_timestamp = discord_timestamp(incident.created_at) relative_timestamp = discord_timestamp(incident.created_at, TimestampFormats.RELATIVE) - reported_on_msg = f"__*Reported {day_timestamp} at {time_timestamp} ({relative_timestamp}).*__" + reported_on_msg = f"*Reported {reported_timestamp} ({relative_timestamp}).*" # If the description will be too long (>4096 total characters), truncate the incident content if len(incident.content) > (allowed_content_chars := 4096-len(reported_on_msg)-2): # -2 for the newlines @@ -111,6 +111,7 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di embed = discord.Embed( description=description, colour=colour, + timestamp=datetime.now(timezone.utc) ) embed.set_footer(text=footer, icon_url=actioned_by.display_avatar.url) diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index ef33aa62b..da0a79ce8 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -119,14 +119,13 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase): current_time = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) incident = MockMessage(content="this is an incident", created_at=current_time) - day_timestamp = discord_timestamp(current_time, TimestampFormats.DATE) - time_timestamp = discord_timestamp(current_time, TimestampFormats.TIME) + reported_timestamp = discord_timestamp(current_time) relative_timestamp = discord_timestamp(current_time, TimestampFormats.RELATIVE) embed, file = await incidents.make_embed(incident, incidents.Signal.ACTIONED, MockMember()) self.assertEqual( - f"{incident.content}\n\n__*Reported {day_timestamp} at {time_timestamp} ({relative_timestamp}).*__", + f"{incident.content}\n\n*Reported {reported_timestamp} ({relative_timestamp}).*", embed.description ) -- cgit v1.2.3 From 3ad7cd9a5880853771e6af1f050bd5eb251b9b88 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 13 Jul 2022 18:59:38 +0100 Subject: Add required config and constants for snekbox 3.11 --- bot/constants.py | 1 + config-default.yml | 1 + docker-compose.yml | 12 ++++++++++++ 3 files changed, 14 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index c39f9d2b8..db98e6f47 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -540,6 +540,7 @@ class URLs(metaclass=YAMLGetter): # Snekbox endpoints snekbox_eval_api: str + snekbox_311_eval_api: str # Discord API endpoints discord_api: str diff --git a/config-default.yml b/config-default.yml index 91945e2b8..a12b680e1 100644 --- a/config-default.yml +++ b/config-default.yml @@ -379,6 +379,7 @@ urls: # Snekbox snekbox_eval_api: !ENV ["SNEKBOX_EVAL_API", "http://snekbox.default.svc.cluster.local/eval"] + snekbox_311_eval_api: !ENV ["SNEKBOX_311_EVAL_API", "http://snekbox-311.default.svc.cluster.local/eval"] # Discord API URLs discord_api: &DISCORD_API "https://discordapp.com/api/v7/" diff --git a/docker-compose.yml b/docker-compose.yml index ce78f65aa..f0d3f934f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,18 @@ services: - "127.0.0.1:8060:8060" privileged: true + snekbox-311: + << : *logging + << : *restart_policy + image: ghcr.io/python-discord/snekbox:3.11-dev + init: true + ipc: none + ports: + - "127.0.0.1:8065:8060" + privileged: true + profiles: + - "3.11" + web: << : *logging << : *restart_policy -- cgit v1.2.3 From 234bd00752b0373c83002b6852bfeead64c808c7 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 13 Jul 2022 22:20:18 +0100 Subject: Allow users to eval code in either 3.11 or 3.10 To do this we need to track, for each user, the active eval's code, python version and response message. This is because a button press can now also trigger a job to continue. If we did not track these, then editing your own code and then re-evaluating it would trigger the wait_fors in continue_job for each time you pressed the button to change languages. Co-authored-by: Mark <1515135+MarkKoz@users.noreply.github.com> --- bot/exts/utils/snekbox.py | 164 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 135 insertions(+), 29 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 48bb1d3de..6841527df 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -1,22 +1,23 @@ import asyncio import contextlib -import datetime import re from functools import partial +from operator import attrgetter from signal import Signals from textwrap import dedent -from typing import Optional, Tuple +from typing import Literal, Optional, Tuple from botcore.utils import scheduling from botcore.utils.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX -from discord import AllowedMentions, HTTPException, Message, NotFound, Reaction, User -from discord.ext.commands import Cog, Command, Context, Converter, command, guild_only +from discord import AllowedMentions, HTTPException, Interaction, Member, Message, NotFound, Reaction, User, ui +from discord.ext.commands import Cog, Command, Context, Converter, command, errors, guild_only from bot.bot import Bot from bot.constants import Categories, Channels, Roles, URLs from bot.decorators import redirect_output from bot.log import get_logger from bot.utils import send_to_paste_service +from bot.utils.lock import LockedResourceError, lock_arg from bot.utils.messages import wait_for_deletion from bot.utils.services import PasteTooLongError, PasteUploadError @@ -116,6 +117,58 @@ class CodeblockConverter(Converter): return codeblocks +class PythonVersionSwitcherView(ui.View): + """ + A view to hold the Try in X Python version button. + + A subclass is required to implement a custom interaction_check so only the command invoker + can change the Python version used. + """ + + def __init__(self, owner_id: int) -> None: + super().__init__() + self.owner_id = owner_id + + async def interaction_check(self, interaction: Interaction) -> bool: + """Ensure the user clicking the button is the view owner.""" + if self.owner_id == interaction.user.id: + return True + + await interaction.response.send_message("This is not your button to click!", ephemeral=True) + return False + + +class PythonVersionSwitcherButton(ui.Button): + """A button that allows users to re-run their eval command in a different Python version.""" + + def __init__( + self, + job_name: str, + version_to_switch_to: Literal["3.10", "3.11"], + snekbox_cog: "Snekbox", + ctx: Context, + code: str + ) -> None: + self.version_to_switch_to = version_to_switch_to + super().__init__(label=f"Run in {self.version_to_switch_to}") + + self.snekbox_cog = snekbox_cog + self.ctx = ctx + self.job_name = job_name + self.code = code + + async def callback(self, interaction: Interaction) -> None: + """ + Tell snekbox to re-run the user's code in the alternative Python version. + + Use a task calling snekbox, as run_job is blocking while it waits for edit/reaction on the message. + """ + # Call run_job in a task as blocking here would cause the interaction to be reported as failed by Discord's UI. + # Discord.py only ACKs the interaction after this callback finishes. + scheduling.create_task(self.snekbox_cog.run_job(self.job_name, self.ctx, self.version_to_switch_to, self.code)) + await interaction.message.delete() + + class Snekbox(Cog): """Safe evaluation of Python code using Snekbox.""" @@ -123,9 +176,37 @@ class Snekbox(Cog): self.bot = bot self.jobs = {} - async def post_job(self, code: str, *, args: Optional[list[str]] = None) -> dict: + def build_python_version_switcher_view( + self, + job_name: str, + member: Member, + current_python_version: Literal["3.10", "3.11"], + ctx: Context, + code: str + ) -> 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": + alt_python_version = "3.11" + else: + alt_python_version = "3.10" + + view = PythonVersionSwitcherView(member.id) + view.add_item(PythonVersionSwitcherButton(job_name, alt_python_version, self, ctx, code)) + return view + + async def post_job( + self, + code: str, + python_version: Literal["3.10", "3.11"], + *, + args: Optional[list[str]] = None + ) -> dict: """Send a POST request to the Snekbox API to evaluate code and return the results.""" - url = URLs.snekbox_eval_api + if python_version == "3.10": + url = URLs.snekbox_eval_api + else: + url = URLs.snekbox_311_eval_api + data = {"input": code} if args is not None: @@ -244,9 +325,11 @@ class Snekbox(Cog): return output, paste_link + @lock_arg("snekbox.send_job", "ctx", attrgetter("author.id"), raise_error=True) async def send_job( self, ctx: Context, + python_version: Literal["3.10", "3.11"], code: str, *, args: Optional[list[str]] = None, @@ -258,7 +341,7 @@ class Snekbox(Cog): Return the bot response. """ async with ctx.typing(): - results = await self.post_job(code, args=args) + results = await self.post_job(code, python_version, args=args) msg, error = self.get_results_message(results, job_name) if error: @@ -286,14 +369,15 @@ 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]) - response = await ctx.send(msg, allowed_mentions=allowed_mentions) + view = self.build_python_version_switcher_view(job_name, ctx.author, python_version, ctx, code) + response = await ctx.send(msg, allowed_mentions=allowed_mentions, view=view) scheduling.create_task(wait_for_deletion(response, (ctx.author.id,)), event_loop=self.bot.loop) log.info(f"{ctx.author}'s {job_name} job had a return code of {results['returncode']}") return response async def continue_job( - self, ctx: Context, response: Message, command: Command + self, ctx: Context, response: Message, job_name: str ) -> tuple[Optional[str], Optional[list[str]]]: """ Check if the job's session should continue. @@ -318,6 +402,11 @@ class Snekbox(Cog): timeout=10 ) + # Ensure the response that's about to be edited is still the most recent. + # This could have already been updated via a button press to switch to an alt Python version. + if self.jobs[ctx.message.id] != response.id: + return None, None + code = await self.get_code(new_message, ctx.command) await ctx.message.clear_reaction(REDO_EMOJI) with contextlib.suppress(HTTPException): @@ -332,7 +421,7 @@ class Snekbox(Cog): codeblocks = await CodeblockConverter.convert(ctx, code) - if command is self.timeit_command: + if job_name == "timeit": return self.prepare_timeit_input(codeblocks) else: return "\n".join(codeblocks), None @@ -363,18 +452,12 @@ class Snekbox(Cog): self, job_name: str, ctx: Context, + python_version: Literal["3.10", "3.11"], code: str, *, args: Optional[list[str]] = None, ) -> None: """Handles checks, stats and re-evaluation of a snekbox job.""" - if ctx.author.id in self.jobs: - await ctx.send( - f"{ctx.author.mention} You've already got a job running - " - "please wait for it to finish!" - ) - return - if Roles.helpers in (role.id for role in ctx.author.roles): self.bot.stats.incr("snekbox_usages.roles.helpers") else: @@ -390,18 +473,19 @@ class Snekbox(Cog): log.info(f"Received code from {ctx.author} for evaluation:\n{code}") while True: - self.jobs[ctx.author.id] = datetime.datetime.now() - try: - response = await self.send_job(ctx, code, args=args, job_name=job_name) - finally: - del self.jobs[ctx.author.id] + response = await self.send_job(ctx, python_version, code, args=args, job_name=job_name) - code, args = await self.continue_job(ctx, response, ctx.command) + # Store the bot's response message id per invocation, to ensure the `wait_for`s in `continue_job` + # don't trigger if the response has already been replaced by a new response. + # This can happen when a button is pressed and then original code is edited and re-run. + self.jobs[ctx.message.id] = response.id + + code, args = await self.continue_job(ctx, response, job_name) if not code: break log.info(f"Re-evaluating code from message {ctx.message.id}:\n{code}") - @command(name="eval", aliases=("e",), usage="") + @command(name="eval", aliases=("e",), usage="[python_version] ") @guild_only() @redirect_output( destination_channel=Channels.bot_commands, @@ -410,7 +494,13 @@ class Snekbox(Cog): channels=NO_SNEKBOX_CHANNELS, ping_user=False ) - async def eval_command(self, ctx: Context, *, code: CodeblockConverter) -> None: + async def eval_command( + self, + ctx: Context, + python_version: Optional[Literal["3.10", "3.11"]], + *, + code: CodeblockConverter + ) -> None: """ Run Python code and get the results. @@ -421,12 +511,17 @@ class Snekbox(Cog): If multiple codeblocks are in a message, all of them will be joined and evaluated, ignoring the text outside of them. + By default your code is run on Python's 3.11 beta release, to assist with testing. If you + run into issues related to this Python version, you can request the bot to use Python + 3.10 by specifying the `python_version` arg and setting it to `3.10`. + We've done our best to make this sandboxed, but do let us know if you manage to find an issue with it! """ - await self.run_job("eval", ctx, "\n".join(code)) + python_version = python_version or "3.11" + await self.run_job("eval", ctx, python_version, "\n".join(code)) - @command(name="timeit", aliases=("ti",), usage="[setup_code] ") + @command(name="timeit", aliases=("ti",), usage="[python_version] [setup_code] ") @guild_only() @redirect_output( destination_channel=Channels.bot_commands, @@ -435,7 +530,13 @@ class Snekbox(Cog): channels=NO_SNEKBOX_CHANNELS, ping_user=False ) - async def timeit_command(self, ctx: Context, *, code: CodeblockConverter) -> None: + async def timeit_command( + self, + ctx: Context, + python_version: Optional[Literal["3.10", "3.11"]], + *, + code: CodeblockConverter + ) -> None: """ Profile Python Code to find execution time. @@ -446,12 +547,17 @@ class Snekbox(Cog): If multiple formatted codeblocks are provided, the first one will be the setup code, which will not be timed. The remaining codeblocks will be joined together and timed. + By default your code is run on Python's 3.11 beta release, to assist with testing. If you + run into issues related to this Python version, you can request the bot to use Python + 3.10 by specifying the `python_version` arg and setting it to `3.10`. + We've done our best to make this sandboxed, but do let us know if you manage to find an issue with it! """ + python_version = python_version or "3.11" code, args = self.prepare_timeit_input(code) - await self.run_job("timeit", ctx, code=code, args=args) + await self.run_job("timeit", ctx, python_version, code=code, args=args) def predicate_message_edit(ctx: Context, old_msg: Message, new_msg: Message) -> bool: -- cgit v1.2.3 From 32d44d10635d9d721c5bbce22400449052853aa5 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 13 Jul 2022 22:44:26 +0100 Subject: Update snekbox tests to reflect current behaviour --- tests/bot/exts/utils/test_snekbox.py | 60 ++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py index b870a9945..2fff20fd9 100644 --- a/tests/bot/exts/utils/test_snekbox.py +++ b/tests/bot/exts/utils/test_snekbox.py @@ -6,9 +6,10 @@ from discord import AllowedMentions from discord.ext import commands from bot import constants +from bot.errors import LockedResourceError from bot.exts.utils import snekbox from bot.exts.utils.snekbox import Snekbox -from tests.helpers import MockBot, MockContext, MockMessage, MockReaction, MockUser +from tests.helpers import MockBot, MockContext, MockMember, MockMessage, MockReaction, MockUser class SnekboxTests(unittest.IsolatedAsyncioTestCase): @@ -26,7 +27,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): context_manager.__aenter__.return_value = resp self.bot.http_session.post.return_value = context_manager - self.assertEqual(await self.cog.post_job("import random"), "return") + self.assertEqual(await self.cog.post_job("import random", "3.10"), "return") self.bot.http_session.post.assert_called_with( constants.URLs.snekbox_eval_api, json={"input": "import random"}, @@ -179,9 +180,9 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.cog.send_job = AsyncMock(return_value=response) self.cog.continue_job = AsyncMock(return_value=(None, None)) - await self.cog.eval_command(self.cog, ctx=ctx, code=['MyAwesomeCode']) - self.cog.send_job.assert_called_once_with(ctx, 'MyAwesomeCode', args=None, job_name='eval') - self.cog.continue_job.assert_called_once_with(ctx, response, ctx.command) + await self.cog.eval_command(self.cog, ctx=ctx, python_version='3.11', code=['MyAwesomeCode']) + self.cog.send_job.assert_called_once_with(ctx, '3.11', 'MyAwesomeCode', args=None, job_name='eval') + self.cog.continue_job.assert_called_once_with(ctx, response, 'eval') async def test_eval_command_evaluate_twice(self): """Test the eval and re-eval command procedure.""" @@ -192,23 +193,28 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.cog.continue_job = AsyncMock() self.cog.continue_job.side_effect = (('MyAwesomeFormattedCode', None), (None, None)) - await self.cog.eval_command(self.cog, ctx=ctx, code=['MyAwesomeCode']) + await self.cog.eval_command(self.cog, ctx=ctx, python_version='3.11', code=['MyAwesomeCode']) self.cog.send_job.assert_called_with( - ctx, 'MyAwesomeFormattedCode', args=None, job_name='eval' + ctx, '3.11', 'MyAwesomeFormattedCode', args=None, job_name='eval' ) - self.cog.continue_job.assert_called_with(ctx, response, ctx.command) + self.cog.continue_job.assert_called_with(ctx, response, 'eval') async def test_eval_command_reject_two_eval_at_the_same_time(self): """Test if the eval command rejects an eval if the author already have a running eval.""" ctx = MockContext() ctx.author.id = 42 - ctx.author.mention = '@LemonLemonishBeard#0042' - ctx.send = AsyncMock() - self.cog.jobs = (42,) - await self.cog.eval_command(self.cog, ctx=ctx, code='MyAwesomeCode') - ctx.send.assert_called_once_with( - "@LemonLemonishBeard#0042 You've already got a job running - please wait for it to finish!" - ) + + async def delay_with_side_effect(*args, **kwargs) -> dict: + """Delay the post_job call to ensure the job runs long enough to conflict.""" + await asyncio.sleep(1) + return {'stdout': '', 'returncode': 0} + + self.cog.post_job = AsyncMock(side_effect=delay_with_side_effect) + with self.assertRaises(LockedResourceError): + await asyncio.gather( + self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval'), + self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval'), + ) async def test_send_job(self): """Test the send_job function.""" @@ -226,7 +232,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False) self.bot.get_cog.return_value = mocked_filter_cog - await self.cog.send_job(ctx, 'MyAwesomeCode', job_name='eval') + await self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval') ctx.send.assert_called_once() self.assertEqual( @@ -237,7 +243,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): expected_allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author]) self.assertEqual(allowed_mentions.to_dict(), expected_allowed_mentions.to_dict()) - self.cog.post_job.assert_called_once_with('MyAwesomeCode', args=None) + self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) self.cog.get_status_emoji.assert_called_once_with({'stdout': '', 'returncode': 0}) self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0}, 'eval') self.cog.format_output.assert_called_once_with('') @@ -258,7 +264,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False) self.bot.get_cog.return_value = mocked_filter_cog - await self.cog.send_job(ctx, 'MyAwesomeCode', job_name='eval') + await self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval') ctx.send.assert_called_once() self.assertEqual( @@ -267,7 +273,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): '\n\n```\nWay too long beard\n```\nFull output: lookatmybeard.com' ) - self.cog.post_job.assert_called_once_with('MyAwesomeCode', args=None) + self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) self.cog.get_status_emoji.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}) self.cog.get_results_message.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}, 'eval') self.cog.format_output.assert_called_once_with('Way too long beard') @@ -287,7 +293,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False) self.bot.get_cog.return_value = mocked_filter_cog - await self.cog.send_job(ctx, 'MyAwesomeCode', job_name='eval') + await self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval') ctx.send.assert_called_once() self.assertEqual( @@ -295,7 +301,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): '@LemonLemonishBeard#0042 :nope!: Return code 127.\n\n```\nBeard got stuck in the eval\n```' ) - self.cog.post_job.assert_called_once_with('MyAwesomeCode', args=None) + self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) self.cog.get_status_emoji.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}, 'eval') self.cog.format_output.assert_not_called() @@ -303,9 +309,17 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): @patch("bot.exts.utils.snekbox.partial") async def test_continue_job_does_continue(self, partial_mock): """Test that the continue_job function does continue if required conditions are met.""" - ctx = MockContext(message=MockMessage(add_reaction=AsyncMock(), clear_reactions=AsyncMock())) - response = MockMessage(delete=AsyncMock()) + ctx = MockContext( + message=MockMessage( + id=4, + add_reaction=AsyncMock(), + clear_reactions=AsyncMock() + ), + author=MockMember(id=14) + ) + response = MockMessage(id=42, delete=AsyncMock()) new_msg = MockMessage() + self.cog.jobs = {4: 42} self.bot.wait_for.side_effect = ((None, new_msg), None) expected = "NewCode" self.cog.get_code = create_autospec(self.cog.get_code, spec_set=True, return_value=expected) -- cgit v1.2.3 From c4a7722498329d316c7633e9a57861727cac2c2d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 16 Jul 2022 18:02:55 +0100 Subject: Use generic views from bot0core for snekbox --- bot/exts/utils/snekbox.py | 38 +- poetry.lock | 1287 +++++++++++++++++++++++++++++++++++++++++---- pyproject.toml | 2 +- 3 files changed, 1192 insertions(+), 135 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 6841527df..2eef04a29 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -7,18 +7,17 @@ from signal import Signals from textwrap import dedent from typing import Literal, Optional, Tuple -from botcore.utils import scheduling +from botcore.utils import interactions, scheduling from botcore.utils.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX -from discord import AllowedMentions, HTTPException, Interaction, Member, Message, NotFound, Reaction, User, ui +from discord import AllowedMentions, HTTPException, Interaction, Member, Message, NotFound, Reaction, User, enums, ui from discord.ext.commands import Cog, Command, Context, Converter, command, errors, guild_only from bot.bot import Bot -from bot.constants import Categories, Channels, Roles, URLs +from bot.constants import Categories, Channels, MODERATION_ROLES, Roles, URLs from bot.decorators import redirect_output from bot.log import get_logger from bot.utils import send_to_paste_service from bot.utils.lock import LockedResourceError, lock_arg -from bot.utils.messages import wait_for_deletion from bot.utils.services import PasteTooLongError, PasteUploadError log = get_logger(__name__) @@ -117,27 +116,6 @@ class CodeblockConverter(Converter): return codeblocks -class PythonVersionSwitcherView(ui.View): - """ - A view to hold the Try in X Python version button. - - A subclass is required to implement a custom interaction_check so only the command invoker - can change the Python version used. - """ - - def __init__(self, owner_id: int) -> None: - super().__init__() - self.owner_id = owner_id - - async def interaction_check(self, interaction: Interaction) -> bool: - """Ensure the user clicking the button is the view owner.""" - if self.owner_id == interaction.user.id: - return True - - await interaction.response.send_message("This is not your button to click!", ephemeral=True) - return False - - class PythonVersionSwitcherButton(ui.Button): """A button that allows users to re-run their eval command in a different Python version.""" @@ -150,7 +128,7 @@ class PythonVersionSwitcherButton(ui.Button): code: str ) -> None: self.version_to_switch_to = version_to_switch_to - super().__init__(label=f"Run in {self.version_to_switch_to}") + super().__init__(label=f"Run in {self.version_to_switch_to}", style=enums.ButtonStyle.primary) self.snekbox_cog = snekbox_cog self.ctx = ctx @@ -190,8 +168,13 @@ class Snekbox(Cog): else: alt_python_version = "3.10" - view = PythonVersionSwitcherView(member.id) + view = interactions.ViewWithUserAndRoleCheck( + allowed_users=(member.id,), + allowed_roles=MODERATION_ROLES, + ) view.add_item(PythonVersionSwitcherButton(job_name, alt_python_version, self, ctx, code)) + view.add_item(interactions.DeleteMessageButton()) + return view async def post_job( @@ -371,7 +354,6 @@ class Snekbox(Cog): allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author]) view = self.build_python_version_switcher_view(job_name, ctx.author, python_version, ctx, code) response = await ctx.send(msg, allowed_mentions=allowed_mentions, view=view) - scheduling.create_task(wait_for_deletion(response, (ctx.author.id,)), event_loop=self.bot.loop) log.info(f"{ctx.author}'s {job_name} job had a return code of {results['returncode']}") return response diff --git a/poetry.lock b/poetry.lock index f41fe2358..4f7012215 100644 --- a/poetry.lock +++ b/poetry.lock @@ -88,7 +88,7 @@ python-versions = ">=3.6" [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -125,7 +125,7 @@ lxml = ["lxml"] [[package]] name = "bot-core" -version = "7.2.0" +version = "7.3.1" description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." category = "main" optional = false @@ -141,8 +141,7 @@ async-rediscache = ["async-rediscache[fakeredis] (==0.2.0)"] [package.source] type = "url" -url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.2.0.zip" - +url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.3.1.zip" [[package]] name = "certifi" version = "2022.6.15" @@ -153,7 +152,7 @@ python-versions = ">=3.6" [[package]] name = "cffi" -version = "1.15.0" +version = "1.15.1" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -268,7 +267,7 @@ url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca [[package]] name = "distlib" -version = "0.3.4" +version = "0.3.5" description = "Distribution utilities" category = "dev" optional = false @@ -490,11 +489,11 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "2.7" +version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.5" [[package]] name = "iniconfig" @@ -520,7 +519,7 @@ plugins = ["setuptools"] [[package]] name = "jarowinkler" -version = "1.0.4" +version = "1.0.5" description = "library for fast approximate string matching using Jaro and Jaro-Winkler similarity" category = "main" optional = false @@ -1100,20 +1099,20 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.5" +version = "1.26.10" 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.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.15.0" +version = "20.15.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1152,104 +1151,1180 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "36098ac3587aada7726bd4cb8f8c9a450d4aa07f849daefebf896d11d020af7d" +content-hash = "1152d8ee2e09a6d0283d82be5b1fee21a9d7bd36d206e02fcee5a4ad4ecd9523" [metadata.files] -aiodns = [] -aiohttp = [] -aioredis = [] -aiosignal = [] -arrow = [] -async-rediscache = [] -async-timeout = [] +aiodns = [ + {file = "aiodns-3.0.0-py3-none-any.whl", hash = "sha256:2b19bc5f97e5c936638d28e665923c093d8af2bf3aa88d35c43417fa25d136a2"}, + {file = "aiodns-3.0.0.tar.gz", hash = "sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6"}, +] +aiohttp = [ + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, + {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, + {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, + {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, + {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, + {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, + {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, + {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, + {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, + {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, +] +aioredis = [ + {file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"}, + {file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"}, +] +aiosignal = [ + {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, + {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, +] +arrow = [ + {file = "arrow-1.2.2-py3-none-any.whl", hash = "sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"}, + {file = "arrow-1.2.2.tar.gz", hash = "sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b"}, +] +async-rediscache = [ + {file = "async-rediscache-0.2.0.tar.gz", hash = "sha256:c1fd95fe530211b999748ebff96e2e9b629f2664957f9b36916b898e42fc57c4"}, + {file = "async_rediscache-0.2.0-py3-none-any.whl", hash = "sha256:710676211b407399c9ad94afa66fa04c22a936be11ba6f227e6c74cfa140ce78"}, +] +async-timeout = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] atomicwrites = [] -attrs = [] -beautifulsoup4 = [] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +beautifulsoup4 = [ + {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, + {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, +] bot-core = [] -certifi = [] -cffi = [] -cfgv = [] -charset-normalizer = [] -colorama = [] -coloredlogs = [] -coverage = [] -deepdiff = [] -deprecated = [] +certifi = [ + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, +] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, + {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coloredlogs = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] +coverage = [ + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, + {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, +] +deepdiff = [ + {file = "deepdiff-5.7.0-py3-none-any.whl", hash = "sha256:1ffb38c3b5d9174eb2df95850c93aee55ec00e19396925036a2e680f725079e0"}, + {file = "deepdiff-5.7.0.tar.gz", hash = "sha256:838766484e323dcd9dec6955926a893a83767dc3f3f94542773e6aa096efe5d4"}, +] +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] "discord.py" = [] distlib = [] -emoji = [] -execnet = [] -fakeredis = [] -feedparser = [] -filelock = [] -flake8 = [] -flake8-annotations = [] -flake8-bugbear = [] -flake8-docstrings = [] -flake8-isort = [] -flake8-polyfill = [] -flake8-string-format = [] -flake8-tidy-imports = [] -flake8-todo = [] -frozenlist = [] -hiredis = [] -humanfriendly = [] -identify = [] -idna = [] -iniconfig = [] -isort = [] -jarowinkler = [] -lupa = [] +emoji = [ + {file = "emoji-1.7.0.tar.gz", hash = "sha256:65c54533ea3c78f30d0729288998715f418d7467de89ec258a31c0ce8660a1d1"}, +] +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.7.5-py3-none-any.whl", hash = "sha256:c4ca2be686e7e7637756ccc7dcad8472a5e4866b065431107d7a4b7a250d4e6f"}, + {file = "fakeredis-1.7.5.tar.gz", hash = "sha256:49375c630981dd4045d9a92e2709fcd4476c91f927e0228493eefa625e705133"}, +] +feedparser = [ + {file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"}, + {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"}, +] +filelock = [ + {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, + {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +flake8-annotations = [ + {file = "flake8-annotations-2.8.0.tar.gz", hash = "sha256:a2765c6043098aab0a3f519b871b33586c7fba7037686404b920cf8100cc1cdc"}, + {file = "flake8_annotations-2.8.0-py3-none-any.whl", hash = "sha256:880f9bb0677b82655f9021112d64513e03caefd2e0d786ab4a59ddb5b262caa9"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-22.3.23.tar.gz", hash = "sha256:e0dc2a36474490d5b1a2d57f9e4ef570abc09f07cbb712b29802e28a2367ff19"}, + {file = "flake8_bugbear-22.3.23-py3-none-any.whl", hash = "sha256:ec5ec92195720cee1589315416b844ffa5e82f73a78e65329e8055322df1e939"}, +] +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-4.1.1.tar.gz", hash = "sha256:d814304ab70e6e58859bc5c3e221e2e6e71c958e7005239202fee19c24f82717"}, + {file = "flake8_isort-4.1.1-py3-none-any.whl", hash = "sha256:c4e8b6dcb7be9b71a02e6e5d4196cefcef0f3447be51e82730fb336fff164949"}, +] +flake8-polyfill = [ + {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, + {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, +] +flake8-string-format = [ + {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, + {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, +] +flake8-tidy-imports = [ + {file = "flake8-tidy-imports-4.6.0.tar.gz", hash = "sha256:3e193d8c4bb4492408a90e956d888b27eed14c698387c9b38230da3dad78058f"}, + {file = "flake8_tidy_imports-4.6.0-py3-none-any.whl", hash = "sha256:6ae9f55d628156e19d19f4c359dd5d3e95431a9bd514f5e2748c53c1398c66b2"}, +] +flake8-todo = [ + {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"}, +] +frozenlist = [ + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, + {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, + {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, + {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, + {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, + {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, + {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, +] +hiredis = [ + {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"}, + {file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"}, + {file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"}, + {file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"}, + {file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"}, + {file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"}, + {file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"}, + {file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"}, + {file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"}, + {file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"}, + {file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"}, + {file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"}, + {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"}, +] +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.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, + {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +jarowinkler = [ + {file = "jarowinkler-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9d875e758e6770180adc2cf7d018f6c4ec51f2b1380f7f777d2981e8d9d289c0"}, + {file = "jarowinkler-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1648c56e2842023ce11bc04138e4c4be4cc11ab1795b1f7490373f8d0e25f91d"}, + {file = "jarowinkler-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5dfc31f7d700d48ec5050e09926160e074aa43d10f651458b141b4aa9c02f787"}, + {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf7d840bac828523d38917d5856f8de3867a9b241e3ee863b37c0cb7a58cef4d"}, + {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2295e73427f5431400e202eb92247ae0ce6f1ea18bf7055bb4aab177326101"}, + {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c98fee353b694435ad2e8ff60e3981c9469a0bac0ba901255af81eef3b96842f"}, + {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:160a355282d674652394d56d616e125f42494f40f9a57df697f51420464ba3c9"}, + {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e40aa8f37a830f463e412f4c2b3bd64c67a57c27d3d84b1b4afa6fb034b1a50"}, + {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1fcb915e886099c507719d682a1963a580ffa729b3b12d0bdd5d82bccdd1b49f"}, + {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1d99cf7ac9003788f3dbc26f92840b4e8b0befb04f19c7601e90fcbe2824347"}, + {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:8f05afd2df6c69b266c5fa16aa3ebb8bf8d63ca9e70aa7950485e2156fc78cdd"}, + {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:36bb3a75977eeb4f6f048209f26c8c813893547e159047f7c3f3dbb320c7f10c"}, + {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:022e108c8cd58a588f204c2861b11a30bb5a71e207fd56ba5a1aa11bc37fa1a8"}, + {file = "jarowinkler-1.0.5-cp310-cp310-win32.whl", hash = "sha256:500ca3acdaa6444867f627d104bc28eef356e40576f60c1f98d2802d0bc45f78"}, + {file = "jarowinkler-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:0a39dc132b74caefbf6c35fa656046fea0c013cf5009d09a09b9983375547588"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5c94562a96b5e3e29c6bea37a2671f1746541d8babb93edbac9c5994895e8eec"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43562cc5ca8f599cffe2015d6b5ac8585a0c5fa4e8ff9ffd397af73523013fed"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c46c603c3e10426d5006ef1fb6bef3d14b311259c504f97dcc692481e31ae80"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:117424c2c134fb64d6b406a0b4be5a570d2c45e4b05572863e4cdb4b89358e7d"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:effc4e7325d80baa83c22f20ba0f9ddd57249c0ef374adab0af2011b3091c612"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2adf6f1766f147caaf061444ee3ff68b4368e6545d5631fe33d9e97cb3cb5853"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:976b663be2e54efd9558ffd42eae2e3c011852a5fdbee8aede621de3c0d58186"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd95a2e1af798fe597fcbbe9f8d7a5e8dfec3fb59670aa74be3501422d8aee76"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:110232f025fa15bfa3beb5d388b3ff5217edf2a5f20555a1beee7d7fb302d865"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:3da6a52acf3b9750bf71f1c7591cd454138a0b44abce52fbc98b44c7e2def87e"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e941b5b2ded630772ba26181d726ff0f1e1f8a72123932c2a0c108f70ae89188"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:e542b78e9d934d625c6b0b7be4964ff79a7dac9dba1c7abef3b9202a38d11261"}, + {file = "jarowinkler-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:57f67552e1da2be03daa48bf1a4d0357b5b28a92ff6539974fd665516ccf31be"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c8fcd41aa0f5a89a566c8afac19621115b5df1d6e542b03d92bd3c9f4b5f81b"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe94701fd2d101d22e1fa3905c77b53913358425c86c93b03a41dfde77347655"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b662164e93d7b1a9808b36d85c390004e7b6a8a64de88929a244a191ea1174b7"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45ebe15affb4bc905bd7d68bce01c393bfc1456b8ffeeccaa76d22041c2bf2cd"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63d510a57963b6a3c771c11e606d79c991f4648f295acb4631d6fa8100507a00"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:592d2447d4fedaf099b44f42f245d0ba3ad776538c2d4022430a528a4d0cafa4"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:33d4fd6247bc31264d4761fff2cbfefab86294f21d057d150e2ffee7dad9f338"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b91db5a9e153837fb6fc5bf53c1fc6dc34b1a86336f8e1dd694fa4cfd640155b"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:2b0f47c8ad3d98bc914d0b29713e3e034c0a68dc3913b2ecd79aaf506a79817c"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5174349057285c877d01fd69b5a5db317c56130b3ccb51500c1ed324edfcf664"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0dc8f98fa00a025b9418e610c0d1d44ea3eba58643ee0d12eee49f7b7921947e"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:1f6640c242e31f6ea991801c14240b0b6a642cca63d5451a1e05ead71566adec"}, + {file = "jarowinkler-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ecd644d7fdc6fc1636f5eabdb3e325f7f477eb15ad1f93af86b83e32e5a22dd2"}, + {file = "jarowinkler-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:08e91b815a5f2fd3bf7c946f7206d83a085e5b39dc215b99127dcf03fa667e2c"}, + {file = "jarowinkler-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b65b771629395944e10129d7cfdad35dd14d75e697c58388e918a1add1f0af9c"}, + {file = "jarowinkler-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:52f40babf8536bf3dfbdcce0b626dcc0453c7d5a80a9702826e93b20fc287b03"}, + {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47b624d06a5729af1fcd741d57e08c43e289996152c690be6ec38267d2ec1ae8"}, + {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8e4468a78577839b65251720d6e88bfbe66977bc53aed80bf32efdfd5a7fafa"}, + {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9ec340eac44452206b6e3c8d6f3198cf10e7758433adb4e9c48390c0ed81c7"}, + {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113d38a2434e6d8347c48aa9b7d4c3c34f4c0ffcd8a65b25f89a8129c373da61"}, + {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6c419a12597b610ecb45c83c358ec9418f788131c3817413c69f16ae2a4211b"}, + {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a835d57440d1836ac6d813782cbc56c88f9a52ecc5ad9f444530b766c718750b"}, + {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:29e473a5988045ee5820ad4f9b700aa09cc60dddac16f5cad625daaa5405f862"}, + {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:67216289c24fa3934d23f781cdf4468aeb910f4d295bcbbfc206335dbed38e78"}, + {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7cc2f84ba258da0e2455337ff4fd9f77a4b672b8997ee9599ed8cc516bc57d2d"}, + {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75156fa4dcc139afdb97eb5419c1fe24ee1f9b6bca02ce36b22c8f28fa06c074"}, + {file = "jarowinkler-1.0.5-cp38-cp38-win32.whl", hash = "sha256:b05a2e291e5b4f8254fa214c911cd80847326237a921a27135572e32747ff5a0"}, + {file = "jarowinkler-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:251152bc224e8dd7ca290d4b6dc1e2ef6d1a6cd52f8611617870d2e1dc615772"}, + {file = "jarowinkler-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:88b3757cdd150c9e8763c63b66154239910cc49179cfbb1841b8783cba8c5be2"}, + {file = "jarowinkler-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:391c887af2c46b18fb1ad40246147a1322bd9073edf61b866fc7143946deed08"}, + {file = "jarowinkler-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81323b75faf3cc62a96524d7f84f34fa3bd49fe6a53eacd6a9c552e3ca5eeeb8"}, + {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce612d1d74775ed444ba0a5f7cc9307d1c10059fafd787b8ef1b18dac96b0dd9"}, + {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b4b198ce38635925e9aaa227499c2c8aeee2e9c2372a73cef56fb34f0af7de0"}, + {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2732bad98bd70dcf00e8b2a4a9e21629aef9e271de0f8e2a71fb6a22d95dca5b"}, + {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:283b964c39863c8e59f3538dd054dbcb32700418cec1b7c8d70d28118b28363a"}, + {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c7336f424fa2c8d739b97f276535b8fecc23d7622b00c647154adb67664029"}, + {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ac6172e4d531aa57f48b6ec778bc818d0184a7e8779079583e12b6beac7fae6"}, + {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e4da9a208ad72318084b0d0d4bbbe0bb14cf75141ef72ef60087d011da410cc9"}, + {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:97b05394a7bd016e39ec3e31bae7341f5cf70ee87cc0faa35f6c2f71c5f9cc64"}, + {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:5ab84f035ae4bd7288abc1d3b4388f491d3630dd7dc79fb207bbadb833844e36"}, + {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a982d80da526fd11e83fe44985c410ec3a15846727c8553af591b758fbbb2aef"}, + {file = "jarowinkler-1.0.5-cp39-cp39-win32.whl", hash = "sha256:c4bcae3864139b87f03ff3d762b64d113a7d4ff9c2a781c8856a7a3be3ecc456"}, + {file = "jarowinkler-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:c68790ae84538504a7b7074fbb81827e19a28b00e942d92104d98d05606c0927"}, + {file = "jarowinkler-1.0.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dd6ff49d794ccb6c624ca5c14cbefc6c2611490b69d17b62c2dcf73e18dca046"}, + {file = "jarowinkler-1.0.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acd563c348d6a9615b068dba24f6c04920527d003a2790b7006facf589b5c509"}, + {file = "jarowinkler-1.0.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:579077f5c449b871c2944c1991a8cba3654dfb72207a9c3990736ec9f5cc57a9"}, + {file = "jarowinkler-1.0.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9dc4420651e2a904d5176fede44b871808b9057497037e0e2b26462600c70ff"}, + {file = "jarowinkler-1.0.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:59c4229dd19b4572c226ab29959ef15706ecb1d58e93cc9218cd9461fc9bb8b3"}, + {file = "jarowinkler-1.0.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:904189811e04331853149be4684896b95dda646b41b6266f8d6813cecd8f77ee"}, + {file = "jarowinkler-1.0.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf59fbc689e8e901429493a3de2932b6fd8ddf7f1cec7b3d55a0f0a0fcfa975"}, + {file = "jarowinkler-1.0.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bc2a594eda6a41a8367b94b3817fa923e097dd64d385549c32f7eb21441b2d5"}, + {file = "jarowinkler-1.0.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af2a2c1b68266c516e9fa329fe5af51b8061d97ea1b230742ae70176dcdfd116"}, + {file = "jarowinkler-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35abfc9c59e3a47c59c7d988e6756c339c1779bac40020fe39c4875a37d0873a"}, + {file = "jarowinkler-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76a69ccdd0961bf79795caf31a0eba12feee3ca2225cfa88040752dfbe5ccd4b"}, + {file = "jarowinkler-1.0.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09fe4f99344ba01b20a29da0aa6b42276abecd2edc465d9cd54582de9aad8c0b"}, + {file = "jarowinkler-1.0.5.tar.gz", hash = "sha256:d0ecdae8e122594d22e09ceebfca23342d290a9305d669958e674e0e39e2e260"}, +] +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"}, +] lxml = [] -markdownify = [] -mccabe = [] -more-itertools = [] -mslex = [] -multidict = [] -nodeenv = [] -ordered-set = [] -packaging = [] -pep8-naming = [] -pip-licenses = [] -platformdirs = [] -pluggy = [] -pre-commit = [] -psutil = [] -ptable = [] -py = [] -pycares = [] -pycodestyle = [] -pycparser = [] -pydocstyle = [] -pyflakes = [] -pyparsing = [] -pyreadline3 = [] -pytest = [] -pytest-cov = [] -pytest-forked = [] -pytest-xdist = [] -python-dateutil = [] -python-dotenv = [] -python-frontmatter = [] -pyyaml = [] -rapidfuzz = [] -redis = [] -regex = [] -requests = [] -requests-file = [] -sentry-sdk = [] -sgmllib3k = [] -six = [] -snowballstemmer = [] -sortedcontainers = [] -soupsieve = [] -statsd = [] -taskipy = [] -testfixtures = [] -tldextract = [] -toml = [] -tomli = [] +markdownify = [ + {file = "markdownify-0.6.1-py3-none-any.whl", hash = "sha256:7489fd5c601536996a376c4afbcd1dd034db7690af807120681461e82fbc0acc"}, + {file = "markdownify-0.6.1.tar.gz", hash = "sha256:31d7c13ac2ada8bfc7535a25fee6622ca720e1b5f2d4a9cbc429d167c21f886d"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +more-itertools = [ + {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, + {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, +] +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"}, +] +nodeenv = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] +ordered-set = [ + {file = "ordered-set-4.0.2.tar.gz", hash = "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pep8-naming = [ + {file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"}, + {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, +] +pip-licenses = [ + {file = "pip-licenses-3.5.3.tar.gz", hash = "sha256:f44860e00957b791c6c6005a3328f2d5eaeee96ddb8e7d87d4b0aa25b02252e4"}, + {file = "pip_licenses-3.5.3-py3-none-any.whl", hash = "sha256:59c148d6a03784bf945d232c0dc0e9de4272a3675acaa0361ad7712398ca86ba"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pre-commit = [ + {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, + {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, +] +psutil = [ + {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87"}, + {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af"}, + {file = "psutil-5.9.1-cp27-cp27m-win32.whl", hash = "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc"}, + {file = "psutil-5.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2"}, + {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0"}, + {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22"}, + {file = "psutil-5.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9"}, + {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8"}, + {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de"}, + {file = "psutil-5.9.1-cp310-cp310-win32.whl", hash = "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329"}, + {file = "psutil-5.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021"}, + {file = "psutil-5.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237"}, + {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453"}, + {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685"}, + {file = "psutil-5.9.1-cp36-cp36m-win32.whl", hash = "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36"}, + {file = "psutil-5.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d"}, + {file = "psutil-5.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc"}, + {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676"}, + {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4"}, + {file = "psutil-5.9.1-cp37-cp37m-win32.whl", hash = "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b"}, + {file = "psutil-5.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680"}, + {file = "psutil-5.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1"}, + {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4"}, + {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b"}, + {file = "psutil-5.9.1-cp38-cp38-win32.whl", hash = "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689"}, + {file = "psutil-5.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0"}, + {file = "psutil-5.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81"}, + {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e"}, + {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537"}, + {file = "psutil-5.9.1-cp39-cp39-win32.whl", hash = "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574"}, + {file = "psutil-5.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5"}, + {file = "psutil-5.9.1.tar.gz", hash = "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954"}, +] +ptable = [ + {file = "PTable-0.9.2.tar.gz", hash = "sha256:aa7fc151cb40f2dabcd2275ba6f7fd0ff8577a86be3365cd3fb297cbe09cc292"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycares = [ + {file = "pycares-4.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d83f193563b42360528167705b1c7bb91e2a09f990b98e3d6378835b72cd5c96"}, + {file = "pycares-4.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b03f69df69f0ab3bfb8dbe54444afddff6ff9389561a08aade96b4f91207a655"}, + {file = "pycares-4.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3b78bdee2f2f1351d5fccc2d1b667aea2d15a55d74d52cb9fd5bea8b5e74c4dc"}, + {file = "pycares-4.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f05223de13467bb26f9a1594a1799ce2d08ad8ea241489fecd9d8ed3bbbfc672"}, + {file = "pycares-4.2.1-cp310-cp310-win32.whl", hash = "sha256:1f37f762414680063b4dfec5be809a84f74cd8e203d939aaf3ba9c807a9e7013"}, + {file = "pycares-4.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:1a9506d496efeb809a1b63647cb2f3f33c67fcf62bf80a2359af692fef2c1755"}, + {file = "pycares-4.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2fd53eb5b441c4f6f9c78d7900e05883e9998b34a14b804be4fc4c6f9fea89f3"}, + {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:061dd4c80fec73feb150455b159704cd51a122f20d36790033bd6375d4198579"}, + {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a521d7f54f3e52ded4d34c306ba05cfe9eb5aaa2e5aaf83c96564b9369495588"}, + {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:99e00e397d07a79c9f43e4303e67f4f97bcabd013bda0d8f2d430509b7aef8a0"}, + {file = "pycares-4.2.1-cp36-cp36m-win32.whl", hash = "sha256:d9cd826d8e0c270059450709bff994bfeb072f79d82fd3f11c701690ff65d0e7"}, + {file = "pycares-4.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f8e6942965465ca98e212376c4afb9aec501d8129054929744b2f4a487c8c14b"}, + {file = "pycares-4.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e75cbd4d3b3d9b02bba6e170846e39893a825e7a5fb1b96728fc6d7b964f8945"}, + {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2e8ec4c8e07c986b70a3cc8f5b297c53b08ac755e5b9797512002a466e2de86"}, + {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5333b51ef4ff3e8973b4a1b57cad5ada13e15552445ee3cd74bd77407dec9d44"}, + {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2113529004df4894783eaa61e9abc3a680756b6f033d942f2800301ae8c71c29"}, + {file = "pycares-4.2.1-cp37-cp37m-win32.whl", hash = "sha256:e7a95763cdc20cf9ec357066e656ea30b8de6b03de6175cbb50890e22aa01868"}, + {file = "pycares-4.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a901776163a04de5d67c42bd63a287cff9cb05fc041668ad1681fe3daa36445"}, + {file = "pycares-4.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:66b5390a4885a578e687d3f2683689c35e1d4573f4d0ecf217431f7bb55c49a0"}, + {file = "pycares-4.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15dd5cf21bc73ad539e8aabf7afe370d1df8af7bc6944cd7298f3bfef0c1a27c"}, + {file = "pycares-4.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ee625d7571039038bca51ae049b047cbfcfc024b302aae6cc53d5d9aa8648a8"}, + {file = "pycares-4.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:396ee487178e9de06ca4122a35a157474db3ce0a0db6038a31c831ebb9863315"}, + {file = "pycares-4.2.1-cp38-cp38-win32.whl", hash = "sha256:e4dc37f732f7110ca6368e0128cbbd0a54f5211515a061b2add64da2ddb8e5ca"}, + {file = "pycares-4.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:3636fccf643c5192c34ee0183c514a2d09419e3a76ca2717cef626638027cb21"}, + {file = "pycares-4.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6724573e830ea2345f4bcf0f968af64cc6d491dc2133e9c617f603445dcdfa58"}, + {file = "pycares-4.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dbfcacbde6c21380c412c13d53ea44b257dea3f7b9d80be2c873bb20e21fee"}, + {file = "pycares-4.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8a46839da642b281ac5f56d3c6336528e128b3c41eab9c5330d250f22325e9d"}, + {file = "pycares-4.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9b05c2cec644a6c66b55bcf6c24d4dfdaf2f7205b16e5c4ceee31db104fac958"}, + {file = "pycares-4.2.1-cp39-cp39-win32.whl", hash = "sha256:8bd6ed3ad3a5358a635c1acf5d0f46be9afb095772b84427ff22283d2f31db1b"}, + {file = "pycares-4.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:fbd53728d798d07811898e11991e22209229c090eab265a53d12270b95d70d1a"}, + {file = "pycares-4.2.1.tar.gz", hash = "sha256:735b4f75fd0f595c4e9184da18cd87737f46bc81a64ea41f4edce2b6b68d46d2"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +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.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +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-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"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +python-dotenv = [ + {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, + {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, +] +python-frontmatter = [ + {file = "python-frontmatter-1.0.0.tar.gz", hash = "sha256:e98152e977225ddafea6f01f40b4b0f1de175766322004c826ca99842d19a7cd"}, + {file = "python_frontmatter-1.0.0-py3-none-any.whl", hash = "sha256:766ae75f1b301ffc5fe3494339147e0fd80bc3deff3d7590a93991978b579b08"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +rapidfuzz = [ + {file = "rapidfuzz-2.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b306b4a1d42a8dfd5f3daff9a82853f1541e5c74a2ec34515a5e5cd51f3c7307"}, + {file = "rapidfuzz-2.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ee380254d8b29d0b0f47a020e7f16375a4d97164b8071b3f94d5c684d744093"}, + {file = "rapidfuzz-2.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac993b8760c5308d885c300355e2c537daf0696ebc5d30436af83818978e661c"}, + {file = "rapidfuzz-2.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06a394e475316aeddbf4bf9691aabf4825f8c1acf87b49abbb7b9dad7e555ae"}, + {file = "rapidfuzz-2.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79883fcfc3e550b356d45ac2bf1af391161f9ddb64b1ed504f9a94086b824709"}, + {file = "rapidfuzz-2.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d44d74ace68b3ec6dee4501188c74f124da8c940877989baf9f672d51368e171"}, + {file = "rapidfuzz-2.0.7-cp310-cp310-win32.whl", hash = "sha256:9ec9fd78d40f392cd4ce91dbb17477cd07740d0cb0b7bf44e9ab67c16ee3d5ce"}, + {file = "rapidfuzz-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:7983ed01b0ac5343bea4d737024576a86a8c68f3c8d811498eb0facf8d3bafc1"}, + {file = "rapidfuzz-2.0.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:49fd3d2a789abc30c811d6ed81db1c5f143caf5e975720bf9ab62c920253d5e9"}, + {file = "rapidfuzz-2.0.7-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:636489517bbd0786f300948f8eba59635f2fb781ecbc2ed19deba3426ee32ab6"}, + {file = "rapidfuzz-2.0.7-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6a47418b86a6b8267a89f253e2b14f9aa8b4b559141b15f8c8a9769d19b109"}, + {file = "rapidfuzz-2.0.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe01ca2cbdb2aee6f80c1fc3a82fa69ee9ef9c44f085a725113b5d12209e05d"}, + {file = "rapidfuzz-2.0.7-cp36-cp36m-win32.whl", hash = "sha256:8157406a1b44cd742d65c65ca8345e47fcc8642148a970626b886fb52b3abd1d"}, + {file = "rapidfuzz-2.0.7-cp36-cp36m-win_amd64.whl", hash = "sha256:3062ea2a0481196376e364470c682d5ebc22eb5d4c114350f05f079119ea61b8"}, + {file = "rapidfuzz-2.0.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cbfc3fcbbd00edf7f917ad0d6bf46350c64a9910c14d05e1936d436170f2531d"}, + {file = "rapidfuzz-2.0.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae15eb44e014101b208c97a253d850d6fb4a8465f3c9ee8be3508b03135ad0e7"}, + {file = "rapidfuzz-2.0.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9224115aae07d42b9250d8ca58d5568cab2ddd8720c551aa7de9dcec661ee86"}, + {file = "rapidfuzz-2.0.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42bc2cf64ebbf2a80e6fd03353679de17118a431dce358cfadc7cdb72ac9510a"}, + {file = "rapidfuzz-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:34416ee6265dfa1415e9f10c7dafe6a85296117f534f67d00021eeaa661c8d9e"}, + {file = "rapidfuzz-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:4044ef50f020f16f99b5979784b648b7ab90cd6bd0d275359818a2c155f9c01d"}, + {file = "rapidfuzz-2.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1332fb51345e431ba39e075c3dbc222bb9770f0e73c097c7a65c8c2ea331004c"}, + {file = "rapidfuzz-2.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ac560a603d0d1b9d70cc0a376d1adf57ece4195e61351d410e0c7b0fa280cbe"}, + {file = "rapidfuzz-2.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0fd757a38e14f247d929af7df6762aee2082f7a6882c85a31f17b09a450bbb5e"}, + {file = "rapidfuzz-2.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723a48d5937e4a558fb5df553b3d0e0b3cc05de7f7a8d43a920682b796010ab5"}, + {file = "rapidfuzz-2.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c9b344e3f69c5b69ae0c96411d3ee1dab02ec49124471e44ce2a16f6446fa6d"}, + {file = "rapidfuzz-2.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f34f905a0e9fa01cf26b9208daac6523708f9439958397b21b95c6c4fe508b"}, + {file = "rapidfuzz-2.0.7-cp38-cp38-win32.whl", hash = "sha256:a95a45939cbd035c2d4779765a81485215a12fa5f1b912c2738374fad93e753d"}, + {file = "rapidfuzz-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:14234ecc57e1799e24c9dcd230bba02630c4f38ca60c0eb075452313da8e0e95"}, + {file = "rapidfuzz-2.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9959374974fb96d3941334f5f8caeea971ea9718279514748c53d381146c5a7"}, + {file = "rapidfuzz-2.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2a988b5ff46823e0d5e14b4a1cce3ef13024009115df61d1d3b7ba14678f421"}, + {file = "rapidfuzz-2.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:603d179205972ebb5b01e7a84ead465d08813d50401216d5cc81fc2589e2c957"}, + {file = "rapidfuzz-2.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a934734aa247f57c683932ae0d38653063b2d97540598b551294b40ff242bd62"}, + {file = "rapidfuzz-2.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c42064174035f3633f4a815c38a76514875ca8531fac3f992202a41d1f338a41"}, + {file = "rapidfuzz-2.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46a46b8bab2ceee4877dfb281e94a43197b118d96cb04325e07540f7f9c57324"}, + {file = "rapidfuzz-2.0.7-cp39-cp39-win32.whl", hash = "sha256:1f892f3dd0acfbc2ba0b90d72cac42dd468ac9a8f7ac2179c91c29c22a4f7960"}, + {file = "rapidfuzz-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:233024373cb77dc2ef510b5fccac0429edb3294ea631ad777a7e3ff614501578"}, + {file = "rapidfuzz-2.0.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be8121175e7096062a312b73823385389635c4dec50a9e0496b29c4ba0b50362"}, + {file = "rapidfuzz-2.0.7-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad5282cf9921c6dbfe1c58e5af05c3014eabc20afd8fafcc0e6a56e9263875a0"}, + {file = "rapidfuzz-2.0.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67d650e25a7c281127865cc50c3588d5319200c8a11837df51ab3eead7cf066"}, + {file = "rapidfuzz-2.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07ccd298a24de2dadead47e75f23ff747ed3ee551964a8401ccae31a577cebb1"}, + {file = "rapidfuzz-2.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1e70ec13c00a9f28cce76a29eb5c4e6aeb5dadb9ddb35b74dfe05d503c09a4a"}, + {file = "rapidfuzz-2.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c47fda63c0d9d8275b319cdc226f96b3f1c16a395409442bff566b6de6b7cac9"}, + {file = "rapidfuzz-2.0.7.tar.gz", hash = "sha256:93bf42784fd74ebf1a8e89ca1596e9bea7f3ac4a61b825ecc6eb2d9893ad6844"}, +] +redis = [ + {file = "redis-4.3.1-py3-none-any.whl", hash = "sha256:84316970995a7adb907a56754d2b92d88fc2d252963dc5ac34c88f0f1a22c25d"}, + {file = "redis-4.3.1.tar.gz", hash = "sha256:94b617b4cd296e94991146f66fc5559756fbefe9493604f0312e4d3298ac63e9"}, +] +regex = [ + {file = "regex-2022.3.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42eb13b93765c6698a5ab3bcd318d8c39bb42e5fa8a7fcf7d8d98923f3babdb1"}, + {file = "regex-2022.3.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9beb03ff6fe509d6455971c2489dceb31687b38781206bcec8e68bdfcf5f1db2"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0a5a1fdc9f148a8827d55b05425801acebeeefc9e86065c7ac8b8cc740a91ff"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb374a2a4dba7c4be0b19dc7b1adc50e6c2c26c3369ac629f50f3c198f3743a4"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c33ce0c665dd325200209340a88438ba7a470bd5f09f7424e520e1a3ff835b52"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04c09b9651fa814eeeb38e029dc1ae83149203e4eeb94e52bb868fadf64852bc"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab5d89cfaf71807da93c131bb7a19c3e19eaefd613d14f3bce4e97de830b15df"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e2630ae470d6a9f8e4967388c1eda4762706f5750ecf387785e0df63a4cc5af"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:df037c01d68d1958dad3463e2881d3638a0d6693483f58ad41001aa53a83fcea"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:940570c1a305bac10e8b2bc934b85a7709c649317dd16520471e85660275083a"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f63877c87552992894ea1444378b9c3a1d80819880ae226bb30b04789c0828c"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3e265b388cc80c7c9c01bb4f26c9e536c40b2c05b7231fbb347381a2e1c8bf43"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:058054c7a54428d5c3e3739ac1e363dc9347d15e64833817797dc4f01fb94bb8"}, + {file = "regex-2022.3.15-cp310-cp310-win32.whl", hash = "sha256:76435a92e444e5b8f346aed76801db1c1e5176c4c7e17daba074fbb46cb8d783"}, + {file = "regex-2022.3.15-cp310-cp310-win_amd64.whl", hash = "sha256:174d964bc683b1e8b0970e1325f75e6242786a92a22cedb2a6ec3e4ae25358bd"}, + {file = "regex-2022.3.15-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e1d8ed9e61f37881c8db383a124829a6e8114a69bd3377a25aecaeb9b3538f8"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b52771f05cff7517f7067fef19ffe545b1f05959e440d42247a17cd9bddae11b"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:673f5a393d603c34477dbad70db30025ccd23996a2d0916e942aac91cc42b31a"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8923e1c5231549fee78ff9b2914fad25f2e3517572bb34bfaa3aea682a758683"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764e66a0e382829f6ad3bbce0987153080a511c19eb3d2f8ead3f766d14433ac"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd00859291658fe1fda48a99559fb34da891c50385b0bfb35b808f98956ef1e7"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa2ce79f3889720b46e0aaba338148a1069aea55fda2c29e0626b4db20d9fcb7"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:34bb30c095342797608727baf5c8aa122406aa5edfa12107b8e08eb432d4c5d7"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:25ecb1dffc5e409ca42f01a2b2437f93024ff1612c1e7983bad9ee191a5e8828"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:aa5eedfc2461c16a092a2fabc5895f159915f25731740c9152a1b00f4bcf629a"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:7d1a6e403ac8f1d91d8f51c441c3f99367488ed822bda2b40836690d5d0059f5"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3e4d710ff6539026e49f15a3797c6b1053573c2b65210373ef0eec24480b900b"}, + {file = "regex-2022.3.15-cp36-cp36m-win32.whl", hash = "sha256:0100f0ded953b6b17f18207907159ba9be3159649ad2d9b15535a74de70359d3"}, + {file = "regex-2022.3.15-cp36-cp36m-win_amd64.whl", hash = "sha256:f320c070dea3f20c11213e56dbbd7294c05743417cde01392148964b7bc2d31a"}, + {file = "regex-2022.3.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fc8c7958d14e8270171b3d72792b609c057ec0fa17d507729835b5cff6b7f69a"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ca6dcd17f537e9f3793cdde20ac6076af51b2bd8ad5fe69fa54373b17b48d3c"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0214ff6dff1b5a4b4740cfe6e47f2c4c92ba2938fca7abbea1359036305c132f"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a98ae493e4e80b3ded6503ff087a8492db058e9c68de371ac3df78e88360b374"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b1cc70e31aacc152a12b39245974c8fccf313187eead559ee5966d50e1b5817"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4829db3737480a9d5bfb1c0320c4ee13736f555f53a056aacc874f140e98f64"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:303b15a3d32bf5fe5a73288c316bac5807587f193ceee4eb6d96ee38663789fa"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:dc7b7c16a519d924c50876fb152af661a20749dcbf653c8759e715c1a7a95b18"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce3057777a14a9a1399b81eca6a6bfc9612047811234398b84c54aeff6d536ea"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:48081b6bff550fe10bcc20c01cf6c83dbca2ccf74eeacbfac240264775fd7ecf"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dcbb7665a9db9f8d7642171152c45da60e16c4f706191d66a1dc47ec9f820aed"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c155a1a80c5e7a8fa1d9bb1bf3c8a953532b53ab1196092749bafb9d3a7cbb60"}, + {file = "regex-2022.3.15-cp37-cp37m-win32.whl", hash = "sha256:04b5ee2b6d29b4a99d38a6469aa1db65bb79d283186e8460542c517da195a8f6"}, + {file = "regex-2022.3.15-cp37-cp37m-win_amd64.whl", hash = "sha256:797437e6024dc1589163675ae82f303103063a0a580c6fd8d0b9a0a6708da29e"}, + {file = "regex-2022.3.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8afcd1c2297bc989dceaa0379ba15a6df16da69493635e53431d2d0c30356086"}, + {file = "regex-2022.3.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0066a6631c92774391f2ea0f90268f0d82fffe39cb946f0f9c6b382a1c61a5e5"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8248f19a878c72d8c0a785a2cd45d69432e443c9f10ab924c29adda77b324ae"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d1f3ea0d1924feb4cf6afb2699259f658a08ac6f8f3a4a806661c2dfcd66db1"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:794a6bc66c43db8ed06698fc32aaeaac5c4812d9f825e9589e56f311da7becd9"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d1445824944e642ffa54c4f512da17a953699c563a356d8b8cbdad26d3b7598"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f553a1190ae6cd26e553a79f6b6cfba7b8f304da2071052fa33469da075ea625"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:75a5e6ce18982f0713c4bac0704bf3f65eed9b277edd3fb9d2b0ff1815943327"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f16cf7e4e1bf88fecf7f41da4061f181a6170e179d956420f84e700fb8a3fd6b"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dad3991f0678facca1a0831ec1ddece2eb4d1dd0f5150acb9440f73a3b863907"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:491fc754428514750ab21c2d294486223ce7385446f2c2f5df87ddbed32979ae"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:6504c22c173bb74075d7479852356bb7ca80e28c8e548d4d630a104f231e04fb"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01c913cf573d1da0b34c9001a94977273b5ee2fe4cb222a5d5b320f3a9d1a835"}, + {file = "regex-2022.3.15-cp38-cp38-win32.whl", hash = "sha256:029e9e7e0d4d7c3446aa92474cbb07dafb0b2ef1d5ca8365f059998c010600e6"}, + {file = "regex-2022.3.15-cp38-cp38-win_amd64.whl", hash = "sha256:947a8525c0a95ba8dc873191f9017d1b1e3024d4dc757f694e0af3026e34044a"}, + {file = "regex-2022.3.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:591d4fba554f24bfa0421ba040cd199210a24301f923ed4b628e1e15a1001ff4"}, + {file = "regex-2022.3.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9809404528a999cf02a400ee5677c81959bc5cb938fdc696b62eb40214e3632"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f08a7e4d62ea2a45557f561eea87c907222575ca2134180b6974f8ac81e24f06"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a86cac984da35377ca9ac5e2e0589bd11b3aebb61801204bd99c41fac516f0d"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:286908cbe86b1a0240a867aecfe26a439b16a1f585d2de133540549831f8e774"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b7494df3fdcc95a1f76cf134d00b54962dd83189520fd35b8fcd474c0aa616d"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b1ceede92400b3acfebc1425937454aaf2c62cd5261a3fabd560c61e74f6da3"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0317eb6331146c524751354ebef76a7a531853d7207a4d760dfb5f553137a2a4"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c144405220c5ad3f5deab4c77f3e80d52e83804a6b48b6bed3d81a9a0238e4c"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b2e24f3ae03af3d8e8e6d824c891fea0ca9035c5d06ac194a2700373861a15c"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f2c53f3af011393ab5ed9ab640fa0876757498aac188f782a0c620e33faa2a3d"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:060f9066d2177905203516c62c8ea0066c16c7342971d54204d4e51b13dfbe2e"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:530a3a16e57bd3ea0dff5ec2695c09632c9d6c549f5869d6cf639f5f7153fb9c"}, + {file = "regex-2022.3.15-cp39-cp39-win32.whl", hash = "sha256:78ce90c50d0ec970bd0002462430e00d1ecfd1255218d52d08b3a143fe4bde18"}, + {file = "regex-2022.3.15-cp39-cp39-win_amd64.whl", hash = "sha256:c5adc854764732dbd95a713f2e6c3e914e17f2ccdc331b9ecb777484c31f73b6"}, + {file = "regex-2022.3.15.tar.gz", hash = "sha256:0a7b75cc7bb4cc0334380053e4671c560e31272c9d2d5a6c4b8e9ae2c9bd0f82"}, +] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] +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.5.8.tar.gz", hash = "sha256:38fd16a92b5ef94203db3ece10e03bdaa291481dd7e00e77a148aa0302267d47"}, + {file = "sentry_sdk-1.5.8-py2.py3-none-any.whl", hash = "sha256:32af1a57954576709242beb8c373b3dbde346ac6bd616921def29d68846fb8c3"}, +] +sgmllib3k = [ + {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] +sortedcontainers = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {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"}, +] +statsd = [ + {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, + {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, +] +taskipy = [ + {file = "taskipy-1.10.1-py3-none-any.whl", hash = "sha256:9b38333654da487b6d16de6fa330b7629d1935d1e74819ba4c5f17a1c372d37b"}, + {file = "taskipy-1.10.1.tar.gz", hash = "sha256:6fa0b11c43d103e376063e90be31d87b435aad50fb7dc1c9a2de9b60a85015ed"}, +] +testfixtures = [ + {file = "testfixtures-6.18.5-py2.py3-none-any.whl", hash = "sha256:7de200e24f50a4a5d6da7019fb1197aaf5abd475efb2ec2422fdcf2f2eb98c1d"}, + {file = "testfixtures-6.18.5.tar.gz", hash = "sha256:02dae883f567f5b70fd3ad3c9eefb95912e78ac90be6c7444b5e2f46bf572c84"}, +] +tldextract = [ + {file = "tldextract-3.2.0-py3-none-any.whl", hash = "sha256:427703b65db54644f7b81d3dcb79bf355c1a7c28a12944e5cc6787531ccc828a"}, + {file = "tldextract-3.2.0.tar.gz", hash = "sha256:3d4b6a2105600b7d0290ea237bf30b6b0dc763e50fcbe40e849a019bd6dbcbff"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, +] urllib3 = [] -virtualenv = [] -wrapt = [] -yarl = [] +virtualenv = [ + {file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"}, + {file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"}, +] +wrapt = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] +yarl = [ + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, + {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, + {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, + {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, + {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, + {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, + {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, + {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, + {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, + {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, + {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, + {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, + {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, + {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, +] diff --git a/pyproject.toml b/pyproject.toml index 241ec3e99..e7ef375a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ python = "3.9.*" "discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca42538d2e7e7a.zip"} # See https://bot-core.pythondiscord.com/ for docs. -bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.2.0.zip", extras = ["async-rediscache"]} +bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.3.1.zip", extras = ["async-rediscache"]} aiodns = "3.0.0" aiohttp = "3.8.1" -- cgit v1.2.3 From b20b72a3c06c463983df834b6ea94c916f7cb5da Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 16 Jul 2022 20:52:05 +0100 Subject: Infer the snekbox invoker from context Rather than passing around superfluous variables. --- bot/exts/utils/snekbox.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 2eef04a29..e1413a013 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -9,7 +9,7 @@ from typing import Literal, Optional, Tuple from botcore.utils import interactions, scheduling from botcore.utils.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX -from discord import AllowedMentions, HTTPException, Interaction, Member, Message, NotFound, Reaction, User, enums, ui +from discord import AllowedMentions, HTTPException, Interaction, Message, NotFound, Reaction, User, enums, ui from discord.ext.commands import Cog, Command, Context, Converter, command, errors, guild_only from bot.bot import Bot @@ -157,7 +157,6 @@ class Snekbox(Cog): def build_python_version_switcher_view( self, job_name: str, - member: Member, current_python_version: Literal["3.10", "3.11"], ctx: Context, code: str @@ -169,7 +168,7 @@ class Snekbox(Cog): alt_python_version = "3.10" view = interactions.ViewWithUserAndRoleCheck( - allowed_users=(member.id,), + allowed_users=(ctx.author.id,), allowed_roles=MODERATION_ROLES, ) view.add_item(PythonVersionSwitcherButton(job_name, alt_python_version, self, ctx, code)) @@ -352,7 +351,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, ctx.author, python_version, ctx, code) + view = self.build_python_version_switcher_view(job_name, python_version, ctx, code) response = await ctx.send(msg, allowed_mentions=allowed_mentions, view=view) log.info(f"{ctx.author}'s {job_name} job had a return code of {results['returncode']}") -- cgit v1.2.3 From 7e1f74f345f8322b0704ab5ba1ee3cf81e021c5d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 16 Jul 2022 22:26:45 +0100 Subject: Move snekbox lock error handling to a try/except The cog_command_error isn't hit when the run_job function is called from the button interaction, this means if the lock error is raiseed, it doees not get handled. --- bot/exts/utils/snekbox.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index e1413a013..f4246a145 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -10,7 +10,7 @@ from typing import Literal, Optional, Tuple from botcore.utils import interactions, scheduling 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, errors, guild_only +from discord.ext.commands import Cog, Command, Context, Converter, command, guild_only from bot.bot import Bot from bot.constants import Categories, Channels, MODERATION_ROLES, Roles, URLs @@ -454,7 +454,14 @@ class Snekbox(Cog): log.info(f"Received code from {ctx.author} for evaluation:\n{code}") while True: - response = await self.send_job(ctx, python_version, code, args=args, job_name=job_name) + try: + response = await self.send_job(ctx, python_version, code, args=args, job_name=job_name) + except LockedResourceError: + await ctx.send( + f"{ctx.author.mention} You've already got a job running - " + "please wait for it to finish!" + ) + return # Store the bot's response message id per invocation, to ensure the `wait_for`s in `continue_job` # don't trigger if the response has already been replaced by a new response. -- cgit v1.2.3 From aca54942212ae2b17bbb83da4631958e71228e14 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 16 Jul 2022 23:09:18 +0100 Subject: Use interaction.defer for snekbox version switch button This is so that we do not need to spawn the run_job call in a seperate task. This also wraps interaction.message.delete() in a NotFound suppress to cover the case where a user re-runs code and very quickly clicks the button. The log arg on send_job will stop the actual job from running in this case. --- bot/exts/utils/snekbox.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index f4246a145..c1f4775a1 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -7,7 +7,7 @@ from signal import Signals from textwrap import dedent from typing import Literal, Optional, Tuple -from botcore.utils import interactions, scheduling +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 @@ -141,10 +141,16 @@ class PythonVersionSwitcherButton(ui.Button): Use a task calling snekbox, as run_job is blocking while it waits for edit/reaction on the message. """ - # Call run_job in a task as blocking here would cause the interaction to be reported as failed by Discord's UI. - # Discord.py only ACKs the interaction after this callback finishes. - scheduling.create_task(self.snekbox_cog.run_job(self.job_name, self.ctx, self.version_to_switch_to, self.code)) - await interaction.message.delete() + # Defer response here so that the Discord UI doesn't mark this interaction as failed if the job + # takes too long to run. + await interaction.response.defer() + + with contextlib.suppress(NotFound): + # Suppress this delete to cover the case where a user re-runs code and very quickly clicks the 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) class Snekbox(Cog): -- cgit v1.2.3 From 25675a1277d463e89c1249c42a0a31d1ee0b56be Mon Sep 17 00:00:00 2001 From: Siddhesh Mhadnak Date: Sun, 17 Jul 2022 17:31:53 +0530 Subject: feat(tags): add print-return tag Since we already create the tag embed from the `embed` object in the metadata, we already have the support to add images in embeds, albeit a bit more verbose than if we had added a `media` property in the metadata containing only the URL. --- bot/resources/tags/print-return.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 bot/resources/tags/print-return.md diff --git a/bot/resources/tags/print-return.md b/bot/resources/tags/print-return.md new file mode 100644 index 000000000..e85ecc31f --- /dev/null +++ b/bot/resources/tags/print-return.md @@ -0,0 +1,10 @@ +--- +embed: + image: + url: https://cdn.discordapp.com/attachments/267659945086812160/998198889154879558/print-return.gif +--- +**Print and Return** + +Here's a handy animation demonstrating how `print` and `return` differ in behavior. + +See also: `!tags return` -- cgit v1.2.3 From b1313b60a5dc86f4f087889ded6a9fc53e2816cc Mon Sep 17 00:00:00 2001 From: Siddhesh Mhadnak Date: Sun, 17 Jul 2022 18:59:26 +0530 Subject: style(tags/print-return): set the `title` property instead of using bolded text --- bot/resources/tags/print-return.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/resources/tags/print-return.md b/bot/resources/tags/print-return.md index e85ecc31f..77dc8a21c 100644 --- a/bot/resources/tags/print-return.md +++ b/bot/resources/tags/print-return.md @@ -1,10 +1,9 @@ --- embed: + title: Print and Return image: url: https://cdn.discordapp.com/attachments/267659945086812160/998198889154879558/print-return.gif --- -**Print and Return** - Here's a handy animation demonstrating how `print` and `return` differ in behavior. See also: `!tags return` -- cgit v1.2.3 From 9ac1bda426602a9f1a056cc9896a147690747f45 Mon Sep 17 00:00:00 2001 From: Siddhesh Mhadnak Date: Sun, 17 Jul 2022 20:14:35 +0530 Subject: chore(tags/print-return): add the GIF to the repo As discussed in https://discord.com/channels/267624335836053506/635950537262759947/998235482494353508, using the raw GitHub URL for the GIF would be more reliable than the Discord CDN URL. --- bot/resources/media/print-return.gif | Bin 0 -> 119946 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 bot/resources/media/print-return.gif diff --git a/bot/resources/media/print-return.gif b/bot/resources/media/print-return.gif new file mode 100644 index 000000000..5d99329dc Binary files /dev/null and b/bot/resources/media/print-return.gif differ -- cgit v1.2.3 From 41f6e4d86cef4d1b2f5e368e65e534d58f1d489b Mon Sep 17 00:00:00 2001 From: Siddhesh Mhadnak Date: Sun, 17 Jul 2022 20:20:56 +0530 Subject: fix(tags/print-return): use the raw GitHub URL for the GIF As mentioned in the previous commit, using the raw GitHub URL would be more reliable than a Discord CDN URL. --- bot/resources/tags/print-return.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/print-return.md b/bot/resources/tags/print-return.md index 77dc8a21c..89d37053f 100644 --- a/bot/resources/tags/print-return.md +++ b/bot/resources/tags/print-return.md @@ -2,7 +2,7 @@ embed: title: Print and Return image: - url: https://cdn.discordapp.com/attachments/267659945086812160/998198889154879558/print-return.gif + 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. -- cgit v1.2.3 From c6ec29105ade727eddbc68a532e33b900927d739 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 17 Jul 2022 13:03:25 +0100 Subject: Use the view clear on timeout feature from bot-core in snekbox This will mean the buttons will be cleared from the response on interaction timeout. --- bot/exts/utils/snekbox.py | 1 + poetry.lock | 13 +++++-------- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index c1f4775a1..7c9f9843a 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -359,6 +359,7 @@ class Snekbox(Cog): allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author]) view = self.build_python_version_switcher_view(job_name, python_version, ctx, code) response = await ctx.send(msg, allowed_mentions=allowed_mentions, view=view) + view.message = response log.info(f"{ctx.author}'s {job_name} job had a return code of {results['returncode']}") return response diff --git a/poetry.lock b/poetry.lock index 4f7012215..62ebf1e66 100644 --- a/poetry.lock +++ b/poetry.lock @@ -125,7 +125,7 @@ lxml = ["lxml"] [[package]] name = "bot-core" -version = "7.3.1" +version = "7.4.0" description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." category = "main" optional = false @@ -141,7 +141,7 @@ async-rediscache = ["async-rediscache[fakeredis] (==0.2.0)"] [package.source] type = "url" -url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.3.1.zip" +url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.4.0.zip" [[package]] name = "certifi" version = "2022.6.15" @@ -478,7 +478,7 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.5.1" +version = "2.5.2" description = "File identification library for Python" category = "dev" optional = false @@ -1151,7 +1151,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "1152d8ee2e09a6d0283d82be5b1fee21a9d7bd36d206e02fcee5a4ad4ecd9523" +content-hash = "43b10dbc644c527ce55e01bc555ce98eb00a8e347409c08152668caf5276688c" [metadata.files] aiodns = [ @@ -1563,10 +1563,7 @@ 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.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, - {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, -] +identify = [] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, diff --git a/pyproject.toml b/pyproject.toml index e7ef375a5..77d8ee3d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ python = "3.9.*" "discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca42538d2e7e7a.zip"} # See https://bot-core.pythondiscord.com/ for docs. -bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.3.1.zip", extras = ["async-rediscache"]} +bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.4.0.zip", extras = ["async-rediscache"]} aiodns = "3.0.0" aiohttp = "3.8.1" -- cgit v1.2.3 From 41482168dbfef410e1b37f6845b5dc8bd6c45775 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 17 Jul 2022 13:03:50 +0100 Subject: Include what version of Python was used in snekbox output. --- bot/exts/utils/snekbox.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 7c9f9843a..8e961b67c 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -233,19 +233,19 @@ class Snekbox(Cog): return code, args @staticmethod - def get_results_message(results: dict, job_name: str) -> Tuple[str, str]: + def get_results_message(results: dict, job_name: str, python_version: Literal["3.10", "3.11"]) -> Tuple[str, str]: """Return a user-friendly message and error corresponding to the process's return code.""" stdout, returncode = results["stdout"], results["returncode"] - msg = f"Your {job_name} job has completed with return code {returncode}" + msg = f"Your {python_version} {job_name} job has completed with return code {returncode}" error = "" if returncode is None: - msg = f"Your {job_name} job has failed" + msg = f"Your {python_version} {job_name} job has failed" error = stdout.strip() elif returncode == 128 + SIGKILL: - msg = f"Your {job_name} job timed out or ran out of memory" + msg = f"Your {python_version} {job_name} job timed out or ran out of memory" elif returncode == 255: - msg = f"Your {job_name} job has failed" + msg = f"Your {python_version} {job_name} job has failed" error = "A fatal NsJail error occurred" else: # Try to append signal's name if one exists @@ -330,7 +330,7 @@ class Snekbox(Cog): """ async with ctx.typing(): results = await self.post_job(code, python_version, args=args) - msg, error = self.get_results_message(results, job_name) + msg, error = self.get_results_message(results, job_name, python_version) if error: output, paste_link = error, None -- cgit v1.2.3 From 28d91d4ab8f560593b2e5fad728e5a74c62e9e4c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 17 Jul 2022 13:16:36 +0100 Subject: Update snekbox tests to expect new output --- tests/bot/exts/utils/test_snekbox.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py index 2fff20fd9..b1f32c210 100644 --- a/tests/bot/exts/utils/test_snekbox.py +++ b/tests/bot/exts/utils/test_snekbox.py @@ -85,28 +85,28 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): def test_get_results_message(self): """Return error and message according to the eval result.""" cases = ( - ('ERROR', None, ('Your eval job has failed', 'ERROR')), - ('', 128 + snekbox.SIGKILL, ('Your eval job timed out or ran out of memory', '')), - ('', 255, ('Your eval job has failed', 'A fatal NsJail error occurred')) + ('ERROR', None, ('Your 3.11 eval job has failed', 'ERROR')), + ('', 128 + snekbox.SIGKILL, ('Your 3.11 eval job timed out or ran out of memory', '')), + ('', 255, ('Your 3.11 eval job has failed', 'A fatal NsJail error occurred')) ) for stdout, returncode, expected in cases: with self.subTest(stdout=stdout, returncode=returncode, expected=expected): - actual = self.cog.get_results_message({'stdout': stdout, 'returncode': returncode}, 'eval') + actual = self.cog.get_results_message({'stdout': stdout, 'returncode': returncode}, 'eval', '3.11') self.assertEqual(actual, expected) @patch('bot.exts.utils.snekbox.Signals', side_effect=ValueError) def test_get_results_message_invalid_signal(self, mock_signals: Mock): self.assertEqual( - self.cog.get_results_message({'stdout': '', 'returncode': 127}, 'eval'), - ('Your eval job has completed with return code 127', '') + self.cog.get_results_message({'stdout': '', 'returncode': 127}, 'eval', '3.11'), + ('Your 3.11 eval job has completed with return code 127', '') ) @patch('bot.exts.utils.snekbox.Signals') def test_get_results_message_valid_signal(self, mock_signals: Mock): mock_signals.return_value.name = 'SIGTEST' self.assertEqual( - self.cog.get_results_message({'stdout': '', 'returncode': 127}, 'eval'), - ('Your eval job has completed with return code 127 (SIGTEST)', '') + self.cog.get_results_message({'stdout': '', 'returncode': 127}, 'eval', '3.11'), + ('Your 3.11 eval job has completed with return code 127 (SIGTEST)', '') ) def test_get_status_emoji(self): @@ -245,7 +245,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) self.cog.get_status_emoji.assert_called_once_with({'stdout': '', 'returncode': 0}) - self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0}, 'eval') + self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0}, 'eval', '3.11') self.cog.format_output.assert_called_once_with('') async def test_send_job_with_paste_link(self): @@ -275,7 +275,9 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) self.cog.get_status_emoji.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}) - self.cog.get_results_message.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}, 'eval') + self.cog.get_results_message.assert_called_once_with( + {'stdout': 'Way too long beard', 'returncode': 0}, 'eval', '3.11' + ) self.cog.format_output.assert_called_once_with('Way too long beard') async def test_send_job_with_non_zero_eval(self): @@ -303,7 +305,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) self.cog.get_status_emoji.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) - self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}, 'eval') + self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}, 'eval', '3.11') self.cog.format_output.assert_not_called() @patch("bot.exts.utils.snekbox.partial") -- cgit v1.2.3 From 76caa093b9943549de6b05d8610cd1c10da2717d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 17 Jul 2022 13:17:45 +0100 Subject: Start 3.11 snekbox container by default Since snekbox uses 3.11 by default, it makes sense for this one to be started by default, and the 3.10 container to be opt-in. --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f0d3f934f..f7759566b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,6 +61,8 @@ services: ports: - "127.0.0.1:8060:8060" privileged: true + profiles: + - "3.10" snekbox-311: << : *logging @@ -71,8 +73,6 @@ services: ports: - "127.0.0.1:8065:8060" privileged: true - profiles: - - "3.11" web: << : *logging -- cgit v1.2.3 From 4638f2af5ed4226c802048c2224dc70b8a97e9f5 Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 14 Jul 2022 20:12:34 +0100 Subject: Update `!modpings off` confirmation to use a discord timestamp. --- bot/exts/moderation/modpings.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index bdefea91b..7c8e4ac13 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -5,15 +5,15 @@ 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 Embed, Member +from discord import Member from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot -from bot.constants import Colours, Emojis, Guild, Icons, MODERATION_ROLES, Roles +from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles from bot.converters import Expiry from bot.log import get_logger -from bot.utils import time from bot.utils.members import get_or_fetch_member +from bot.utils.time import TimestampFormats, discord_timestamp log = get_logger(__name__) @@ -177,9 +177,10 @@ class ModPings(Cog): self._role_scheduler.cancel(mod.id) self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) - embed = Embed(timestamp=duration, colour=Colours.bright_green) - embed.set_footer(text="Moderators role has been removed until", icon_url=Icons.green_checkmark) - await ctx.send(embed=embed) + await ctx.send( + f"{Emojis.check_mark} Moderators role has been removed " + f"until {discord_timestamp(duration, format=TimestampFormats.DAY_TIME)}." + ) @modpings_group.command(name='on') @has_any_role(*MODERATION_ROLES) @@ -239,8 +240,8 @@ class ModPings(Cog): await ctx.send( f"{Emojis.ok_hand} {ctx.author.mention} Scheduled mod pings from " - f"{time.discord_timestamp(start, time.TimestampFormats.TIME)} to " - f"{time.discord_timestamp(end, time.TimestampFormats.TIME)}!" + f"{discord_timestamp(start, TimestampFormats.TIME)} to " + f"{discord_timestamp(end, TimestampFormats.TIME)}!" ) @schedule_modpings.command(name='delete', aliases=('del', 'd')) -- cgit v1.2.3 From a6ae842fec87e0467de91438c1c4ac1b1a4a2684 Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 25 Jul 2022 22:20:50 +0100 Subject: Add ability to delete multiple reminders --- bot/exts/utils/reminders.py | 71 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 45cddd7a2..a08ba8e75 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -5,14 +5,18 @@ 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 bot.bot import Bot -from bot.constants import Guild, Icons, MODERATION_ROLES, POSITIVE_REPLIES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES +from bot.constants import ( + Guild, Icons, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES +) from bot.converters import Duration, UnambiguousUser +from bot.errors import LockedResourceError from bot.log import get_logger from bot.pagination import LinePaginator from bot.utils import time @@ -450,35 +454,76 @@ class Reminders(Cog): ) await self._reschedule_reminder(reminder) - @remind_group.command("delete", aliases=("remove", "cancel")) @lock_arg(LOCK_NAMESPACE, "id_", raise_error=True) - async def delete_reminder(self, ctx: Context, id_: int) -> None: - """Delete one of your active reminders.""" - if not await self._can_modify(ctx, id_): - return + async def _delete_reminder(self, ctx: Context, id_: int) -> bool: + """Acquires a lock on `id_` and returns `True` if reminder is deleted, otherwise `False`.""" + if not await self._can_modify(ctx, id_, send_on_denial=False): + return False await self.bot.api_client.delete(f"bot/reminders/{id_}") self.scheduler.cancel(id_) + return True - await self._send_confirmation( - ctx, - on_success="That reminder has been deleted successfully!", - reminder_id=id_ + @remind_group.command("delete", aliases=("remove", "cancel")) + async def delete_reminder(self, ctx: Context, ids: Greedy[int]) -> None: + """Delete one of your active reminders.""" + deleted_ids = [] + for id_ in ids: + try: + reminder_deleted = await self._delete_reminder(ctx, id_) + except LockedResourceError: + continue + else: + if reminder_deleted: + deleted_ids.append(str(id_)) + + if deleted_ids: + colour = discord.Colour.green() + title = random.choice(POSITIVE_REPLIES) + deletion_message = f"Successfully deleted the following reminder(s): {', '.join(deleted_ids)}" + + if len(deleted_ids) != len(ids): + deletion_message += ( + "\n\nThe other reminder(s) could not be deleted as they're either locked, " + "belong to someone else, or don't exist." + ) + else: + colour = discord.Colour.red() + title = random.choice(NEGATIVE_REPLIES) + deletion_message = ( + "Could not delete the reminder(s) as they're either locked, " + "belong to someone else, or don't exist." + ) + + embed = discord.Embed( + description=deletion_message, + colour=colour, + title=title ) + await ctx.send(embed=embed) - async def _can_modify(self, ctx: Context, reminder_id: t.Union[str, int]) -> bool: + async def _can_modify(self, ctx: Context, reminder_id: t.Union[str, int], send_on_denial: bool = True) -> bool: """ Check whether the reminder can be modified by the ctx author. The check passes when the user is an admin, or if they created the reminder. """ + try: + api_response = await self.bot.api_client.get(f"bot/reminders/{reminder_id}") + except ResponseCodeError as e: + # Override error-handling so that a 404 message isn't sent to Discord when `send_on_denial` is `False` + if not send_on_denial: + if e.status == 404: + return False + raise e + if await has_any_role_check(ctx, Roles.admins): return True - api_response = await self.bot.api_client.get(f"bot/reminders/{reminder_id}") if not api_response["author"] == ctx.author.id: log.debug(f"{ctx.author} is not the reminder author and does not pass the check.") - await send_denial(ctx, "You can't modify reminders of other users!") + if send_on_denial: + await send_denial(ctx, "You can't modify reminders of other users!") return False log.debug(f"{ctx.author} is the reminder author and passes the check.") -- cgit v1.2.3 From f7bff3d0c2e9711e147e327f44169e4718535f66 Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 25 Jul 2022 22:21:41 +0100 Subject: Display mentions instead of name attribute in `!reminder list` --- bot/exts/utils/reminders.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index a08ba8e75..51fdd7873 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -352,8 +352,8 @@ class Reminders(Cog): expiry = time.format_relative(remind_at) mentions = ", ".join([ - # Both Role and User objects have the `name` attribute - mention.name async for mention in self.get_mentionables(mentions) + # Both Role and User objects have the `mention` attribute + mentionable.mention async for mentionable in self.get_mentionables(mentions) ]) mention_string = f"\n**Mentions:** {mentions}" if mentions else "" @@ -381,7 +381,6 @@ class Reminders(Cog): lines, ctx, embed, max_lines=3, - empty=True ) @remind_group.group(name="edit", aliases=("change", "modify"), invoke_without_command=True) -- cgit v1.2.3 From 40ab60fdf85f44b08f0a9316a22e1b0bef6e0646 Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 25 Jul 2022 23:14:27 +0100 Subject: Allow referencing message as argument to `!remind edit content` --- bot/exts/utils/reminders.py | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 45cddd7a2..f33a9f448 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -209,6 +209,29 @@ class Reminders(Cog): log.debug(f"Deleting reminder #{reminder['id']} (the user has been reminded).") await self.bot.api_client.delete(f"bot/reminders/{reminder['id']}") + @staticmethod + async def try_get_content_from_reply(ctx: Context) -> t.Optional[str]: + """ + Attempts to get content from the referenced message, if applicable. + + Differs from botcore.utils.commands.clean_text_or_reply as allows for embeds. + """ + content = None + if reference := ctx.message.reference: + if isinstance((resolved_message := reference.resolved), discord.Message): + content = resolved_message.content + + # If we weren't able to get the content of a replied message + if content is None: + await send_denial(ctx, "Your reminder must have a content and/or reply to a message.") + return + + # If the replied message has no content (e.g. only attachments/embeds) + if content == "": + content = "See referenced message." + + return content + @group(name="remind", aliases=("reminder", "reminders", "remindme"), invoke_without_command=True) async def remind_group( self, ctx: Context, mentions: Greedy[ReminderMention], expiration: Duration, *, content: t.Optional[str] = None @@ -282,18 +305,11 @@ class Reminders(Cog): # If `content` isn't provided then we try to get message content of a replied message if not content: - if reference := ctx.message.reference: - if isinstance((resolved_message := reference.resolved), discord.Message): - content = resolved_message.content - # If we weren't able to get the content of a replied message - if content is None: - await send_denial(ctx, "Your reminder must have a content and/or reply to a message.") + content = await self.try_get_content_from_reply(ctx) + if not content: + # Couldn't get content from reply return - # If the replied message has no content (e.g. only attachments/embeds) - if content == "": - content = "See referenced message." - # Now we can attempt to actually set the reminder. reminder = await self.bot.api_client.post( 'bot/reminders', @@ -417,8 +433,13 @@ class Reminders(Cog): await self.edit_reminder(ctx, id_, {'expiration': expiration.isoformat()}) @edit_reminder_group.command(name="content", aliases=("reason",)) - async def edit_reminder_content(self, ctx: Context, id_: int, *, content: str) -> None: + async def edit_reminder_content(self, ctx: Context, id_: int, *, content: t.Optional[str] = None) -> None: """Edit one of your reminder's content.""" + if not content: + content = await self.try_get_content_from_reply(ctx) + if not content: + # Couldn't get content from reply + return await self.edit_reminder(ctx, id_, {"content": content}) @edit_reminder_group.command(name="mentions", aliases=("pings",)) -- cgit v1.2.3 From 0be3332904690730589113b99198f2f3fc2f1091 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 03:42:31 -0400 Subject: Added `DurationOrExpiry` type union --- bot/converters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/converters.py b/bot/converters.py index 5800ea044..e97a25bdd 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -574,5 +574,6 @@ if t.TYPE_CHECKING: Infraction = t.Optional[dict] # noqa: F811 Expiry = t.Union[Duration, ISODateTime] +DurationOrExpiry = t.Union[DurationDelta, ISODateTime] MemberOrUser = t.Union[discord.Member, discord.User] UnambiguousMemberOrUser = t.Union[UnambiguousMember, UnambiguousUser] -- cgit v1.2.3 From 0921838112120355aed0936923983b545320edef Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 04:31:05 -0400 Subject: Refactoring for DurationOrExpiry --- bot/exts/moderation/infraction/_utils.py | 20 ++++++++++----- bot/exts/moderation/infraction/infractions.py | 34 +++++++++++++------------- bot/exts/moderation/infraction/superstarify.py | 4 +-- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 3a2485ec2..368a637c2 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,5 +1,5 @@ import typing as t -from datetime import datetime +from datetime import datetime, timezone import arrow import discord @@ -8,7 +8,7 @@ from discord.ext.commands import Context import bot from bot.constants import Colours, Icons -from bot.converters import MemberOrUser +from bot.converters import MemberOrUser, DurationOrExpiry from bot.errors import InvalidInfractedUserError from bot.log import get_logger from bot.utils import time @@ -80,7 +80,7 @@ async def post_infraction( user: MemberOrUser, infr_type: str, reason: str, - expires_at: datetime = None, + duration_or_expiry: t.Optional[DurationOrExpiry] = None, hidden: bool = False, active: bool = True, dm_sent: bool = False, @@ -92,6 +92,8 @@ async def post_infraction( log.trace(f"Posting {infr_type} infraction for {user} to the API.") + current_time = datetime.now(tz=timezone.utc) + payload = { "actor": ctx.author.id, # Don't use ctx.message.author; antispam only patches ctx.author. "hidden": hidden, @@ -99,10 +101,16 @@ async def post_infraction( "type": infr_type, "user": user.id, "active": active, - "dm_sent": dm_sent + "dm_sent": dm_sent, + "last_applied": current_time, } - if expires_at: - payload['expires_at'] = expires_at.isoformat() + + # Parse duration or expiry + if duration_or_expiry is not None: + if isinstance(duration_or_expiry, datetime): + payload['expires_at'] = duration_or_expiry.isoformat() + else: # is relativedelta + payload['expires_at'] = (current_time + duration_or_expiry).isoformat() # Try to apply the infraction. If it fails because the user doesn't exist, try to add it. for should_post_user in (True, False): diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 46fd3381c..1818997bb 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser +from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser, DurationOrExpiry from bot.decorators import ensure_future_timestamp, respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -86,7 +86,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[Expiry] = None, + duration: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] = None ) -> None: @@ -95,7 +95,7 @@ class Infractions(InfractionScheduler, commands.Cog): If duration is specified, it temporarily bans that user for the given duration. """ - await self.apply_ban(ctx, user, reason, expires_at=duration) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) @command(aliases=("cban", "purgeban", "pban")) @ensure_future_timestamp(timestamp_arg=3) @@ -103,7 +103,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[Expiry] = None, + duration: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] = None ) -> None: @@ -115,10 +115,10 @@ class Infractions(InfractionScheduler, commands.Cog): clean_cog: t.Optional[Clean] = self.bot.get_cog("Clean") if clean_cog is None: # If we can't get the clean cog, fall back to native purgeban. - await self.apply_ban(ctx, user, reason, purge_days=1, expires_at=duration) + await self.apply_ban(ctx, user, reason, purge_days=1, duration_or_expiry=duration) return - infraction = await self.apply_ban(ctx, user, reason, expires_at=duration) + infraction = await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) if not infraction or not infraction.get("id"): # Ban was unsuccessful, quit early. await ctx.send(":x: Failed to apply ban.") @@ -168,7 +168,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[Expiry] = None, + duration: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] ) -> None: @@ -177,7 +177,7 @@ class Infractions(InfractionScheduler, commands.Cog): If duration is specified, it temporarily voice mutes that user for the given duration. """ - await self.apply_voice_mute(ctx, user, reason, expires_at=duration) + await self.apply_voice_mute(ctx, user, reason, duration_or_expiry=duration) # endregion # region: Temporary infractions @@ -187,7 +187,7 @@ class Infractions(InfractionScheduler, commands.Cog): async def tempmute( self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[Expiry] = None, + duration: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] = None ) -> None: @@ -214,7 +214,7 @@ class Infractions(InfractionScheduler, commands.Cog): if duration is None: duration = await Duration().convert(ctx, "1h") - await self.apply_mute(ctx, user, reason, expires_at=duration) + await self.apply_mute(ctx, user, reason, duration_or_expiry=duration) @command(aliases=("tban",)) @ensure_future_timestamp(timestamp_arg=3) @@ -241,7 +241,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_ban(ctx, user, reason, expires_at=duration) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) @command(aliases=("tempvban", "tvban")) async def tempvoiceban(self, ctx: Context) -> None: @@ -258,7 +258,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: Expiry, + duration: DurationOrExpiry, *, reason: t.Optional[str] ) -> None: @@ -277,7 +277,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_voice_mute(ctx, user, reason, expires_at=duration) + await self.apply_voice_mute(ctx, user, reason, duration_or_expiry=duration) # endregion # region: Permanent shadow infractions @@ -305,7 +305,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: Expiry, + duration: DurationOrExpiry, *, reason: t.Optional[str] = None ) -> None: @@ -324,7 +324,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_ban(ctx, user, reason, expires_at=duration, hidden=True) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration, hidden=True) # endregion # region: Remove infractions (un- commands) @@ -428,7 +428,7 @@ class Infractions(InfractionScheduler, commands.Cog): return None # In the case of a permanent ban, we don't need get_active_infractions to tell us if one is active - is_temporary = kwargs.get("expires_at") is not None + is_temporary = kwargs.get("duration_or_expiry") is not None active_infraction = await _utils.get_active_infraction(ctx, user, "ban", is_temporary) if active_infraction: @@ -436,7 +436,7 @@ class Infractions(InfractionScheduler, commands.Cog): log.trace("Tempban ignored as it cannot overwrite an active ban.") return None - if active_infraction.get('expires_at') is None: + if active_infraction.get("duration_or_expiry") is None: log.trace("Permaban already exists, notify.") await ctx.send(f":x: User is already permanently banned (#{active_infraction['id']}).") return None diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 0e6aaa1e7..f2aab7a92 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -10,7 +10,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Duration, Expiry +from bot.converters import Duration, DurationOrExpiry from bot.decorators import ensure_future_timestamp from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -109,7 +109,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: t.Optional[Expiry], + duration: t.Optional[DurationOrExpiry], *, reason: str = '', ) -> None: -- cgit v1.2.3 From 73a67d175283ca0525af4520018aa01f288bd242 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 04:38:18 -0400 Subject: Ran isort on imports --- bot/exts/moderation/infraction/_utils.py | 2 +- bot/exts/moderation/infraction/infractions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 368a637c2..80b65dae8 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -8,7 +8,7 @@ from discord.ext.commands import Context import bot from bot.constants import Colours, Icons -from bot.converters import MemberOrUser, DurationOrExpiry +from bot.converters import DurationOrExpiry, MemberOrUser from bot.errors import InvalidInfractedUserError from bot.log import get_logger from bot.utils import time diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 1818997bb..aff5e392f 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser, DurationOrExpiry +from bot.converters import Age, Duration, DurationOrExpiry, Expiry, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import ensure_future_timestamp, respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler -- cgit v1.2.3 From 7f153894d656d01a6221fa6520ec1df9a37bd478 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 04:54:26 -0400 Subject: Updated tests - Refactored tests for new time duration arguments --- .../exts/moderation/infraction/test_infractions.py | 11 ++++---- tests/bot/exts/moderation/infraction/test_utils.py | 29 +++++++++++++--------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 052048053..a18a4d23b 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -79,13 +79,13 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): """Should call voice mute applying function without expiry.""" self.cog.apply_voice_mute = AsyncMock() self.assertIsNone(await self.cog.voicemute(self.cog, self.ctx, self.user, reason="foobar")) - self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at=None) + self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", duration_or_expiry=None) async def test_temporary_voice_mute(self): """Should call voice mute applying function with expiry.""" self.cog.apply_voice_mute = AsyncMock() self.assertIsNone(await self.cog.tempvoicemute(self.cog, self.ctx, self.user, "baz", reason="foobar")) - self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at="baz") + self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", duration_or_expiry="baz") async def test_voice_unmute(self): """Should call infraction pardoning function.""" @@ -189,7 +189,8 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): user = MockUser() await self.cog.voicemute(self.cog, self.ctx, user, reason=None) - post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_mute", None, active=True, expires_at=None) + post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_mute", None, active=True, + duration_or_expiry=None) apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, ANY) # Test action @@ -273,7 +274,7 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): self.user, "FooBar", purge_days=1, - expires_at=None, + duration_or_expiry=None, ) async def test_cleanban_doesnt_purge_messages_if_clean_cog_available(self): @@ -285,7 +286,7 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): self.ctx, self.user, "FooBar", - expires_at=None, + duration_or_expiry=None, ) @patch("bot.exts.moderation.infraction.infractions.Age") diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 5cf02033d..4c78c0bd8 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -318,14 +318,17 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "user": self.member.id, "active": False, "expires_at": now.isoformat(), - "dm_sent": False + "dm_sent": False, + "last_applied": datetime(2020, 1, 1).isoformat(), } - self.ctx.bot.api_client.post.return_value = "foo" - actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) + # Patch the time.now(tz=timezone.utc) function to return a specific time + with patch("bot.exts.moderation.infraction._utils.datetime.now", return_value=datetime(2020, 1, 1)): + self.ctx.bot.api_client.post.return_value = "foo" + actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) - self.assertEqual(actual, "foo") - self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + self.assertEqual(actual, "foo") + self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) async def test_unknown_error_post_infraction(self): """Should send an error message to chat when a non-400 error occurs.""" @@ -356,12 +359,14 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "type": "mute", "user": self.user.id, "active": True, - "dm_sent": False + "dm_sent": False, + "last_applied": datetime(2020, 1, 1), } - self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] - - actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") - self.assertEqual(actual, "foo") - self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) - post_user_mock.assert_awaited_once_with(self.ctx, self.user) + # Patch the time.now(tz=timezone.utc) function to return a specific time + with patch("bot.exts.moderation.infraction._utils.datetime.now", return_value=datetime(2020, 1, 1)): + self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] + actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") + self.assertEqual(actual, "foo") + self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) + post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From 2670fc4027f30776eb091682c6cdd48188793163 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 05:04:38 -0400 Subject: Fixed test patches --- tests/bot/exts/moderation/infraction/test_utils.py | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 4c78c0bd8..def06932b 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -307,7 +307,8 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) - async def test_normal_post_infraction(self): + @patch("bot.exts.moderation.infraction._utils.datetime", wraps=datetime) + async def test_normal_post_infraction(self, mock_datetime): """Should return response from POST request if there are no errors.""" now = datetime.now() payload = { @@ -322,13 +323,13 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "last_applied": datetime(2020, 1, 1).isoformat(), } - # Patch the time.now(tz=timezone.utc) function to return a specific time - with patch("bot.exts.moderation.infraction._utils.datetime.now", return_value=datetime(2020, 1, 1)): - self.ctx.bot.api_client.post.return_value = "foo" - actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) + # Patch the datetime.now function to return a specific time + mock_datetime.now.return_value = datetime(2020, 1, 1) + self.ctx.bot.api_client.post.return_value = "foo" + actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) - self.assertEqual(actual, "foo") - self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + self.assertEqual(actual, "foo") + self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) async def test_unknown_error_post_infraction(self): """Should send an error message to chat when a non-400 error occurs.""" @@ -349,8 +350,9 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.assertIsNone(actual) post_user_mock.assert_awaited_once_with(self.ctx, self.user) + @patch("bot.exts.moderation.infraction._utils.datetime", wraps=datetime) @patch("bot.exts.moderation.infraction._utils.post_user", return_value="bar") - async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): + async def test_first_fail_second_success_user_post_infraction(self, post_user_mock, mock_datetime): """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" payload = { "actor": self.ctx.author.id, @@ -363,10 +365,10 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "last_applied": datetime(2020, 1, 1), } - # Patch the time.now(tz=timezone.utc) function to return a specific time - with patch("bot.exts.moderation.infraction._utils.datetime.now", return_value=datetime(2020, 1, 1)): - self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] - actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") - self.assertEqual(actual, "foo") - self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) - post_user_mock.assert_awaited_once_with(self.ctx, self.user) + # Patch the datetime.now function to return a specific time + mock_datetime.now.return_value = datetime(2020, 1, 1) + self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] + actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") + self.assertEqual(actual, "foo") + self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) + post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From c85f7a5ecb46448a235588329439545b42091006 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 05:19:30 -0400 Subject: Added isoformat for test payload --- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index def06932b..d3a908b28 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -362,7 +362,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "user": self.user.id, "active": True, "dm_sent": False, - "last_applied": datetime(2020, 1, 1), + "last_applied": datetime(2020, 1, 1).isoformat(), } # Patch the datetime.now function to return a specific time -- cgit v1.2.3 From 9d73552235b8e80f90dec06d7c0220e84734e205 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 05:20:48 -0400 Subject: Updated parameter names - Changed `duration` parameter names to `duration_or_expiry` to more accurately reflect options for help --- bot/exts/moderation/infraction/infractions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index aff5e392f..12bd084df 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Age, Duration, DurationOrExpiry, Expiry, MemberOrUser, UnambiguousMemberOrUser +from bot.converters import Age, Duration, DurationOrExpiry, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import ensure_future_timestamp, respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -86,7 +86,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[DurationOrExpiry] = None, + duration_or_expiry: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] = None ) -> None: @@ -95,7 +95,7 @@ class Infractions(InfractionScheduler, commands.Cog): If duration is specified, it temporarily bans that user for the given duration. """ - await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration_or_expiry) @command(aliases=("cban", "purgeban", "pban")) @ensure_future_timestamp(timestamp_arg=3) @@ -222,7 +222,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: Expiry, + duration_or_expiry: DurationOrExpiry, *, reason: t.Optional[str] = None ) -> None: @@ -241,7 +241,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration_or_expiry) @command(aliases=("tempvban", "tvban")) async def tempvoiceban(self, ctx: Context) -> None: -- cgit v1.2.3 From 517d3aad25c912a0f5ae47388a0d5a9deefe9d50 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 05:36:45 -0400 Subject: Updated ban command docstring - Updated docstring to be more explicit on parameter fields --- bot/exts/moderation/infraction/infractions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 12bd084df..83a947a19 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -91,9 +91,11 @@ class Infractions(InfractionScheduler, commands.Cog): reason: t.Optional[str] = None ) -> None: """ - Permanently ban a user for the given reason and stop watching them with Big Brother. + Permanently ban a `user` for the given `reason` and stop watching them with Big Brother. - If duration is specified, it temporarily bans that user for the given duration. + If a duration is specified, it temporarily bans the `user` for the given duration. + Alternatively, an ISO 8601 timestamp representing the expiry time can be provided + for `duration_or_expiry`. """ await self.apply_ban(ctx, user, reason, duration_or_expiry=duration_or_expiry) -- cgit v1.2.3 From 14eb99bf130ab4f8a7e14d9b4a4172a969e7a1c0 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 06:11:11 -0400 Subject: Fixed tests - Corrected datetime patching --- tests/bot/exts/moderation/infraction/test_utils.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index d3a908b28..b1f23e31c 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -307,8 +307,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) - @patch("bot.exts.moderation.infraction._utils.datetime", wraps=datetime) - async def test_normal_post_infraction(self, mock_datetime): + async def test_normal_post_infraction(self): """Should return response from POST request if there are no errors.""" now = datetime.now() payload = { @@ -320,16 +319,18 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "active": False, "expires_at": now.isoformat(), "dm_sent": False, - "last_applied": datetime(2020, 1, 1).isoformat(), } - # Patch the datetime.now function to return a specific time - mock_datetime.now.return_value = datetime(2020, 1, 1) self.ctx.bot.api_client.post.return_value = "foo" actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) self.assertEqual(actual, "foo") - self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + self.ctx.bot.api_client.post.assert_awaited_once() + await_args = str(self.ctx.bot.api_client.post.await_args) + # Check existing keys present, allow for additional keys (e.g. `last_applied`) + for key, value in payload.items(): + self.assertTrue(key in await_args) + self.assertTrue(str(value) in await_args) async def test_unknown_error_post_infraction(self): """Should send an error message to chat when a non-400 error occurs.""" -- cgit v1.2.3 From f3c15ed33185c110f486c6e933ea46272c25f6db Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 06:11:23 -0400 Subject: Correct last_applied formatting --- bot/exts/moderation/infraction/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 80b65dae8..cd82f5b2e 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -102,7 +102,7 @@ async def post_infraction( "user": user.id, "active": active, "dm_sent": dm_sent, - "last_applied": current_time, + "last_applied": current_time.isoformat(), } # Parse duration or expiry -- cgit v1.2.3 From a98c82c1ec6b244868356b1b2db1daa52e0f2555 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 18:57:01 -0400 Subject: Use `last_applied` to display duration --- bot/exts/moderation/infraction/_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index cd82f5b2e..23ae517e9 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -188,9 +188,10 @@ async def notify_infraction( expires_at = "Never" duration = "Permanent" else: + origin = arrow.get(infraction["last_applied"]) expiry = arrow.get(infraction["expires_at"]) expires_at = time.format_relative(expiry) - duration = time.humanize_delta(infraction["inserted_at"], expiry, max_units=2) + duration = time.humanize_delta(origin, expiry, max_units=2) if infraction["active"]: remaining = time.humanize_delta(expiry, arrow.utcnow(), max_units=2) -- cgit v1.2.3 From b115d42195841e69e38c601f40a6a49826e7eac6 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sun, 31 Jul 2022 02:09:35 -0400 Subject: Added new expiry usage to apply - Added new usage of `last_applied` time for duration calculation in `apply_infraction` --- bot/exts/moderation/infraction/_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index c7f03b2e9..28aafec2a 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -137,7 +137,7 @@ class InfractionScheduler: infr_type = infraction["type"] icon = _utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] - expiry = time.format_with_duration(infraction["expires_at"]) + expiry = time.format_with_duration(infraction["expires_at"], infraction["last_applied"]) id_ = infraction['id'] if user_reason is None: -- cgit v1.2.3 From b699d114c5fb5810915dc85f5aa0649a0b091a79 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Mon, 1 Aug 2022 00:55:00 -0400 Subject: Added microsecond rounding for `humanize_delta` --- bot/utils/time.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index a0379c3ef..104ea026d 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -195,7 +195,11 @@ def humanize_delta( end = arrow.get(args[0]) start = arrow.get(args[1]) if len(args) == 2 else arrow.utcnow() - delta = relativedelta(end.datetime, start.datetime) + # Round microseconds + end = round_datetime(end.datetime) + start = round_datetime(start.datetime) + + delta = relativedelta(end, start) if absolute: delta = abs(delta) else: @@ -326,3 +330,14 @@ def until_expiration(expiry: Optional[Timestamp]) -> str: return "Expired" return format_relative(expiry) + + +def round_datetime(dt: datetime.datetime) -> datetime.datetime: + """ + Round a datetime object to the nearest second. + + Resulting datetime objects will have microsecond values of 0, useful for delta comparisons. + """ + if dt.microsecond >= 500000: + dt += datetime.timedelta(seconds=1) + return dt.replace(microsecond=0) -- cgit v1.2.3 From 090824f125e1e6879d179b00bd3297d829c0920a Mon Sep 17 00:00:00 2001 From: ionite34 Date: Mon, 1 Aug 2022 01:00:00 -0400 Subject: Infraction duration fallback if no `last_applied` field --- bot/exts/moderation/infraction/_scheduler.py | 8 +++++++- bot/exts/moderation/infraction/_utils.py | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 28aafec2a..cfb585bf5 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -137,9 +137,15 @@ class InfractionScheduler: infr_type = infraction["type"] icon = _utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] - expiry = time.format_with_duration(infraction["expires_at"], infraction["last_applied"]) id_ = infraction['id'] + if "last_applied" in infraction: + origin = infraction["last_applied"] + else: # Fallback for previous API versions without `last_applied` + log.trace(f"No last_applied for infraction {id_}, using inserted_at time.") + origin = infraction["inserted_at"] + expiry = time.format_with_duration(infraction["expires_at"], origin) + if user_reason is None: user_reason = reason diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 23ae517e9..470ce05e1 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -188,7 +188,12 @@ async def notify_infraction( expires_at = "Never" duration = "Permanent" else: - origin = arrow.get(infraction["last_applied"]) + if "last_applied" in infraction: + origin = infraction["last_applied"] + else: # Fallback for previous API versions without `last_applied` + log.trace(f"No last_applied for infraction {infraction}, using inserted_at time.") + origin = infraction["inserted_at"] + origin = arrow.get(origin) expiry = arrow.get(infraction["expires_at"]) expires_at = time.format_relative(expiry) duration = time.humanize_delta(origin, expiry, max_units=2) -- cgit v1.2.3 From 2c5b85bba7f245c61905d5272444fc83329e3c1e Mon Sep 17 00:00:00 2001 From: ionite34 Date: Wed, 3 Aug 2022 13:50:58 -0400 Subject: Updated `purge` to require >1 users --- bot/exts/moderation/clean.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index d7c2c7673..a6104d91c 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -7,7 +7,7 @@ from datetime import datetime from itertools import takewhile from typing import Callable, Iterable, Literal, Optional, TYPE_CHECKING, Union -from discord import Colour, Message, NotFound, TextChannel, Thread, User, errors +from discord import Colour, Embed, Message, NotFound, TextChannel, Thread, User, errors from discord.ext.commands import Cog, Context, Converter, Greedy, command, group, has_any_role from discord.ext.commands.converter import TextChannelConverter from discord.ext.commands.errors import BadArgument @@ -627,12 +627,18 @@ class Clean(Cog): @command() async def purge(self, ctx: Context, users: Greedy[User], age: Optional[Union[Age, ISODateTime]] = None) -> None: """ - Clean messages of users from all public channels up to a certain message age (10 minutes by default). + Clean messages of `users` from all public channels up to a certain message `age` (10 minutes by default). - The age is *exclusive*, meaning that `10s` won't delete a message exactly 10 seconds old. + Requires 1 or more users to be specified. For channel-based cleaning, use `clean` instead. + + The `age` is *exclusive*, meaning that `10s` won't delete a message exactly 10 seconds old. """ + if not users: + raise BadArgument("At least one user must be specified.") + if age is None: age = await Age().convert(ctx, "10M") + await self._clean_messages(ctx, channels="*", users=users, first_limit=age) # endregion -- cgit v1.2.3 From 2d16b21988dc71992c325bbc3e8f87284f3d5918 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Wed, 3 Aug 2022 13:56:02 -0400 Subject: Removed unused import --- bot/exts/moderation/clean.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index a6104d91c..8f2a1e3db 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -7,7 +7,7 @@ from datetime import datetime from itertools import takewhile from typing import Callable, Iterable, Literal, Optional, TYPE_CHECKING, Union -from discord import Colour, Embed, Message, NotFound, TextChannel, Thread, User, errors +from discord import Colour, Message, NotFound, TextChannel, Thread, User, errors from discord.ext.commands import Cog, Context, Converter, Greedy, command, group, has_any_role from discord.ext.commands.converter import TextChannelConverter from discord.ext.commands.errors import BadArgument -- cgit v1.2.3 From 87de12cfdcf9e5028e62f24bbbdd357496a4683f Mon Sep 17 00:00:00 2001 From: ionite34 Date: Wed, 3 Aug 2022 14:32:37 -0400 Subject: Improved `purge` help message --- bot/exts/moderation/clean.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index 8f2a1e3db..748c018d2 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -631,7 +631,7 @@ class Clean(Cog): Requires 1 or more users to be specified. For channel-based cleaning, use `clean` instead. - The `age` is *exclusive*, meaning that `10s` won't delete a message exactly 10 seconds old. + `age` can be a duration or an ISO 8601 timestamp. """ if not users: raise BadArgument("At least one user must be specified.") -- cgit v1.2.3 From 0f4bc18ceb5ef5e55e003da409a3da8e3e1d9cf8 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Thu, 4 Aug 2022 14:54:09 +0100 Subject: Disable nose plugin in pytest This fixes an issue with pytest running functions called setup in test files when they shouldn't be run --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 77d8ee3d4..255710386 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,3 +84,9 @@ case_sensitive = true combine_as_imports = true line_length = 120 atomic = true + +[tool.pytest.ini_options] +# We don't use nose style tests so disable them in pytest. +# This stops pytest from running functions named `setup` in test files. +# See https://github.com/python-discord/bot/pull/2229#issuecomment-1204436420 +addopts = "-p no:nose" -- cgit v1.2.3 From 31f6b907b77aceb12eb8ef5079d007828a66784e Mon Sep 17 00:00:00 2001 From: wookie184 Date: Thu, 4 Aug 2022 17:30:59 +0100 Subject: Refactor infractions to avoid passing around coroutines This avoids warnings if they are never awaited, e.g. when functions are mocked in tests --- bot/exts/moderation/infraction/_scheduler.py | 13 +++++++------ bot/exts/moderation/infraction/infractions.py | 15 ++++++++++----- bot/exts/moderation/infraction/superstarify.py | 11 ++++++----- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index c7f03b2e9..db2ae9454 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -1,6 +1,7 @@ import textwrap import typing as t from abc import abstractmethod +from collections.abc import Callable from gettext import ngettext import arrow @@ -79,7 +80,7 @@ class InfractionScheduler: async def reapply_infraction( self, infraction: _utils.Infraction, - apply_coro: t.Optional[t.Awaitable] + action: t.Optional[Callable[[], None]] ) -> None: """Reapply an infraction if it's still active or deactivate it if less than 60 sec left.""" if infraction["expires_at"] is not None: @@ -101,7 +102,7 @@ class InfractionScheduler: # Allowing mod log since this is a passive action that should be logged. try: - await apply_coro + await action() except discord.HTTPException as e: # When user joined and then right after this left again before action completed, this can't apply roles if e.code == 10007 or e.status == 404: @@ -121,14 +122,14 @@ class InfractionScheduler: ctx: Context, infraction: _utils.Infraction, user: MemberOrUser, - action_coro: t.Optional[t.Awaitable] = None, + action: t.Optional[Callable[[], None]] = None, user_reason: t.Optional[str] = None, additional_info: str = "", ) -> bool: """ Apply an infraction to the user, log the infraction, and optionally notify the user. - `action_coro`, if not provided, will result in the infraction not getting scheduled for deletion. + `action`, if not provided, will result in the infraction not getting scheduled for deletion. `user_reason`, if provided, will be sent to the user in place of the infraction reason. `additional_info` will be attached to the text field in the mod-log embed. @@ -194,10 +195,10 @@ class InfractionScheduler: purge = infraction.get("purge", "") # Execute the necessary actions to apply the infraction on Discord. - if action_coro: + if action: log.trace(f"Awaiting the infraction #{id_} application action coroutine.") try: - await action_coro + await action() if expiry: # Schedule the expiration of the infraction. self.schedule_expiration(infraction) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 46fd3381c..8d784549f 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -52,8 +52,9 @@ class Infractions(InfractionScheduler, commands.Cog): if active_mutes: reason = f"Re-applying active mute: {active_mutes[0]['id']}" - action = member.add_roles(self._muted_role, reason=reason) + async def action() -> None: + await member.add_roles(self._muted_role, reason=reason) await self.reapply_infraction(active_mutes[0], action) # region: Permanent infractions @@ -388,7 +389,7 @@ class Infractions(InfractionScheduler, commands.Cog): log.trace(f"Attempting to kick {user} from voice because they've been muted.") await user.move_to(None, reason=reason) - await self.apply_infraction(ctx, infraction, user, action()) + await self.apply_infraction(ctx, infraction, user, action) @respect_role_hierarchy(member_arg=2) async def apply_kick(self, ctx: Context, user: Member, reason: t.Optional[str], **kwargs) -> None: @@ -406,7 +407,9 @@ class Infractions(InfractionScheduler, commands.Cog): if reason: reason = textwrap.shorten(reason, width=512, placeholder="...") - action = user.kick(reason=reason) + async def action() -> None: + await user.kick(reason=reason) + await self.apply_infraction(ctx, infraction, user, action) @respect_role_hierarchy(member_arg=2) @@ -455,7 +458,9 @@ class Infractions(InfractionScheduler, commands.Cog): if reason: reason = textwrap.shorten(reason, width=512, placeholder="...") - action = ctx.guild.ban(user, reason=reason, delete_message_days=purge_days) + async def action() -> None: + await ctx.guild.ban(user, reason=reason, delete_message_days=purge_days) + await self.apply_infraction(ctx, infraction, user, action) bb_cog: t.Optional[BigBrother] = self.bot.get_cog("Big Brother") @@ -493,7 +498,7 @@ class Infractions(InfractionScheduler, commands.Cog): await user.move_to(None, reason="Disconnected from voice to apply voice mute.") await user.remove_roles(self._voice_verified_role, reason=reason) - await self.apply_infraction(ctx, infraction, user, action()) + await self.apply_infraction(ctx, infraction, user, action) # endregion # region: Base pardon functions diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 0e6aaa1e7..02fbe7957 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -96,11 +96,12 @@ class Superstarify(InfractionScheduler, Cog): if active_superstarifies: infraction = active_superstarifies[0] - action = member.edit( - nick=self.get_nick(infraction["id"], member.id), - reason=f"Superstarified member tried to escape the prison: {infraction['id']}" - ) + async def action() -> None: + await member.edit( + nick=self.get_nick(infraction["id"], member.id), + reason=f"Superstarified member tried to escape the prison: {infraction['id']}" + ) await self.reapply_infraction(infraction, action) @command(name="superstarify", aliases=("force_nick", "star", "starify", "superstar")) @@ -175,7 +176,7 @@ class Superstarify(InfractionScheduler, Cog): ).format successful = await self.apply_infraction( - ctx, infraction, member, action(), + ctx, infraction, member, action, user_reason=user_message(reason=f'**Additional details:** {reason}\n\n' if reason else ''), additional_info=nickname_info ) -- cgit v1.2.3 From fb419bc8f0525e8296abc5017a0d316adb22d4c4 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Thu, 4 Aug 2022 17:32:12 +0100 Subject: Fix tests --- .../exts/moderation/infraction/test_infractions.py | 30 +++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 052048053..9636f0146 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -35,17 +35,20 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase): self.cog.apply_infraction = AsyncMock() self.bot.get_cog.return_value = AsyncMock() self.cog.mod_log.ignore = Mock() - self.ctx.guild.ban = Mock() + self.ctx.guild.ban = AsyncMock() await self.cog.apply_ban(self.ctx, self.target, "foo bar" * 3000) - self.ctx.guild.ban.assert_called_once_with( + self.cog.apply_infraction.assert_awaited_once_with( + self.ctx, {"foo": "bar", "purge": ""}, self.target, ANY + ) + + action = self.cog.apply_infraction.call_args.args[-1] + await action() + self.ctx.guild.ban.assert_awaited_once_with( self.target, reason=textwrap.shorten("foo bar" * 3000, 512, placeholder="..."), delete_message_days=0 ) - self.cog.apply_infraction.assert_awaited_once_with( - self.ctx, {"foo": "bar", "purge": ""}, self.target, self.ctx.guild.ban.return_value - ) @patch("bot.exts.moderation.infraction._utils.post_infraction") async def test_apply_kick_reason_truncation(self, post_infraction_mock): @@ -54,14 +57,17 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase): self.cog.apply_infraction = AsyncMock() self.cog.mod_log.ignore = Mock() - self.target.kick = Mock() + self.target.kick = AsyncMock() await self.cog.apply_kick(self.ctx, self.target, "foo bar" * 3000) - self.target.kick.assert_called_once_with(reason=textwrap.shorten("foo bar" * 3000, 512, placeholder="...")) self.cog.apply_infraction.assert_awaited_once_with( - self.ctx, {"foo": "bar"}, self.target, self.target.kick.return_value + self.ctx, {"foo": "bar"}, self.target, ANY ) + action = self.cog.apply_infraction.call_args.args[-1] + await action() + self.target.kick.assert_awaited_once_with(reason=textwrap.shorten("foo bar" * 3000, 512, placeholder="...")) + @patch("bot.exts.moderation.infraction.infractions.constants.Roles.voice_verified", new=123456) class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): @@ -141,8 +147,8 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): async def action_tester(self, action, reason: str) -> None: """Helper method to test voice mute action.""" - self.assertTrue(inspect.iscoroutine(action)) - await action + self.assertTrue(inspect.iscoroutinefunction(action)) + await action() self.user.move_to.assert_called_once_with(None, reason=ANY) self.user.remove_roles.assert_called_once_with(self.cog._voice_verified_role, reason=reason) @@ -194,8 +200,8 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): # Test action action = self.cog.apply_infraction.call_args[0][-1] - self.assertTrue(inspect.iscoroutine(action)) - await action + self.assertTrue(inspect.iscoroutinefunction(action)) + await action() async def test_voice_unmute_user_not_found(self): """Should include info to return dict when user was not found from guild.""" -- cgit v1.2.3 From 29d882065e12f9a042b14d6dfaf3161af215c953 Mon Sep 17 00:00:00 2001 From: Izan Date: Sat, 6 Aug 2022 15:49:02 +0100 Subject: Make reference message in reminders italic. --- bot/exts/utils/reminders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index f33a9f448..f0da37f3b 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -228,7 +228,7 @@ class Reminders(Cog): # If the replied message has no content (e.g. only attachments/embeds) if content == "": - content = "See referenced message." + content = "*See referenced message.*" return content -- cgit v1.2.3 From 4a9bcdf44263d4ff1d144ec22d43d3e83c3ad85c Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 6 Aug 2022 13:10:59 -0400 Subject: Changed datetime.now to arrow.utcnow - Used arrow.utcnow to reduce complexity and import --- bot/exts/moderation/infraction/_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 470ce05e1..eda56b97c 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,5 +1,5 @@ import typing as t -from datetime import datetime, timezone +from datetime import datetime import arrow import discord @@ -92,7 +92,7 @@ async def post_infraction( log.trace(f"Posting {infr_type} infraction for {user} to the API.") - current_time = datetime.now(tz=timezone.utc) + current_time = arrow.utcnow() payload = { "actor": ctx.author.id, # Don't use ctx.message.author; antispam only patches ctx.author. -- cgit v1.2.3 From 97b940a24ac90fd7ec8405b84e0b4f419c95d31a Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 6 Aug 2022 13:11:36 -0400 Subject: Removed `inserted_at` fallback Given API updates, the fallback is not needed --- bot/exts/moderation/infraction/_scheduler.py | 11 ++++------- bot/exts/moderation/infraction/_utils.py | 7 +------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index cfb585bf5..cb3f7149f 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -138,13 +138,10 @@ class InfractionScheduler: icon = _utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] id_ = infraction['id'] - - if "last_applied" in infraction: - origin = infraction["last_applied"] - else: # Fallback for previous API versions without `last_applied` - log.trace(f"No last_applied for infraction {id_}, using inserted_at time.") - origin = infraction["inserted_at"] - expiry = time.format_with_duration(infraction["expires_at"], origin) + expiry = time.format_with_duration( + infraction["expires_at"], + infraction["last_applied"] + ) if user_reason is None: user_reason = reason diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index eda56b97c..407c97251 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -188,12 +188,7 @@ async def notify_infraction( expires_at = "Never" duration = "Permanent" else: - if "last_applied" in infraction: - origin = infraction["last_applied"] - else: # Fallback for previous API versions without `last_applied` - log.trace(f"No last_applied for infraction {infraction}, using inserted_at time.") - origin = infraction["inserted_at"] - origin = arrow.get(origin) + origin = arrow.get(infraction["last_applied"]) expiry = arrow.get(infraction["expires_at"]) expires_at = time.format_relative(expiry) duration = time.humanize_delta(origin, expiry, max_units=2) -- cgit v1.2.3 From d042f29420abf3682cdae3b8a16e9b5e043ad9e5 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 6 Aug 2022 13:47:49 -0400 Subject: Refactored test to not use datetime patch - Used new method of dict subset comparison instead of datetime patching for better compat. with argument types --- tests/bot/exts/moderation/infraction/test_utils.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index b1f23e31c..5ba0f4273 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -1,7 +1,7 @@ import unittest from collections import namedtuple from datetime import datetime -from unittest.mock import AsyncMock, MagicMock, call, patch +from unittest.mock import AsyncMock, MagicMock, patch from botcore.site_api import ResponseCodeError from discord import Embed, Forbidden, HTTPException, NotFound @@ -351,11 +351,10 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.assertIsNone(actual) post_user_mock.assert_awaited_once_with(self.ctx, self.user) - @patch("bot.exts.moderation.infraction._utils.datetime", wraps=datetime) @patch("bot.exts.moderation.infraction._utils.post_user", return_value="bar") - async def test_first_fail_second_success_user_post_infraction(self, post_user_mock, mock_datetime): + async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" - payload = { + expected = { "actor": self.ctx.author.id, "hidden": False, "reason": "Test reason", @@ -363,13 +362,17 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "user": self.user.id, "active": True, "dm_sent": False, - "last_applied": datetime(2020, 1, 1).isoformat(), } - # Patch the datetime.now function to return a specific time - mock_datetime.now.return_value = datetime(2020, 1, 1) self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") self.assertEqual(actual, "foo") - self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) + await_args = self.bot.api_client.post.await_args_list + self.assertEqual(len(await_args), 2, "Expected 2 awaits") + + # Since `last_applied` is based on current time, just check if expected is a subset of payload + for args in await_args: + payload: dict = args.kwargs["json"] + self.assertEqual(payload, payload | expected) + post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From 1eff1d81a19883136501757738cd8444634be893 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Sat, 6 Aug 2022 18:51:51 +0100 Subject: Add note to docstring, fix type-hints, and update log messages --- bot/exts/moderation/infraction/_scheduler.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index db2ae9454..a44b66804 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -1,7 +1,7 @@ import textwrap import typing as t from abc import abstractmethod -from collections.abc import Callable +from collections.abc import Awaitable, Callable from gettext import ngettext import arrow @@ -80,9 +80,14 @@ class InfractionScheduler: async def reapply_infraction( self, infraction: _utils.Infraction, - action: t.Optional[Callable[[], None]] + action: t.Optional[Callable[[], Awaitable[None]]] ) -> None: - """Reapply an infraction if it's still active or deactivate it if less than 60 sec left.""" + """ + Reapply an infraction if it's still active or deactivate it if less than 60 sec left. + + Note: The `action` provided is an async function rather than a coroutine + to prevent getting a RuntimeWarning if it is not used (e.g. in mocked tests). + """ if infraction["expires_at"] is not None: # Calculate the time remaining, in seconds, for the mute. expiry = dateutil.parser.isoparse(infraction["expires_at"]) @@ -112,7 +117,7 @@ class InfractionScheduler: else: log.exception( f"Got unexpected HTTPException (HTTP {e.status}, Discord code {e.code})" - f"when awaiting {infraction['type']} coroutine for {infraction['user']}." + f"when running {infraction['type']} action for {infraction['user']}." ) else: log.info(f"Re-applied {infraction['type']} to user {infraction['user']} upon rejoining.") @@ -122,7 +127,7 @@ class InfractionScheduler: ctx: Context, infraction: _utils.Infraction, user: MemberOrUser, - action: t.Optional[Callable[[], None]] = None, + action: t.Optional[Callable[[], Awaitable[None]]] = None, user_reason: t.Optional[str] = None, additional_info: str = "", ) -> bool: @@ -133,6 +138,9 @@ class InfractionScheduler: `user_reason`, if provided, will be sent to the user in place of the infraction reason. `additional_info` will be attached to the text field in the mod-log embed. + Note: The `action` provided is an async function rather than just a coroutine + to prevent getting a RuntimeWarning if it is not used (e.g. in mocked tests). + Returns whether or not the infraction succeeded. """ infr_type = infraction["type"] @@ -196,7 +204,7 @@ class InfractionScheduler: # Execute the necessary actions to apply the infraction on Discord. if action: - log.trace(f"Awaiting the infraction #{id_} application action coroutine.") + log.trace(f"Running the infraction #{id_} application action.") try: await action() if expiry: -- cgit v1.2.3 From a309f4a2791831f975f98ddb4025323948fdf512 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 6 Aug 2022 13:57:51 -0400 Subject: Updated previous tests to use subset method --- tests/bot/exts/moderation/infraction/test_utils.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 5ba0f4273..6c9af2555 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -310,7 +310,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): async def test_normal_post_infraction(self): """Should return response from POST request if there are no errors.""" now = datetime.now() - payload = { + expected = { "actor": self.ctx.author.id, "hidden": True, "reason": "Test reason", @@ -323,14 +323,12 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.ctx.bot.api_client.post.return_value = "foo" actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) - self.assertEqual(actual, "foo") self.ctx.bot.api_client.post.assert_awaited_once() - await_args = str(self.ctx.bot.api_client.post.await_args) - # Check existing keys present, allow for additional keys (e.g. `last_applied`) - for key, value in payload.items(): - self.assertTrue(key in await_args) - self.assertTrue(str(value) in await_args) + + # Since `last_applied` is based on current time, just check if expected is a subset of payload + payload: dict = self.ctx.bot.api_client.post.await_args_list[0].kwargs["json"] + self.assertEqual(payload, payload | expected) async def test_unknown_error_post_infraction(self): """Should send an error message to chat when a non-400 error occurs.""" -- cgit v1.2.3 From e4d00ffd7f747097cb07baf54aa8b2ad72bbb010 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 13 Aug 2022 15:45:52 -0400 Subject: Corrected test use of utcnow Corrected test case to use `datetime.utcnow()` to be consistent with target --- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 6c9af2555..29dadf372 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -309,7 +309,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): async def test_normal_post_infraction(self): """Should return response from POST request if there are no errors.""" - now = datetime.now() + now = datetime.utcnow() expected = { "actor": self.ctx.author.id, "hidden": True, -- cgit v1.2.3 From 88a981e211c688a5b0dda4eb39181c505c8d14b5 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 13 Aug 2022 15:48:54 -0400 Subject: Updated infractions display for updates - Added new infraction delta calculations to updated infractions. - Updates of infraction durations now also update the `last_applied` field. - `inserted_at` is now sent by the bot client to denote the original unmodified infraction application time --- bot/exts/moderation/infraction/_utils.py | 9 +++-- bot/exts/moderation/infraction/management.py | 24 ++++++++++---- bot/utils/time.py | 49 +++++++++++++++++++++------- 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 407c97251..5e708d7fe 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -12,6 +12,7 @@ from bot.converters import DurationOrExpiry, MemberOrUser from bot.errors import InvalidInfractedUserError from bot.log import get_logger from bot.utils import time +from bot.utils.time import unpack_duration log = get_logger(__name__) @@ -102,15 +103,13 @@ async def post_infraction( "user": user.id, "active": active, "dm_sent": dm_sent, + "inserted_at": current_time.isoformat(), "last_applied": current_time.isoformat(), } - # Parse duration or expiry if duration_or_expiry is not None: - if isinstance(duration_or_expiry, datetime): - payload['expires_at'] = duration_or_expiry.isoformat() - else: # is relativedelta - payload['expires_at'] = (current_time + duration_or_expiry).isoformat() + _, expiry = unpack_duration(duration_or_expiry, current_time) + payload["expires_at"] = expiry.isoformat() # Try to apply the infraction. If it fails because the user doesn't exist, try to add it. for should_post_user in (True, False): diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index a7d7a844a..6ef382119 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -2,6 +2,7 @@ import re import textwrap import typing as t +import arrow import discord from discord.ext import commands from discord.ext.commands import Context @@ -9,7 +10,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Expiry, Infraction, MemberOrUser, Snowflake, UnambiguousUser +from bot.converters import DurationOrExpiry, Infraction, MemberOrUser, Snowflake, UnambiguousUser from bot.decorators import ensure_future_timestamp from bot.errors import InvalidInfraction from bot.exts.moderation.infraction import _utils @@ -20,6 +21,7 @@ from bot.pagination import LinePaginator from bot.utils import messages, time from bot.utils.channel import is_mod_channel from bot.utils.members import get_or_fetch_member +from bot.utils.time import unpack_duration log = get_logger(__name__) @@ -89,7 +91,7 @@ class ModManagement(commands.Cog): self, ctx: Context, infraction: Infraction, - duration: t.Union[Expiry, t.Literal["p", "permanent"], None], + duration: t.Union[DurationOrExpiry, t.Literal["p", "permanent"], None], *, reason: str = None ) -> None: @@ -129,7 +131,7 @@ class ModManagement(commands.Cog): self, ctx: Context, infraction: Infraction, - duration: t.Union[Expiry, t.Literal["p", "permanent"], None], + duration: t.Union[DurationOrExpiry, t.Literal["p", "permanent"], None], *, reason: str = None ) -> None: @@ -172,8 +174,11 @@ class ModManagement(commands.Cog): request_data['expires_at'] = None confirm_messages.append("marked as permanent") elif duration is not None: - request_data['expires_at'] = duration.isoformat() - expiry = time.format_with_duration(duration) + origin, expiry = unpack_duration(duration) + # Update `last_applied` if expiry changes. + request_data['last_applied'] = origin.isoformat() + request_data['expires_at'] = expiry.isoformat() + expiry = time.format_with_duration(expiry, origin) confirm_messages.append(f"set to expire on {expiry}") else: confirm_messages.append("expiry unchanged") @@ -380,7 +385,10 @@ class ModManagement(commands.Cog): user = infraction["user"] expires_at = infraction["expires_at"] inserted_at = infraction["inserted_at"] + last_applied = infraction["last_applied"] created = time.discord_timestamp(inserted_at) + applied = time.discord_timestamp(last_applied) + duration_edited = arrow.get(last_applied) > arrow.get(inserted_at) dm_sent = infraction["dm_sent"] # Format the user string. @@ -400,7 +408,11 @@ class ModManagement(commands.Cog): if expires_at is None: duration = "*Permanent*" else: - duration = time.humanize_delta(inserted_at, expires_at) + duration = time.humanize_delta(last_applied, expires_at) + + # Notice if infraction expiry was edited. + if duration_edited: + duration += f" (edited {applied})" # Format `dm_sent` if dm_sent is None: diff --git a/bot/utils/time.py b/bot/utils/time.py index 104ea026d..820ac2929 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -1,12 +1,18 @@ +from __future__ import annotations + import datetime import re +from copy import copy from enum import Enum from time import struct_time -from typing import Literal, Optional, Union, overload +from typing import Literal, Optional, TYPE_CHECKING, Union, overload import arrow from dateutil.relativedelta import relativedelta +if TYPE_CHECKING: + from bot.converters import DurationOrExpiry + _DURATION_REGEX = re.compile( r"((?P\d+?) ?(years|year|Y|y) ?)?" r"((?P\d+?) ?(months|month|m) ?)?" @@ -194,12 +200,8 @@ def humanize_delta( elif len(args) <= 2: end = arrow.get(args[0]) start = arrow.get(args[1]) if len(args) == 2 else arrow.utcnow() + delta = round_delta(relativedelta(end.datetime, start.datetime)) - # Round microseconds - end = round_datetime(end.datetime) - start = round_datetime(start.datetime) - - delta = relativedelta(end, start) if absolute: delta = abs(delta) else: @@ -332,12 +334,35 @@ def until_expiration(expiry: Optional[Timestamp]) -> str: return format_relative(expiry) -def round_datetime(dt: datetime.datetime) -> datetime.datetime: +def unpack_duration( + duration_or_expiry: DurationOrExpiry, + origin: Optional[Union[datetime.datetime, arrow.Arrow]] = None +) -> tuple[datetime.datetime, datetime.datetime]: + """ + Unpacks a DurationOrExpiry into a tuple of (origin, expiry). + + The `origin` defaults to the current UTC time at function call. + """ + if origin is None: + origin = datetime.datetime.now(tz=datetime.timezone.utc) + + if isinstance(origin, arrow.Arrow): + origin = origin.datetime + + if isinstance(duration_or_expiry, relativedelta): + return origin, origin + duration_or_expiry + else: + return origin, duration_or_expiry + + +def round_delta(delta: relativedelta) -> relativedelta: """ - Round a datetime object to the nearest second. + Rounds `delta` to the nearest second. - Resulting datetime objects will have microsecond values of 0, useful for delta comparisons. + Returns a copy with microsecond values of 0. """ - if dt.microsecond >= 500000: - dt += datetime.timedelta(seconds=1) - return dt.replace(microsecond=0) + delta = copy(delta) + if delta.microseconds >= 500000: + delta += relativedelta(seconds=1) + delta.microseconds = 0 + return delta -- cgit v1.2.3 From fb028d19ccfa768ad7accc9fbddc8c3b1696e0a0 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 13 Aug 2022 17:00:41 -0400 Subject: Removed unused datetime import --- bot/exts/moderation/infraction/_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 5e708d7fe..5f2529c6b 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,5 +1,4 @@ import typing as t -from datetime import datetime import arrow import discord -- cgit v1.2.3 From be64bb58640183025a13e7a502382bf4b14f79a5 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:42:43 +0100 Subject: Update the docker-compose snekbox dep for bot The bot service was still configured to depend on the snekbox service, even though this service is now optional, in favour of the snekbox-311 service. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index f7759566b..be7370d6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,7 @@ services: depends_on: - web - redis - - snekbox + - snekbox-311 env_file: - .env environment: -- cgit v1.2.3 From fdb55d1beb0329064fde66bf1ee04a7aef032054 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:43:19 +0100 Subject: Bump to Python 3.10 --- .github/workflows/lint-test.yml | 2 +- Dockerfile | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index 57cc544d9..2b3dd5b4f 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -61,7 +61,7 @@ jobs: id: python uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.10' # This step caches our Python dependencies. To make sure we # only restore a cache when the dependencies, the python version, diff --git a/Dockerfile b/Dockerfile index 30bf8a361..5bb400658 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 python:3.9-slim +FROM --platform=linux/amd64 python:3.10-slim # Set pip to have no saved cache ENV PIP_NO_CACHE_DIR=false \ diff --git a/pyproject.toml b/pyproject.toml index 77d8ee3d4..af3fdc115 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Python Discord "] license = "MIT" [tool.poetry.dependencies] -python = "3.9.*" +python = "3.10.*" "discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca42538d2e7e7a.zip"} # See https://bot-core.pythondiscord.com/ for docs. -- cgit v1.2.3 From f599c7bb945a4d0e26ff3e9f5f234f3f34f5ff16 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:44:58 +0100 Subject: Remove call to get_event_loop in tests get_event_loop is deprecated as of 3.10 if there is no running loop. --- tests/bot/exts/moderation/test_silence.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 65aecad28..82ec138db 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -16,20 +16,19 @@ from tests.helpers import ( ) redis_session = None -redis_loop = asyncio.get_event_loop() def setUpModule(): # noqa: N802 """Create and connect to the fakeredis session.""" global redis_session redis_session = RedisSession(use_fakeredis=True) - redis_loop.run_until_complete(redis_session.connect()) + asyncio.run(redis_session.connect()) def tearDownModule(): # noqa: N802 """Close the fakeredis session.""" if redis_session: - redis_loop.run_until_complete(redis_session.close()) + asyncio.run(redis_session.client.close()) # Have to subclass it because builtins can't be patched. -- cgit v1.2.3 From c906daa2250558962f00be1c423a9a0cff98f905 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:46:18 +0100 Subject: Remove warnings in error handler tests These warnings were caused by the setup coro from error_handler.py being imported directly, causing a warning about an un-awaited coro whenever the Cog was accessed from the same file. --- tests/bot/exts/backend/test_error_handler.py | 103 ++++++++++++--------------- 1 file changed, 47 insertions(+), 56 deletions(-) diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 0a58126e7..7562f6aa8 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -5,7 +5,7 @@ from botcore.site_api import ResponseCodeError from discord.ext.commands import errors from bot.errors import InvalidInfractedUserError, LockedResourceError -from bot.exts.backend.error_handler import ErrorHandler, setup +from bot.exts.backend import error_handler from bot.exts.info.tags import Tags from bot.exts.moderation.silence import Silence from bot.utils.checks import InWhitelistCheckFailure @@ -18,14 +18,14 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.bot = MockBot() self.ctx = MockContext(bot=self.bot) + self.cog = error_handler.ErrorHandler(self.bot) async def test_error_handler_already_handled(self): """Should not do anything when error is already handled by local error handler.""" self.ctx.reset_mock() - cog = ErrorHandler(self.bot) error = errors.CommandError() error.handled = "foo" - self.assertIsNone(await cog.on_command_error(self.ctx, error)) + self.assertIsNone(await self.cog.on_command_error(self.ctx, error)) self.ctx.send.assert_not_awaited() async def test_error_handler_command_not_found_error_not_invoked_by_handler(self): @@ -45,28 +45,27 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): "called_try_get_tag": True } ) - cog = ErrorHandler(self.bot) - cog.try_silence = AsyncMock() - cog.try_get_tag = AsyncMock() - cog.try_run_eval = AsyncMock(return_value=False) + self.cog.try_silence = AsyncMock() + self.cog.try_get_tag = AsyncMock() + self.cog.try_run_eval = 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"]): self.ctx.reset_mock() - cog.try_silence.reset_mock(return_value=True) - cog.try_get_tag.reset_mock() + self.cog.try_silence.reset_mock(return_value=True) + self.cog.try_get_tag.reset_mock() - cog.try_silence.return_value = case["try_silence_return"] + self.cog.try_silence.return_value = case["try_silence_return"] self.ctx.channel.id = 1234 - self.assertIsNone(await cog.on_command_error(self.ctx, error)) + self.assertIsNone(await self.cog.on_command_error(self.ctx, error)) if case["try_silence_return"]: - cog.try_get_tag.assert_not_awaited() - cog.try_silence.assert_awaited_once() + self.cog.try_get_tag.assert_not_awaited() + self.cog.try_silence.assert_awaited_once() else: - cog.try_silence.assert_awaited_once() - cog.try_get_tag.assert_awaited_once() + self.cog.try_silence.assert_awaited_once() + self.cog.try_get_tag.assert_awaited_once() self.ctx.send.assert_not_awaited() @@ -74,59 +73,54 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): """Should do nothing when error is `CommandNotFound` and have attribute `invoked_from_error_handler`.""" ctx = MockContext(bot=self.bot, invoked_from_error_handler=True) - cog = ErrorHandler(self.bot) - cog.try_silence = AsyncMock() - cog.try_get_tag = AsyncMock() - cog.try_run_eval = AsyncMock() + self.cog.try_silence = AsyncMock() + self.cog.try_get_tag = AsyncMock() + self.cog.try_run_eval = AsyncMock() error = errors.CommandNotFound() - self.assertIsNone(await cog.on_command_error(ctx, error)) + self.assertIsNone(await self.cog.on_command_error(ctx, error)) - cog.try_silence.assert_not_awaited() - cog.try_get_tag.assert_not_awaited() - cog.try_run_eval.assert_not_awaited() + self.cog.try_silence.assert_not_awaited() + self.cog.try_get_tag.assert_not_awaited() + self.cog.try_run_eval.assert_not_awaited() self.ctx.send.assert_not_awaited() async def test_error_handler_user_input_error(self): """Should await `ErrorHandler.handle_user_input_error` when error is `UserInputError`.""" self.ctx.reset_mock() - cog = ErrorHandler(self.bot) - cog.handle_user_input_error = AsyncMock() + self.cog.handle_user_input_error = AsyncMock() error = errors.UserInputError() - self.assertIsNone(await cog.on_command_error(self.ctx, error)) - cog.handle_user_input_error.assert_awaited_once_with(self.ctx, error) + self.assertIsNone(await self.cog.on_command_error(self.ctx, error)) + self.cog.handle_user_input_error.assert_awaited_once_with(self.ctx, error) async def test_error_handler_check_failure(self): """Should await `ErrorHandler.handle_check_failure` when error is `CheckFailure`.""" self.ctx.reset_mock() - cog = ErrorHandler(self.bot) - cog.handle_check_failure = AsyncMock() + self.cog.handle_check_failure = AsyncMock() error = errors.CheckFailure() - self.assertIsNone(await cog.on_command_error(self.ctx, error)) - cog.handle_check_failure.assert_awaited_once_with(self.ctx, error) + self.assertIsNone(await self.cog.on_command_error(self.ctx, error)) + self.cog.handle_check_failure.assert_awaited_once_with(self.ctx, error) async def test_error_handler_command_on_cooldown(self): """Should send error with `ctx.send` when error is `CommandOnCooldown`.""" self.ctx.reset_mock() - cog = ErrorHandler(self.bot) error = errors.CommandOnCooldown(10, 9, type=None) - self.assertIsNone(await cog.on_command_error(self.ctx, error)) + self.assertIsNone(await self.cog.on_command_error(self.ctx, error)) self.ctx.send.assert_awaited_once_with(error) async def test_error_handler_command_invoke_error(self): """Should call `handle_api_error` or `handle_unexpected_error` depending on original error.""" - cog = ErrorHandler(self.bot) - cog.handle_api_error = AsyncMock() - cog.handle_unexpected_error = AsyncMock() + self.cog.handle_api_error = AsyncMock() + self.cog.handle_unexpected_error = AsyncMock() test_cases = ( { "args": (self.ctx, errors.CommandInvokeError(ResponseCodeError(AsyncMock()))), - "expect_mock_call": cog.handle_api_error + "expect_mock_call": self.cog.handle_api_error }, { "args": (self.ctx, errors.CommandInvokeError(TypeError)), - "expect_mock_call": cog.handle_unexpected_error + "expect_mock_call": self.cog.handle_unexpected_error }, { "args": (self.ctx, errors.CommandInvokeError(LockedResourceError("abc", "test"))), @@ -141,7 +135,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): for case in test_cases: with self.subTest(args=case["args"], expect_mock_call=case["expect_mock_call"]): self.ctx.send.reset_mock() - self.assertIsNone(await cog.on_command_error(*case["args"])) + self.assertIsNone(await self.cog.on_command_error(*case["args"])) if case["expect_mock_call"] == "send": self.ctx.send.assert_awaited_once() else: @@ -151,29 +145,27 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): async def test_error_handler_conversion_error(self): """Should call `handle_api_error` or `handle_unexpected_error` depending on original error.""" - cog = ErrorHandler(self.bot) - cog.handle_api_error = AsyncMock() - cog.handle_unexpected_error = AsyncMock() + self.cog.handle_api_error = AsyncMock() + self.cog.handle_unexpected_error = AsyncMock() cases = ( { "error": errors.ConversionError(AsyncMock(), ResponseCodeError(AsyncMock())), - "mock_function_to_call": cog.handle_api_error + "mock_function_to_call": self.cog.handle_api_error }, { "error": errors.ConversionError(AsyncMock(), TypeError), - "mock_function_to_call": cog.handle_unexpected_error + "mock_function_to_call": self.cog.handle_unexpected_error } ) for case in cases: with self.subTest(**case): - self.assertIsNone(await cog.on_command_error(self.ctx, case["error"])) + self.assertIsNone(await self.cog.on_command_error(self.ctx, case["error"])) case["mock_function_to_call"].assert_awaited_once_with(self.ctx, case["error"].original) async def test_error_handler_two_other_errors(self): """Should call `handle_unexpected_error` if error is `MaxConcurrencyReached` or `ExtensionError`.""" - cog = ErrorHandler(self.bot) - cog.handle_unexpected_error = AsyncMock() + self.cog.handle_unexpected_error = AsyncMock() errs = ( errors.MaxConcurrencyReached(1, MagicMock()), errors.ExtensionError(name="foo") @@ -181,16 +173,15 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): for err in errs: with self.subTest(error=err): - cog.handle_unexpected_error.reset_mock() - self.assertIsNone(await cog.on_command_error(self.ctx, err)) - cog.handle_unexpected_error.assert_awaited_once_with(self.ctx, err) + self.cog.handle_unexpected_error.reset_mock() + self.assertIsNone(await self.cog.on_command_error(self.ctx, err)) + self.cog.handle_unexpected_error.assert_awaited_once_with(self.ctx, err) @patch("bot.exts.backend.error_handler.log") async def test_error_handler_other_errors(self, log_mock): """Should `log.debug` other errors.""" - cog = ErrorHandler(self.bot) error = errors.DisabledCommand() # Use this just as a other error - self.assertIsNone(await cog.on_command_error(self.ctx, error)) + self.assertIsNone(await self.cog.on_command_error(self.ctx, error)) log_mock.debug.assert_called_once() @@ -202,7 +193,7 @@ class TrySilenceTests(unittest.IsolatedAsyncioTestCase): self.silence = Silence(self.bot) self.bot.get_command.return_value = self.silence.silence self.ctx = MockContext(bot=self.bot) - self.cog = ErrorHandler(self.bot) + self.cog = error_handler.ErrorHandler(self.bot) async def test_try_silence_context_invoked_from_error_handler(self): """Should set `Context.invoked_from_error_handler` to `True`.""" @@ -334,7 +325,7 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): self.bot = MockBot() self.ctx = MockContext() self.tag = Tags(self.bot) - self.cog = ErrorHandler(self.bot) + self.cog = error_handler.ErrorHandler(self.bot) self.bot.get_command.return_value = self.tag.get_command async def test_try_get_tag_get_command(self): @@ -399,7 +390,7 @@ class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.bot = MockBot() self.ctx = MockContext(bot=self.bot) - self.cog = ErrorHandler(self.bot) + self.cog = error_handler.ErrorHandler(self.bot) async def test_handle_input_error_handler_errors(self): """Should handle each error probably.""" @@ -555,5 +546,5 @@ class ErrorHandlerSetupTests(unittest.IsolatedAsyncioTestCase): async def test_setup(self): """Should call `bot.add_cog` with `ErrorHandler`.""" bot = MockBot() - await setup(bot) + await error_handler.setup(bot) bot.add_cog.assert_awaited_once() -- cgit v1.2.3 From f74eab3b498e7565b37a605e062922ff8db6f236 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:46:58 +0100 Subject: Bump bot-core version This bump comes with a move to redis-py over aioredis. As such, pin new transitive dependancies to exact versions. --- poetry.lock | 1325 +++++--------------------------------------------------- pyproject.toml | 9 +- 2 files changed, 118 insertions(+), 1216 deletions(-) diff --git a/poetry.lock b/poetry.lock index 62ebf1e66..e7f82d207 100644 --- a/poetry.lock +++ b/poetry.lock @@ -29,18 +29,6 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["aiodns", "brotli", "cchardet"] -[[package]] -name = "aioredis" -version = "1.3.1" -description = "asyncio (PEP 3156) Redis support" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -async-timeout = "*" -hiredis = "*" - [[package]] name = "aiosignal" version = "1.2.0" @@ -65,18 +53,18 @@ python-dateutil = ">=2.7.0" [[package]] name = "async-rediscache" -version = "0.2.0" +version = "1.0.0rc2" description = "An easy to use asynchronous Redis cache" category = "main" optional = false python-versions = "~=3.7" [package.dependencies] -aioredis = ">=1" -fakeredis = {version = ">=1.4.4", extras = ["lua"], optional = true, markers = "extra == \"fakeredis\""} +fakeredis = {version = ">=1.7.1", extras = ["lua"], optional = true, markers = "extra == \"fakeredis\""} +redis = ">=4.2,<5.0" [package.extras] -fakeredis = ["fakeredis[lua] (>=1.4.4)"] +fakeredis = ["fakeredis[lua] (>=1.7.1)"] [[package]] name = "async-timeout" @@ -125,23 +113,24 @@ lxml = ["lxml"] [[package]] name = "bot-core" -version = "7.4.0" +version = "8.0.0-beta.4" description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." category = "main" optional = false -python-versions = "3.9.*" +python-versions = "3.10.*" [package.dependencies] -async-rediscache = {version = "0.2.0", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""} +aiodns = "3.0.0" +async-rediscache = {version = "1.0.0rc2", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""} "discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca42538d2e7e7a.zip"} statsd = "3.3.0" [package.extras] -async-rediscache = ["async-rediscache[fakeredis] (==0.2.0)"] +async-rediscache = ["async-rediscache[fakeredis] (==1.0.0rc2)"] [package.source] type = "url" -url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.4.0.zip" +url = "https://github.com/python-discord/bot-core/archive/refs/tags/v8.0.0-beta.4.zip" [[package]] name = "certifi" version = "2022.6.15" @@ -297,22 +286,21 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.7.5" +version = "1.8.2" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.7,<4.0" [package.dependencies] -lupa = {version = "*", optional = true, markers = "extra == \"lua\""} -packaging = "*" -redis = "<=4.3.1" -six = ">=1.12" -sortedcontainers = "*" +lupa = {version = ">=1.13,<2.0", optional = true, markers = "extra == \"lua\""} +redis = "<4.4" +six = ">=1.16.0,<2.0.0" +sortedcontainers = ">=2.4.0,<3.0.0" [package.extras] -aioredis = ["aioredis"] -lua = ["lupa"] +aioredis = ["aioredis (>=2.0.1,<3.0.0)"] +lua = ["lupa (>=1.13,<2.0)"] [[package]] name = "feedparser" @@ -457,14 +445,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "hiredis" -version = "2.0.0" -description = "Python wrapper for hiredis" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "humanfriendly" version = "10.0" @@ -665,8 +645,8 @@ optional = false python-versions = ">=3.6" [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] [[package]] name = "pre-commit" @@ -816,7 +796,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "pytest-forked" @@ -882,8 +862,8 @@ python-versions = "*" PyYAML = "*" [package.extras] +test = ["pyaml", "toml", "pytest"] docs = ["sphinx"] -test = ["pytest", "toml", "pyaml"] [[package]] name = "pyyaml" @@ -909,7 +889,7 @@ full = ["numpy"] [[package]] name = "redis" -version = "4.3.1" +version = "4.3.4" description = "Python client for Redis database and key-value store" category = "main" optional = false @@ -1099,7 +1079,7 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.10" +version = "1.26.11" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1150,1178 +1130,103 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" -python-versions = "3.9.*" -content-hash = "43b10dbc644c527ce55e01bc555ce98eb00a8e347409c08152668caf5276688c" +python-versions = "3.10.*" +content-hash = "3b9ec0ebe5dea22a952b807abf941c2f7c3a3b97364504d289189e7e947fe34d" [metadata.files] -aiodns = [ - {file = "aiodns-3.0.0-py3-none-any.whl", hash = "sha256:2b19bc5f97e5c936638d28e665923c093d8af2bf3aa88d35c43417fa25d136a2"}, - {file = "aiodns-3.0.0.tar.gz", hash = "sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6"}, -] -aiohttp = [ - {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, - {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, - {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, - {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, - {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, - {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, - {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, - {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, - {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, - {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, - {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, - {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, - {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, - {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, - {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, - {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, - {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, - {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, - {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, - {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, - {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, - {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, -] -aioredis = [ - {file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"}, - {file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"}, -] -aiosignal = [ - {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, - {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, -] -arrow = [ - {file = "arrow-1.2.2-py3-none-any.whl", hash = "sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"}, - {file = "arrow-1.2.2.tar.gz", hash = "sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b"}, -] -async-rediscache = [ - {file = "async-rediscache-0.2.0.tar.gz", hash = "sha256:c1fd95fe530211b999748ebff96e2e9b629f2664957f9b36916b898e42fc57c4"}, - {file = "async_rediscache-0.2.0-py3-none-any.whl", hash = "sha256:710676211b407399c9ad94afa66fa04c22a936be11ba6f227e6c74cfa140ce78"}, -] -async-timeout = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, -] +aiodns = [] +aiohttp = [] +aiosignal = [] +arrow = [] +async-rediscache = [] +async-timeout = [] atomicwrites = [] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -beautifulsoup4 = [ - {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, - {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, -] +attrs = [] +beautifulsoup4 = [] bot-core = [] -certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -cfgv = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, - {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coloredlogs = [ - {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, - {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, -] -coverage = [ - {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, - {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, - {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, - {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, - {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, - {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, - {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, - {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, - {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, - {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, - {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, - {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, - {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, -] -deepdiff = [ - {file = "deepdiff-5.7.0-py3-none-any.whl", hash = "sha256:1ffb38c3b5d9174eb2df95850c93aee55ec00e19396925036a2e680f725079e0"}, - {file = "deepdiff-5.7.0.tar.gz", hash = "sha256:838766484e323dcd9dec6955926a893a83767dc3f3f94542773e6aa096efe5d4"}, -] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] +certifi = [] +cffi = [] +cfgv = [] +charset-normalizer = [] +colorama = [] +coloredlogs = [] +coverage = [] +deepdiff = [] +deprecated = [] "discord.py" = [] distlib = [] -emoji = [ - {file = "emoji-1.7.0.tar.gz", hash = "sha256:65c54533ea3c78f30d0729288998715f418d7467de89ec258a31c0ce8660a1d1"}, -] -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.7.5-py3-none-any.whl", hash = "sha256:c4ca2be686e7e7637756ccc7dcad8472a5e4866b065431107d7a4b7a250d4e6f"}, - {file = "fakeredis-1.7.5.tar.gz", hash = "sha256:49375c630981dd4045d9a92e2709fcd4476c91f927e0228493eefa625e705133"}, -] -feedparser = [ - {file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"}, - {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"}, -] -filelock = [ - {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, - {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -flake8-annotations = [ - {file = "flake8-annotations-2.8.0.tar.gz", hash = "sha256:a2765c6043098aab0a3f519b871b33586c7fba7037686404b920cf8100cc1cdc"}, - {file = "flake8_annotations-2.8.0-py3-none-any.whl", hash = "sha256:880f9bb0677b82655f9021112d64513e03caefd2e0d786ab4a59ddb5b262caa9"}, -] -flake8-bugbear = [ - {file = "flake8-bugbear-22.3.23.tar.gz", hash = "sha256:e0dc2a36474490d5b1a2d57f9e4ef570abc09f07cbb712b29802e28a2367ff19"}, - {file = "flake8_bugbear-22.3.23-py3-none-any.whl", hash = "sha256:ec5ec92195720cee1589315416b844ffa5e82f73a78e65329e8055322df1e939"}, -] -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-4.1.1.tar.gz", hash = "sha256:d814304ab70e6e58859bc5c3e221e2e6e71c958e7005239202fee19c24f82717"}, - {file = "flake8_isort-4.1.1-py3-none-any.whl", hash = "sha256:c4e8b6dcb7be9b71a02e6e5d4196cefcef0f3447be51e82730fb336fff164949"}, -] -flake8-polyfill = [ - {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, - {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, -] -flake8-string-format = [ - {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, - {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, -] -flake8-tidy-imports = [ - {file = "flake8-tidy-imports-4.6.0.tar.gz", hash = "sha256:3e193d8c4bb4492408a90e956d888b27eed14c698387c9b38230da3dad78058f"}, - {file = "flake8_tidy_imports-4.6.0-py3-none-any.whl", hash = "sha256:6ae9f55d628156e19d19f4c359dd5d3e95431a9bd514f5e2748c53c1398c66b2"}, -] -flake8-todo = [ - {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"}, -] -frozenlist = [ - {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, - {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, - {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, - {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, - {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, - {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, - {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, - {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, - {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, - {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, - {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, - {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, - {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, -] -hiredis = [ - {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"}, - {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"}, - {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"}, - {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"}, - {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"}, - {file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"}, - {file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"}, - {file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"}, - {file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"}, - {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"}, - {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"}, - {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"}, - {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"}, - {file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"}, - {file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"}, - {file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"}, - {file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"}, - {file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"}, - {file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"}, - {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"}, - {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"}, - {file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"}, - {file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"}, - {file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"}, - {file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"}, - {file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"}, - {file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"}, - {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"}, - {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"}, - {file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"}, - {file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"}, - {file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"}, - {file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"}, - {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"}, - {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"}, - {file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"}, - {file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"}, - {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"}, - {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"}, - {file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"}, - {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"}, -] -humanfriendly = [ - {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, - {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, -] +emoji = [] +execnet = [] +fakeredis = [] +feedparser = [] +filelock = [] +flake8 = [] +flake8-annotations = [] +flake8-bugbear = [] +flake8-docstrings = [] +flake8-isort = [] +flake8-polyfill = [] +flake8-string-format = [] +flake8-tidy-imports = [] +flake8-todo = [] +frozenlist = [] +humanfriendly = [] identify = [] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -jarowinkler = [ - {file = "jarowinkler-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9d875e758e6770180adc2cf7d018f6c4ec51f2b1380f7f777d2981e8d9d289c0"}, - {file = "jarowinkler-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1648c56e2842023ce11bc04138e4c4be4cc11ab1795b1f7490373f8d0e25f91d"}, - {file = "jarowinkler-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5dfc31f7d700d48ec5050e09926160e074aa43d10f651458b141b4aa9c02f787"}, - {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf7d840bac828523d38917d5856f8de3867a9b241e3ee863b37c0cb7a58cef4d"}, - {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2295e73427f5431400e202eb92247ae0ce6f1ea18bf7055bb4aab177326101"}, - {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c98fee353b694435ad2e8ff60e3981c9469a0bac0ba901255af81eef3b96842f"}, - {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:160a355282d674652394d56d616e125f42494f40f9a57df697f51420464ba3c9"}, - {file = "jarowinkler-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e40aa8f37a830f463e412f4c2b3bd64c67a57c27d3d84b1b4afa6fb034b1a50"}, - {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1fcb915e886099c507719d682a1963a580ffa729b3b12d0bdd5d82bccdd1b49f"}, - {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1d99cf7ac9003788f3dbc26f92840b4e8b0befb04f19c7601e90fcbe2824347"}, - {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:8f05afd2df6c69b266c5fa16aa3ebb8bf8d63ca9e70aa7950485e2156fc78cdd"}, - {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:36bb3a75977eeb4f6f048209f26c8c813893547e159047f7c3f3dbb320c7f10c"}, - {file = "jarowinkler-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:022e108c8cd58a588f204c2861b11a30bb5a71e207fd56ba5a1aa11bc37fa1a8"}, - {file = "jarowinkler-1.0.5-cp310-cp310-win32.whl", hash = "sha256:500ca3acdaa6444867f627d104bc28eef356e40576f60c1f98d2802d0bc45f78"}, - {file = "jarowinkler-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:0a39dc132b74caefbf6c35fa656046fea0c013cf5009d09a09b9983375547588"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5c94562a96b5e3e29c6bea37a2671f1746541d8babb93edbac9c5994895e8eec"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43562cc5ca8f599cffe2015d6b5ac8585a0c5fa4e8ff9ffd397af73523013fed"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c46c603c3e10426d5006ef1fb6bef3d14b311259c504f97dcc692481e31ae80"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:117424c2c134fb64d6b406a0b4be5a570d2c45e4b05572863e4cdb4b89358e7d"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:effc4e7325d80baa83c22f20ba0f9ddd57249c0ef374adab0af2011b3091c612"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2adf6f1766f147caaf061444ee3ff68b4368e6545d5631fe33d9e97cb3cb5853"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:976b663be2e54efd9558ffd42eae2e3c011852a5fdbee8aede621de3c0d58186"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd95a2e1af798fe597fcbbe9f8d7a5e8dfec3fb59670aa74be3501422d8aee76"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:110232f025fa15bfa3beb5d388b3ff5217edf2a5f20555a1beee7d7fb302d865"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:3da6a52acf3b9750bf71f1c7591cd454138a0b44abce52fbc98b44c7e2def87e"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e941b5b2ded630772ba26181d726ff0f1e1f8a72123932c2a0c108f70ae89188"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:e542b78e9d934d625c6b0b7be4964ff79a7dac9dba1c7abef3b9202a38d11261"}, - {file = "jarowinkler-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:57f67552e1da2be03daa48bf1a4d0357b5b28a92ff6539974fd665516ccf31be"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c8fcd41aa0f5a89a566c8afac19621115b5df1d6e542b03d92bd3c9f4b5f81b"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe94701fd2d101d22e1fa3905c77b53913358425c86c93b03a41dfde77347655"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b662164e93d7b1a9808b36d85c390004e7b6a8a64de88929a244a191ea1174b7"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45ebe15affb4bc905bd7d68bce01c393bfc1456b8ffeeccaa76d22041c2bf2cd"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63d510a57963b6a3c771c11e606d79c991f4648f295acb4631d6fa8100507a00"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:592d2447d4fedaf099b44f42f245d0ba3ad776538c2d4022430a528a4d0cafa4"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:33d4fd6247bc31264d4761fff2cbfefab86294f21d057d150e2ffee7dad9f338"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b91db5a9e153837fb6fc5bf53c1fc6dc34b1a86336f8e1dd694fa4cfd640155b"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:2b0f47c8ad3d98bc914d0b29713e3e034c0a68dc3913b2ecd79aaf506a79817c"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5174349057285c877d01fd69b5a5db317c56130b3ccb51500c1ed324edfcf664"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0dc8f98fa00a025b9418e610c0d1d44ea3eba58643ee0d12eee49f7b7921947e"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:1f6640c242e31f6ea991801c14240b0b6a642cca63d5451a1e05ead71566adec"}, - {file = "jarowinkler-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ecd644d7fdc6fc1636f5eabdb3e325f7f477eb15ad1f93af86b83e32e5a22dd2"}, - {file = "jarowinkler-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:08e91b815a5f2fd3bf7c946f7206d83a085e5b39dc215b99127dcf03fa667e2c"}, - {file = "jarowinkler-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b65b771629395944e10129d7cfdad35dd14d75e697c58388e918a1add1f0af9c"}, - {file = "jarowinkler-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:52f40babf8536bf3dfbdcce0b626dcc0453c7d5a80a9702826e93b20fc287b03"}, - {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47b624d06a5729af1fcd741d57e08c43e289996152c690be6ec38267d2ec1ae8"}, - {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8e4468a78577839b65251720d6e88bfbe66977bc53aed80bf32efdfd5a7fafa"}, - {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9ec340eac44452206b6e3c8d6f3198cf10e7758433adb4e9c48390c0ed81c7"}, - {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113d38a2434e6d8347c48aa9b7d4c3c34f4c0ffcd8a65b25f89a8129c373da61"}, - {file = "jarowinkler-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6c419a12597b610ecb45c83c358ec9418f788131c3817413c69f16ae2a4211b"}, - {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a835d57440d1836ac6d813782cbc56c88f9a52ecc5ad9f444530b766c718750b"}, - {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:29e473a5988045ee5820ad4f9b700aa09cc60dddac16f5cad625daaa5405f862"}, - {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:67216289c24fa3934d23f781cdf4468aeb910f4d295bcbbfc206335dbed38e78"}, - {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7cc2f84ba258da0e2455337ff4fd9f77a4b672b8997ee9599ed8cc516bc57d2d"}, - {file = "jarowinkler-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75156fa4dcc139afdb97eb5419c1fe24ee1f9b6bca02ce36b22c8f28fa06c074"}, - {file = "jarowinkler-1.0.5-cp38-cp38-win32.whl", hash = "sha256:b05a2e291e5b4f8254fa214c911cd80847326237a921a27135572e32747ff5a0"}, - {file = "jarowinkler-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:251152bc224e8dd7ca290d4b6dc1e2ef6d1a6cd52f8611617870d2e1dc615772"}, - {file = "jarowinkler-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:88b3757cdd150c9e8763c63b66154239910cc49179cfbb1841b8783cba8c5be2"}, - {file = "jarowinkler-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:391c887af2c46b18fb1ad40246147a1322bd9073edf61b866fc7143946deed08"}, - {file = "jarowinkler-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81323b75faf3cc62a96524d7f84f34fa3bd49fe6a53eacd6a9c552e3ca5eeeb8"}, - {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce612d1d74775ed444ba0a5f7cc9307d1c10059fafd787b8ef1b18dac96b0dd9"}, - {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b4b198ce38635925e9aaa227499c2c8aeee2e9c2372a73cef56fb34f0af7de0"}, - {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2732bad98bd70dcf00e8b2a4a9e21629aef9e271de0f8e2a71fb6a22d95dca5b"}, - {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:283b964c39863c8e59f3538dd054dbcb32700418cec1b7c8d70d28118b28363a"}, - {file = "jarowinkler-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c7336f424fa2c8d739b97f276535b8fecc23d7622b00c647154adb67664029"}, - {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ac6172e4d531aa57f48b6ec778bc818d0184a7e8779079583e12b6beac7fae6"}, - {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e4da9a208ad72318084b0d0d4bbbe0bb14cf75141ef72ef60087d011da410cc9"}, - {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:97b05394a7bd016e39ec3e31bae7341f5cf70ee87cc0faa35f6c2f71c5f9cc64"}, - {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:5ab84f035ae4bd7288abc1d3b4388f491d3630dd7dc79fb207bbadb833844e36"}, - {file = "jarowinkler-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a982d80da526fd11e83fe44985c410ec3a15846727c8553af591b758fbbb2aef"}, - {file = "jarowinkler-1.0.5-cp39-cp39-win32.whl", hash = "sha256:c4bcae3864139b87f03ff3d762b64d113a7d4ff9c2a781c8856a7a3be3ecc456"}, - {file = "jarowinkler-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:c68790ae84538504a7b7074fbb81827e19a28b00e942d92104d98d05606c0927"}, - {file = "jarowinkler-1.0.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dd6ff49d794ccb6c624ca5c14cbefc6c2611490b69d17b62c2dcf73e18dca046"}, - {file = "jarowinkler-1.0.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acd563c348d6a9615b068dba24f6c04920527d003a2790b7006facf589b5c509"}, - {file = "jarowinkler-1.0.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:579077f5c449b871c2944c1991a8cba3654dfb72207a9c3990736ec9f5cc57a9"}, - {file = "jarowinkler-1.0.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9dc4420651e2a904d5176fede44b871808b9057497037e0e2b26462600c70ff"}, - {file = "jarowinkler-1.0.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:59c4229dd19b4572c226ab29959ef15706ecb1d58e93cc9218cd9461fc9bb8b3"}, - {file = "jarowinkler-1.0.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:904189811e04331853149be4684896b95dda646b41b6266f8d6813cecd8f77ee"}, - {file = "jarowinkler-1.0.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf59fbc689e8e901429493a3de2932b6fd8ddf7f1cec7b3d55a0f0a0fcfa975"}, - {file = "jarowinkler-1.0.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bc2a594eda6a41a8367b94b3817fa923e097dd64d385549c32f7eb21441b2d5"}, - {file = "jarowinkler-1.0.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af2a2c1b68266c516e9fa329fe5af51b8061d97ea1b230742ae70176dcdfd116"}, - {file = "jarowinkler-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35abfc9c59e3a47c59c7d988e6756c339c1779bac40020fe39c4875a37d0873a"}, - {file = "jarowinkler-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76a69ccdd0961bf79795caf31a0eba12feee3ca2225cfa88040752dfbe5ccd4b"}, - {file = "jarowinkler-1.0.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09fe4f99344ba01b20a29da0aa6b42276abecd2edc465d9cd54582de9aad8c0b"}, - {file = "jarowinkler-1.0.5.tar.gz", hash = "sha256:d0ecdae8e122594d22e09ceebfca23342d290a9305d669958e674e0e39e2e260"}, -] -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"}, -] +idna = [] +iniconfig = [] +isort = [] +jarowinkler = [] +lupa = [] lxml = [] -markdownify = [ - {file = "markdownify-0.6.1-py3-none-any.whl", hash = "sha256:7489fd5c601536996a376c4afbcd1dd034db7690af807120681461e82fbc0acc"}, - {file = "markdownify-0.6.1.tar.gz", hash = "sha256:31d7c13ac2ada8bfc7535a25fee6622ca720e1b5f2d4a9cbc429d167c21f886d"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -more-itertools = [ - {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, - {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, -] -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"}, -] -nodeenv = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] -ordered-set = [ - {file = "ordered-set-4.0.2.tar.gz", hash = "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pep8-naming = [ - {file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"}, - {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, -] -pip-licenses = [ - {file = "pip-licenses-3.5.3.tar.gz", hash = "sha256:f44860e00957b791c6c6005a3328f2d5eaeee96ddb8e7d87d4b0aa25b02252e4"}, - {file = "pip_licenses-3.5.3-py3-none-any.whl", hash = "sha256:59c148d6a03784bf945d232c0dc0e9de4272a3675acaa0361ad7712398ca86ba"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -pre-commit = [ - {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, - {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, -] -psutil = [ - {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87"}, - {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af"}, - {file = "psutil-5.9.1-cp27-cp27m-win32.whl", hash = "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc"}, - {file = "psutil-5.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2"}, - {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0"}, - {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22"}, - {file = "psutil-5.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9"}, - {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8"}, - {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de"}, - {file = "psutil-5.9.1-cp310-cp310-win32.whl", hash = "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329"}, - {file = "psutil-5.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021"}, - {file = "psutil-5.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237"}, - {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453"}, - {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685"}, - {file = "psutil-5.9.1-cp36-cp36m-win32.whl", hash = "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36"}, - {file = "psutil-5.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d"}, - {file = "psutil-5.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc"}, - {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676"}, - {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4"}, - {file = "psutil-5.9.1-cp37-cp37m-win32.whl", hash = "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b"}, - {file = "psutil-5.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680"}, - {file = "psutil-5.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1"}, - {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4"}, - {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b"}, - {file = "psutil-5.9.1-cp38-cp38-win32.whl", hash = "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689"}, - {file = "psutil-5.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0"}, - {file = "psutil-5.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81"}, - {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e"}, - {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537"}, - {file = "psutil-5.9.1-cp39-cp39-win32.whl", hash = "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574"}, - {file = "psutil-5.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5"}, - {file = "psutil-5.9.1.tar.gz", hash = "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954"}, -] -ptable = [ - {file = "PTable-0.9.2.tar.gz", hash = "sha256:aa7fc151cb40f2dabcd2275ba6f7fd0ff8577a86be3365cd3fb297cbe09cc292"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycares = [ - {file = "pycares-4.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d83f193563b42360528167705b1c7bb91e2a09f990b98e3d6378835b72cd5c96"}, - {file = "pycares-4.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b03f69df69f0ab3bfb8dbe54444afddff6ff9389561a08aade96b4f91207a655"}, - {file = "pycares-4.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3b78bdee2f2f1351d5fccc2d1b667aea2d15a55d74d52cb9fd5bea8b5e74c4dc"}, - {file = "pycares-4.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f05223de13467bb26f9a1594a1799ce2d08ad8ea241489fecd9d8ed3bbbfc672"}, - {file = "pycares-4.2.1-cp310-cp310-win32.whl", hash = "sha256:1f37f762414680063b4dfec5be809a84f74cd8e203d939aaf3ba9c807a9e7013"}, - {file = "pycares-4.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:1a9506d496efeb809a1b63647cb2f3f33c67fcf62bf80a2359af692fef2c1755"}, - {file = "pycares-4.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2fd53eb5b441c4f6f9c78d7900e05883e9998b34a14b804be4fc4c6f9fea89f3"}, - {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:061dd4c80fec73feb150455b159704cd51a122f20d36790033bd6375d4198579"}, - {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a521d7f54f3e52ded4d34c306ba05cfe9eb5aaa2e5aaf83c96564b9369495588"}, - {file = "pycares-4.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:99e00e397d07a79c9f43e4303e67f4f97bcabd013bda0d8f2d430509b7aef8a0"}, - {file = "pycares-4.2.1-cp36-cp36m-win32.whl", hash = "sha256:d9cd826d8e0c270059450709bff994bfeb072f79d82fd3f11c701690ff65d0e7"}, - {file = "pycares-4.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f8e6942965465ca98e212376c4afb9aec501d8129054929744b2f4a487c8c14b"}, - {file = "pycares-4.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e75cbd4d3b3d9b02bba6e170846e39893a825e7a5fb1b96728fc6d7b964f8945"}, - {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2e8ec4c8e07c986b70a3cc8f5b297c53b08ac755e5b9797512002a466e2de86"}, - {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5333b51ef4ff3e8973b4a1b57cad5ada13e15552445ee3cd74bd77407dec9d44"}, - {file = "pycares-4.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2113529004df4894783eaa61e9abc3a680756b6f033d942f2800301ae8c71c29"}, - {file = "pycares-4.2.1-cp37-cp37m-win32.whl", hash = "sha256:e7a95763cdc20cf9ec357066e656ea30b8de6b03de6175cbb50890e22aa01868"}, - {file = "pycares-4.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a901776163a04de5d67c42bd63a287cff9cb05fc041668ad1681fe3daa36445"}, - {file = "pycares-4.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:66b5390a4885a578e687d3f2683689c35e1d4573f4d0ecf217431f7bb55c49a0"}, - {file = "pycares-4.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15dd5cf21bc73ad539e8aabf7afe370d1df8af7bc6944cd7298f3bfef0c1a27c"}, - {file = "pycares-4.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ee625d7571039038bca51ae049b047cbfcfc024b302aae6cc53d5d9aa8648a8"}, - {file = "pycares-4.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:396ee487178e9de06ca4122a35a157474db3ce0a0db6038a31c831ebb9863315"}, - {file = "pycares-4.2.1-cp38-cp38-win32.whl", hash = "sha256:e4dc37f732f7110ca6368e0128cbbd0a54f5211515a061b2add64da2ddb8e5ca"}, - {file = "pycares-4.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:3636fccf643c5192c34ee0183c514a2d09419e3a76ca2717cef626638027cb21"}, - {file = "pycares-4.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6724573e830ea2345f4bcf0f968af64cc6d491dc2133e9c617f603445dcdfa58"}, - {file = "pycares-4.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dbfcacbde6c21380c412c13d53ea44b257dea3f7b9d80be2c873bb20e21fee"}, - {file = "pycares-4.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8a46839da642b281ac5f56d3c6336528e128b3c41eab9c5330d250f22325e9d"}, - {file = "pycares-4.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9b05c2cec644a6c66b55bcf6c24d4dfdaf2f7205b16e5c4ceee31db104fac958"}, - {file = "pycares-4.2.1-cp39-cp39-win32.whl", hash = "sha256:8bd6ed3ad3a5358a635c1acf5d0f46be9afb095772b84427ff22283d2f31db1b"}, - {file = "pycares-4.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:fbd53728d798d07811898e11991e22209229c090eab265a53d12270b95d70d1a"}, - {file = "pycares-4.2.1.tar.gz", hash = "sha256:735b4f75fd0f595c4e9184da18cd87737f46bc81a64ea41f4edce2b6b68d46d2"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pydocstyle = [ - {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, - {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -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.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, - {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -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-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"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] -python-dotenv = [ - {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, - {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, -] -python-frontmatter = [ - {file = "python-frontmatter-1.0.0.tar.gz", hash = "sha256:e98152e977225ddafea6f01f40b4b0f1de175766322004c826ca99842d19a7cd"}, - {file = "python_frontmatter-1.0.0-py3-none-any.whl", hash = "sha256:766ae75f1b301ffc5fe3494339147e0fd80bc3deff3d7590a93991978b579b08"}, -] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -rapidfuzz = [ - {file = "rapidfuzz-2.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b306b4a1d42a8dfd5f3daff9a82853f1541e5c74a2ec34515a5e5cd51f3c7307"}, - {file = "rapidfuzz-2.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ee380254d8b29d0b0f47a020e7f16375a4d97164b8071b3f94d5c684d744093"}, - {file = "rapidfuzz-2.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac993b8760c5308d885c300355e2c537daf0696ebc5d30436af83818978e661c"}, - {file = "rapidfuzz-2.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06a394e475316aeddbf4bf9691aabf4825f8c1acf87b49abbb7b9dad7e555ae"}, - {file = "rapidfuzz-2.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79883fcfc3e550b356d45ac2bf1af391161f9ddb64b1ed504f9a94086b824709"}, - {file = "rapidfuzz-2.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d44d74ace68b3ec6dee4501188c74f124da8c940877989baf9f672d51368e171"}, - {file = "rapidfuzz-2.0.7-cp310-cp310-win32.whl", hash = "sha256:9ec9fd78d40f392cd4ce91dbb17477cd07740d0cb0b7bf44e9ab67c16ee3d5ce"}, - {file = "rapidfuzz-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:7983ed01b0ac5343bea4d737024576a86a8c68f3c8d811498eb0facf8d3bafc1"}, - {file = "rapidfuzz-2.0.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:49fd3d2a789abc30c811d6ed81db1c5f143caf5e975720bf9ab62c920253d5e9"}, - {file = "rapidfuzz-2.0.7-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:636489517bbd0786f300948f8eba59635f2fb781ecbc2ed19deba3426ee32ab6"}, - {file = "rapidfuzz-2.0.7-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6a47418b86a6b8267a89f253e2b14f9aa8b4b559141b15f8c8a9769d19b109"}, - {file = "rapidfuzz-2.0.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe01ca2cbdb2aee6f80c1fc3a82fa69ee9ef9c44f085a725113b5d12209e05d"}, - {file = "rapidfuzz-2.0.7-cp36-cp36m-win32.whl", hash = "sha256:8157406a1b44cd742d65c65ca8345e47fcc8642148a970626b886fb52b3abd1d"}, - {file = "rapidfuzz-2.0.7-cp36-cp36m-win_amd64.whl", hash = "sha256:3062ea2a0481196376e364470c682d5ebc22eb5d4c114350f05f079119ea61b8"}, - {file = "rapidfuzz-2.0.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cbfc3fcbbd00edf7f917ad0d6bf46350c64a9910c14d05e1936d436170f2531d"}, - {file = "rapidfuzz-2.0.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae15eb44e014101b208c97a253d850d6fb4a8465f3c9ee8be3508b03135ad0e7"}, - {file = "rapidfuzz-2.0.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9224115aae07d42b9250d8ca58d5568cab2ddd8720c551aa7de9dcec661ee86"}, - {file = "rapidfuzz-2.0.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42bc2cf64ebbf2a80e6fd03353679de17118a431dce358cfadc7cdb72ac9510a"}, - {file = "rapidfuzz-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:34416ee6265dfa1415e9f10c7dafe6a85296117f534f67d00021eeaa661c8d9e"}, - {file = "rapidfuzz-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:4044ef50f020f16f99b5979784b648b7ab90cd6bd0d275359818a2c155f9c01d"}, - {file = "rapidfuzz-2.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1332fb51345e431ba39e075c3dbc222bb9770f0e73c097c7a65c8c2ea331004c"}, - {file = "rapidfuzz-2.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ac560a603d0d1b9d70cc0a376d1adf57ece4195e61351d410e0c7b0fa280cbe"}, - {file = "rapidfuzz-2.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0fd757a38e14f247d929af7df6762aee2082f7a6882c85a31f17b09a450bbb5e"}, - {file = "rapidfuzz-2.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723a48d5937e4a558fb5df553b3d0e0b3cc05de7f7a8d43a920682b796010ab5"}, - {file = "rapidfuzz-2.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c9b344e3f69c5b69ae0c96411d3ee1dab02ec49124471e44ce2a16f6446fa6d"}, - {file = "rapidfuzz-2.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f34f905a0e9fa01cf26b9208daac6523708f9439958397b21b95c6c4fe508b"}, - {file = "rapidfuzz-2.0.7-cp38-cp38-win32.whl", hash = "sha256:a95a45939cbd035c2d4779765a81485215a12fa5f1b912c2738374fad93e753d"}, - {file = "rapidfuzz-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:14234ecc57e1799e24c9dcd230bba02630c4f38ca60c0eb075452313da8e0e95"}, - {file = "rapidfuzz-2.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9959374974fb96d3941334f5f8caeea971ea9718279514748c53d381146c5a7"}, - {file = "rapidfuzz-2.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2a988b5ff46823e0d5e14b4a1cce3ef13024009115df61d1d3b7ba14678f421"}, - {file = "rapidfuzz-2.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:603d179205972ebb5b01e7a84ead465d08813d50401216d5cc81fc2589e2c957"}, - {file = "rapidfuzz-2.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a934734aa247f57c683932ae0d38653063b2d97540598b551294b40ff242bd62"}, - {file = "rapidfuzz-2.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c42064174035f3633f4a815c38a76514875ca8531fac3f992202a41d1f338a41"}, - {file = "rapidfuzz-2.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46a46b8bab2ceee4877dfb281e94a43197b118d96cb04325e07540f7f9c57324"}, - {file = "rapidfuzz-2.0.7-cp39-cp39-win32.whl", hash = "sha256:1f892f3dd0acfbc2ba0b90d72cac42dd468ac9a8f7ac2179c91c29c22a4f7960"}, - {file = "rapidfuzz-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:233024373cb77dc2ef510b5fccac0429edb3294ea631ad777a7e3ff614501578"}, - {file = "rapidfuzz-2.0.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be8121175e7096062a312b73823385389635c4dec50a9e0496b29c4ba0b50362"}, - {file = "rapidfuzz-2.0.7-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad5282cf9921c6dbfe1c58e5af05c3014eabc20afd8fafcc0e6a56e9263875a0"}, - {file = "rapidfuzz-2.0.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67d650e25a7c281127865cc50c3588d5319200c8a11837df51ab3eead7cf066"}, - {file = "rapidfuzz-2.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07ccd298a24de2dadead47e75f23ff747ed3ee551964a8401ccae31a577cebb1"}, - {file = "rapidfuzz-2.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1e70ec13c00a9f28cce76a29eb5c4e6aeb5dadb9ddb35b74dfe05d503c09a4a"}, - {file = "rapidfuzz-2.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c47fda63c0d9d8275b319cdc226f96b3f1c16a395409442bff566b6de6b7cac9"}, - {file = "rapidfuzz-2.0.7.tar.gz", hash = "sha256:93bf42784fd74ebf1a8e89ca1596e9bea7f3ac4a61b825ecc6eb2d9893ad6844"}, -] -redis = [ - {file = "redis-4.3.1-py3-none-any.whl", hash = "sha256:84316970995a7adb907a56754d2b92d88fc2d252963dc5ac34c88f0f1a22c25d"}, - {file = "redis-4.3.1.tar.gz", hash = "sha256:94b617b4cd296e94991146f66fc5559756fbefe9493604f0312e4d3298ac63e9"}, -] -regex = [ - {file = "regex-2022.3.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42eb13b93765c6698a5ab3bcd318d8c39bb42e5fa8a7fcf7d8d98923f3babdb1"}, - {file = "regex-2022.3.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9beb03ff6fe509d6455971c2489dceb31687b38781206bcec8e68bdfcf5f1db2"}, - {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0a5a1fdc9f148a8827d55b05425801acebeeefc9e86065c7ac8b8cc740a91ff"}, - {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb374a2a4dba7c4be0b19dc7b1adc50e6c2c26c3369ac629f50f3c198f3743a4"}, - {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c33ce0c665dd325200209340a88438ba7a470bd5f09f7424e520e1a3ff835b52"}, - {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04c09b9651fa814eeeb38e029dc1ae83149203e4eeb94e52bb868fadf64852bc"}, - {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab5d89cfaf71807da93c131bb7a19c3e19eaefd613d14f3bce4e97de830b15df"}, - {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e2630ae470d6a9f8e4967388c1eda4762706f5750ecf387785e0df63a4cc5af"}, - {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:df037c01d68d1958dad3463e2881d3638a0d6693483f58ad41001aa53a83fcea"}, - {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:940570c1a305bac10e8b2bc934b85a7709c649317dd16520471e85660275083a"}, - {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f63877c87552992894ea1444378b9c3a1d80819880ae226bb30b04789c0828c"}, - {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3e265b388cc80c7c9c01bb4f26c9e536c40b2c05b7231fbb347381a2e1c8bf43"}, - {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:058054c7a54428d5c3e3739ac1e363dc9347d15e64833817797dc4f01fb94bb8"}, - {file = "regex-2022.3.15-cp310-cp310-win32.whl", hash = "sha256:76435a92e444e5b8f346aed76801db1c1e5176c4c7e17daba074fbb46cb8d783"}, - {file = "regex-2022.3.15-cp310-cp310-win_amd64.whl", hash = "sha256:174d964bc683b1e8b0970e1325f75e6242786a92a22cedb2a6ec3e4ae25358bd"}, - {file = "regex-2022.3.15-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e1d8ed9e61f37881c8db383a124829a6e8114a69bd3377a25aecaeb9b3538f8"}, - {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b52771f05cff7517f7067fef19ffe545b1f05959e440d42247a17cd9bddae11b"}, - {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:673f5a393d603c34477dbad70db30025ccd23996a2d0916e942aac91cc42b31a"}, - {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8923e1c5231549fee78ff9b2914fad25f2e3517572bb34bfaa3aea682a758683"}, - {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764e66a0e382829f6ad3bbce0987153080a511c19eb3d2f8ead3f766d14433ac"}, - {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd00859291658fe1fda48a99559fb34da891c50385b0bfb35b808f98956ef1e7"}, - {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa2ce79f3889720b46e0aaba338148a1069aea55fda2c29e0626b4db20d9fcb7"}, - {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:34bb30c095342797608727baf5c8aa122406aa5edfa12107b8e08eb432d4c5d7"}, - {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:25ecb1dffc5e409ca42f01a2b2437f93024ff1612c1e7983bad9ee191a5e8828"}, - {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:aa5eedfc2461c16a092a2fabc5895f159915f25731740c9152a1b00f4bcf629a"}, - {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:7d1a6e403ac8f1d91d8f51c441c3f99367488ed822bda2b40836690d5d0059f5"}, - {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3e4d710ff6539026e49f15a3797c6b1053573c2b65210373ef0eec24480b900b"}, - {file = "regex-2022.3.15-cp36-cp36m-win32.whl", hash = "sha256:0100f0ded953b6b17f18207907159ba9be3159649ad2d9b15535a74de70359d3"}, - {file = "regex-2022.3.15-cp36-cp36m-win_amd64.whl", hash = "sha256:f320c070dea3f20c11213e56dbbd7294c05743417cde01392148964b7bc2d31a"}, - {file = "regex-2022.3.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fc8c7958d14e8270171b3d72792b609c057ec0fa17d507729835b5cff6b7f69a"}, - {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ca6dcd17f537e9f3793cdde20ac6076af51b2bd8ad5fe69fa54373b17b48d3c"}, - {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0214ff6dff1b5a4b4740cfe6e47f2c4c92ba2938fca7abbea1359036305c132f"}, - {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a98ae493e4e80b3ded6503ff087a8492db058e9c68de371ac3df78e88360b374"}, - {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b1cc70e31aacc152a12b39245974c8fccf313187eead559ee5966d50e1b5817"}, - {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4829db3737480a9d5bfb1c0320c4ee13736f555f53a056aacc874f140e98f64"}, - {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:303b15a3d32bf5fe5a73288c316bac5807587f193ceee4eb6d96ee38663789fa"}, - {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:dc7b7c16a519d924c50876fb152af661a20749dcbf653c8759e715c1a7a95b18"}, - {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce3057777a14a9a1399b81eca6a6bfc9612047811234398b84c54aeff6d536ea"}, - {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:48081b6bff550fe10bcc20c01cf6c83dbca2ccf74eeacbfac240264775fd7ecf"}, - {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dcbb7665a9db9f8d7642171152c45da60e16c4f706191d66a1dc47ec9f820aed"}, - {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c155a1a80c5e7a8fa1d9bb1bf3c8a953532b53ab1196092749bafb9d3a7cbb60"}, - {file = "regex-2022.3.15-cp37-cp37m-win32.whl", hash = "sha256:04b5ee2b6d29b4a99d38a6469aa1db65bb79d283186e8460542c517da195a8f6"}, - {file = "regex-2022.3.15-cp37-cp37m-win_amd64.whl", hash = "sha256:797437e6024dc1589163675ae82f303103063a0a580c6fd8d0b9a0a6708da29e"}, - {file = "regex-2022.3.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8afcd1c2297bc989dceaa0379ba15a6df16da69493635e53431d2d0c30356086"}, - {file = "regex-2022.3.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0066a6631c92774391f2ea0f90268f0d82fffe39cb946f0f9c6b382a1c61a5e5"}, - {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8248f19a878c72d8c0a785a2cd45d69432e443c9f10ab924c29adda77b324ae"}, - {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d1f3ea0d1924feb4cf6afb2699259f658a08ac6f8f3a4a806661c2dfcd66db1"}, - {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:794a6bc66c43db8ed06698fc32aaeaac5c4812d9f825e9589e56f311da7becd9"}, - {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d1445824944e642ffa54c4f512da17a953699c563a356d8b8cbdad26d3b7598"}, - {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f553a1190ae6cd26e553a79f6b6cfba7b8f304da2071052fa33469da075ea625"}, - {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:75a5e6ce18982f0713c4bac0704bf3f65eed9b277edd3fb9d2b0ff1815943327"}, - {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f16cf7e4e1bf88fecf7f41da4061f181a6170e179d956420f84e700fb8a3fd6b"}, - {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dad3991f0678facca1a0831ec1ddece2eb4d1dd0f5150acb9440f73a3b863907"}, - {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:491fc754428514750ab21c2d294486223ce7385446f2c2f5df87ddbed32979ae"}, - {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:6504c22c173bb74075d7479852356bb7ca80e28c8e548d4d630a104f231e04fb"}, - {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01c913cf573d1da0b34c9001a94977273b5ee2fe4cb222a5d5b320f3a9d1a835"}, - {file = "regex-2022.3.15-cp38-cp38-win32.whl", hash = "sha256:029e9e7e0d4d7c3446aa92474cbb07dafb0b2ef1d5ca8365f059998c010600e6"}, - {file = "regex-2022.3.15-cp38-cp38-win_amd64.whl", hash = "sha256:947a8525c0a95ba8dc873191f9017d1b1e3024d4dc757f694e0af3026e34044a"}, - {file = "regex-2022.3.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:591d4fba554f24bfa0421ba040cd199210a24301f923ed4b628e1e15a1001ff4"}, - {file = "regex-2022.3.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9809404528a999cf02a400ee5677c81959bc5cb938fdc696b62eb40214e3632"}, - {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f08a7e4d62ea2a45557f561eea87c907222575ca2134180b6974f8ac81e24f06"}, - {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a86cac984da35377ca9ac5e2e0589bd11b3aebb61801204bd99c41fac516f0d"}, - {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:286908cbe86b1a0240a867aecfe26a439b16a1f585d2de133540549831f8e774"}, - {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b7494df3fdcc95a1f76cf134d00b54962dd83189520fd35b8fcd474c0aa616d"}, - {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b1ceede92400b3acfebc1425937454aaf2c62cd5261a3fabd560c61e74f6da3"}, - {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0317eb6331146c524751354ebef76a7a531853d7207a4d760dfb5f553137a2a4"}, - {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c144405220c5ad3f5deab4c77f3e80d52e83804a6b48b6bed3d81a9a0238e4c"}, - {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b2e24f3ae03af3d8e8e6d824c891fea0ca9035c5d06ac194a2700373861a15c"}, - {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f2c53f3af011393ab5ed9ab640fa0876757498aac188f782a0c620e33faa2a3d"}, - {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:060f9066d2177905203516c62c8ea0066c16c7342971d54204d4e51b13dfbe2e"}, - {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:530a3a16e57bd3ea0dff5ec2695c09632c9d6c549f5869d6cf639f5f7153fb9c"}, - {file = "regex-2022.3.15-cp39-cp39-win32.whl", hash = "sha256:78ce90c50d0ec970bd0002462430e00d1ecfd1255218d52d08b3a143fe4bde18"}, - {file = "regex-2022.3.15-cp39-cp39-win_amd64.whl", hash = "sha256:c5adc854764732dbd95a713f2e6c3e914e17f2ccdc331b9ecb777484c31f73b6"}, - {file = "regex-2022.3.15.tar.gz", hash = "sha256:0a7b75cc7bb4cc0334380053e4671c560e31272c9d2d5a6c4b8e9ae2c9bd0f82"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -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.5.8.tar.gz", hash = "sha256:38fd16a92b5ef94203db3ece10e03bdaa291481dd7e00e77a148aa0302267d47"}, - {file = "sentry_sdk-1.5.8-py2.py3-none-any.whl", hash = "sha256:32af1a57954576709242beb8c373b3dbde346ac6bd616921def29d68846fb8c3"}, -] -sgmllib3k = [ - {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -sortedcontainers = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {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"}, -] -statsd = [ - {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, - {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, -] -taskipy = [ - {file = "taskipy-1.10.1-py3-none-any.whl", hash = "sha256:9b38333654da487b6d16de6fa330b7629d1935d1e74819ba4c5f17a1c372d37b"}, - {file = "taskipy-1.10.1.tar.gz", hash = "sha256:6fa0b11c43d103e376063e90be31d87b435aad50fb7dc1c9a2de9b60a85015ed"}, -] -testfixtures = [ - {file = "testfixtures-6.18.5-py2.py3-none-any.whl", hash = "sha256:7de200e24f50a4a5d6da7019fb1197aaf5abd475efb2ec2422fdcf2f2eb98c1d"}, - {file = "testfixtures-6.18.5.tar.gz", hash = "sha256:02dae883f567f5b70fd3ad3c9eefb95912e78ac90be6c7444b5e2f46bf572c84"}, -] -tldextract = [ - {file = "tldextract-3.2.0-py3-none-any.whl", hash = "sha256:427703b65db54644f7b81d3dcb79bf355c1a7c28a12944e5cc6787531ccc828a"}, - {file = "tldextract-3.2.0.tar.gz", hash = "sha256:3d4b6a2105600b7d0290ea237bf30b6b0dc763e50fcbe40e849a019bd6dbcbff"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, -] +markdownify = [] +mccabe = [] +more-itertools = [] +mslex = [] +multidict = [] +nodeenv = [] +ordered-set = [] +packaging = [] +pep8-naming = [] +pip-licenses = [] +platformdirs = [] +pluggy = [] +pre-commit = [] +psutil = [] +ptable = [] +py = [] +pycares = [] +pycodestyle = [] +pycparser = [] +pydocstyle = [] +pyflakes = [] +pyparsing = [] +pyreadline3 = [] +pytest = [] +pytest-cov = [] +pytest-forked = [] +pytest-xdist = [] +python-dateutil = [] +python-dotenv = [] +python-frontmatter = [] +pyyaml = [] +rapidfuzz = [] +redis = [] +regex = [] +requests = [] +requests-file = [] +sentry-sdk = [] +sgmllib3k = [] +six = [] +snowballstemmer = [] +sortedcontainers = [] +soupsieve = [] +statsd = [] +taskipy = [] +testfixtures = [] +tldextract = [] +toml = [] +tomli = [] urllib3 = [] -virtualenv = [ - {file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"}, - {file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"}, -] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] -yarl = [ - {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, - {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, - {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, - {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, - {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, - {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, - {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, - {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, - {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, - {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, - {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, - {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, - {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, - {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, - {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, - {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, -] +virtualenv = [] +wrapt = [] +yarl = [] diff --git a/pyproject.toml b/pyproject.toml index af3fdc115..a44f0214a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,16 +8,13 @@ license = "MIT" [tool.poetry.dependencies] python = "3.10.*" -"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/0eb3d26343969a25ffc43ba72eca42538d2e7e7a.zip"} # See https://bot-core.pythondiscord.com/ for docs. -bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.4.0.zip", extras = ["async-rediscache"]} +bot-core = { url = "https://github.com/python-discord/bot-core/archive/refs/tags/v8.0.0-beta.4.zip", extras = ["async-rediscache"] } +redis = "4.3.4" +fakeredis = { version = "1.8.2", extras = ["lua"] } -aiodns = "3.0.0" aiohttp = "3.8.1" -aioredis = "1.3.1" -fakeredis = "1.7.5" arrow = "1.2.2" -async-rediscache = { version = "0.2.0", extras = ["fakeredis"] } beautifulsoup4 = "4.10.0" colorama = { version = "0.4.4", markers = "sys_platform == 'win32'" } coloredlogs = "15.0.1" -- cgit v1.2.3 From b1310afba5280ec74d39a7190beecfd91a32641d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 25 Jul 2022 23:20:06 +0100 Subject: Bump all deps to latest --- poetry.lock | 105 ++++++++++++++++++++++----------------------------------- pyproject.toml | 42 +++++++++++------------ 2 files changed, 62 insertions(+), 85 deletions(-) diff --git a/poetry.lock b/poetry.lock index e7f82d207..771416bf5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -98,11 +98,11 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "beautifulsoup4" -version = "4.10.0" +version = "4.11.1" description = "Screen-scraping library" category = "main" optional = false -python-versions = ">3.0.0" +python-versions = ">=3.6.0" [package.dependencies] soupsieve = ">1.2" @@ -171,7 +171,7 @@ unicode_backport = ["unicodedata2"] [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "main" optional = false @@ -193,28 +193,28 @@ cron = ["capturer (>=2.4)"] [[package]] name = "coverage" -version = "6.3.2" +version = "6.4.2" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -tomli = {version = "*", optional = true, markers = "extra == \"toml\""} +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "deepdiff" -version = "5.7.0" +version = "5.8.1" description = "Deep Difference and Search of any Python object/data." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -ordered-set = "4.0.2" +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)"] @@ -264,7 +264,7 @@ python-versions = "*" [[package]] name = "emoji" -version = "1.7.0" +version = "2.0.0" description = "Emoji for Python" category = "main" optional = false @@ -304,7 +304,7 @@ lua = ["lupa (>=1.13,<2.0)"] [[package]] name = "feedparser" -version = "6.0.8" +version = "6.0.10" description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" category = "main" optional = false @@ -340,7 +340,7 @@ pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "flake8-annotations" -version = "2.8.0" +version = "2.9.0" description = "Flake8 Type Annotation Checks" category = "dev" optional = false @@ -352,7 +352,7 @@ flake8 = ">=3.7" [[package]] name = "flake8-bugbear" -version = "22.3.23" +version = "22.7.1" 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 @@ -379,7 +379,7 @@ pydocstyle = ">=2.1" [[package]] name = "flake8-isort" -version = "4.1.1" +version = "4.1.2.post0" description = "flake8 plugin that integrates isort ." category = "dev" optional = false @@ -388,22 +388,10 @@ python-versions = "*" [package.dependencies] flake8 = ">=3.2.1,<5" isort = ">=4.3.5,<6" -testfixtures = ">=6.8.0,<7" [package.extras] test = ["pytest-cov"] -[[package]] -name = "flake8-polyfill" -version = "1.0.2" -description = "Polyfill package for Flake8 plugins" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -flake8 = "*" - [[package]] name = "flake8-string-format" version = "0.3.0" @@ -417,7 +405,7 @@ flake8 = "*" [[package]] name = "flake8-tidy-imports" -version = "4.6.0" +version = "4.8.0" description = "A flake8 plugin that helps you write tidier imports." category = "dev" optional = false @@ -499,7 +487,7 @@ plugins = ["setuptools"] [[package]] name = "jarowinkler" -version = "1.0.5" +version = "1.2.0" description = "library for fast approximate string matching using Jaro and Jaro-Winkler similarity" category = "main" optional = false @@ -529,15 +517,15 @@ source = ["Cython (>=0.29.7)"] [[package]] name = "markdownify" -version = "0.6.1" +version = "0.11.2" description = "Convert HTML to markdown." category = "main" optional = false python-versions = "*" [package.dependencies] -beautifulsoup4 = "*" -six = "*" +beautifulsoup4 = ">=4.9,<5" +six = ">=1.15,<2" [[package]] name = "mccabe" @@ -549,7 +537,7 @@ python-versions = "*" [[package]] name = "more-itertools" -version = "8.12.0" +version = "8.13.0" description = "More routines for operating on iterables, beyond itertools" category = "main" optional = false @@ -581,11 +569,14 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.* [[package]] name = "ordered-set" -version = "4.0.2" -description = "A set that remembers its order, and allows looking up its items by their index in that order." +version = "4.1.0" +description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" + +[package.extras] +dev = ["pytest", "black", "mypy"] [[package]] name = "packaging" @@ -600,7 +591,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pep8-naming" -version = "0.12.1" +version = "0.13.1" description = "Check PEP-8 naming conventions, plugin for flake8" category = "dev" optional = false @@ -608,11 +599,10 @@ python-versions = "*" [package.dependencies] flake8 = ">=3.9.1" -flake8-polyfill = ">=1.0.2,<2" [[package]] name = "pip-licenses" -version = "3.5.3" +version = "3.5.4" description = "Dump the software license list of Python packages installed with pip." category = "dev" optional = false @@ -650,11 +640,11 @@ dev = ["tox", "pre-commit"] [[package]] name = "pre-commit" -version = "2.17.0" +version = "2.20.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" @@ -764,7 +754,7 @@ python-versions = "*" [[package]] name = "pytest" -version = "7.1.1" +version = "7.1.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -875,14 +865,14 @@ python-versions = ">=3.6" [[package]] name = "rapidfuzz" -version = "2.0.7" +version = "2.3.0" description = "rapid fuzzy string matching" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -jarowinkler = ">=1.0.2,<1.1.0" +jarowinkler = ">=1.2.0,<2.0.0" [package.extras] full = ["numpy"] @@ -906,7 +896,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "regex" -version = "2022.3.15" +version = "2022.7.25" description = "Alternative regular expression module, to replace re." category = "main" optional = false @@ -944,7 +934,7 @@ six = "*" [[package]] name = "sentry-sdk" -version = "1.5.8" +version = "1.8.0" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false @@ -962,6 +952,7 @@ celery = ["celery (>=3)"] chalice = ["chalice (>=1.16.0)"] django = ["django (>=1.8)"] falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] flask = ["flask (>=0.11)", "blinker (>=1.1)"] httpx = ["httpx (>=0.16.0)"] pure_eval = ["pure-eval", "executing", "asttokens"] @@ -970,6 +961,7 @@ quart = ["quart (>=0.16.1)", "blinker (>=1.1)"] rq = ["rq (>=0.6)"] sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] tornado = ["tornado (>=5)"] [[package]] @@ -1022,7 +1014,7 @@ python-versions = "*" [[package]] name = "taskipy" -version = "1.10.1" +version = "1.10.2" description = "tasks runner for python projects" category = "dev" optional = false @@ -1032,25 +1024,12 @@ python-versions = ">=3.6,<4.0" colorama = ">=0.4.4,<0.5.0" mslex = {version = ">=0.3.0,<0.4.0", markers = "sys_platform == \"win32\""} psutil = ">=5.7.2,<6.0.0" -tomli = ">=1.2.3,<2.0.0" - -[[package]] -name = "testfixtures" -version = "6.18.5" -description = "A collection of helpers and mock objects for unit tests and doc tests." -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -build = ["setuptools-git", "wheel", "twine"] -docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] -test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] +tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} [[package]] name = "tldextract" -version = "3.2.0" -description = "Accurately separate the TLD from the registered domain and subdomains of a URL, using the Public Suffix List. By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." +version = "3.3.1" +description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." category = "main" optional = false python-versions = ">=3.7" @@ -1071,11 +1050,11 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.3" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "urllib3" @@ -1165,7 +1144,6 @@ flake8-annotations = [] flake8-bugbear = [] flake8-docstrings = [] flake8-isort = [] -flake8-polyfill = [] flake8-string-format = [] flake8-tidy-imports = [] flake8-todo = [] @@ -1222,7 +1200,6 @@ sortedcontainers = [] soupsieve = [] statsd = [] taskipy = [] -testfixtures = [] tldextract = [] toml = [] tomli = [] diff --git a/pyproject.toml b/pyproject.toml index a44f0214a..917ea8c46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,40 +15,40 @@ fakeredis = { version = "1.8.2", extras = ["lua"] } aiohttp = "3.8.1" arrow = "1.2.2" -beautifulsoup4 = "4.10.0" -colorama = { version = "0.4.4", markers = "sys_platform == 'win32'" } +beautifulsoup4 = "4.11.1" +colorama = { version = "0.4.5", markers = "sys_platform == 'win32'" } coloredlogs = "15.0.1" -deepdiff = "5.7.0" -emoji = "1.7.0" -feedparser = "6.0.8" -rapidfuzz = "2.0.7" +deepdiff = "5.8.1" +emoji = "2.0.0" +feedparser = "6.0.10" +rapidfuzz = "2.3.0" lxml = "4.9.1" -markdownify = "0.6.1" -more_itertools = "8.12.0" +markdownify = "0.11.2" +more_itertools = "8.13.0" python-dateutil = "2.8.2" python-frontmatter = "1.0.0" pyyaml = "6.0" -regex = "2022.3.15" -sentry-sdk = "1.5.8" +regex = "2022.7.25" +sentry-sdk = "1.8.0" statsd = "3.3.0" -tldextract = "3.2.0" +tldextract = "3.3.1" [tool.poetry.dev-dependencies] -coverage = "6.3.2" +coverage = "6.4.2" flake8 = "4.0.1" -flake8-annotations = "2.8.0" -flake8-bugbear = "22.3.23" +flake8-annotations = "2.9.0" +flake8-bugbear = "22.7.1" flake8-docstrings = "1.6.0" flake8-string-format = "0.3.0" -flake8-tidy-imports = "4.6.0" +flake8-tidy-imports = "4.8.0" flake8-todo = "0.7" -flake8-isort = "4.1.1" -pep8-naming = "0.12.1" -pre-commit = "2.17.0" -taskipy = "1.10.1" -pip-licenses = "3.5.3" +flake8-isort = "4.1.2.post0" +pep8-naming = "0.13.1" +pre-commit = "2.20.0" +taskipy = "1.10.2" +pip-licenses = "3.5.4" python-dotenv = "0.20.0" -pytest = "7.1.1" +pytest = "7.1.2" pytest-cov = "3.0.0" pytest-xdist = "2.5.0" -- cgit v1.2.3 From 1eafa20702ce3ceb0f76c71c9ab24629e447e1fb Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 25 Jul 2022 23:31:14 +0100 Subject: redis-py breaking changes This commit resolves all the breaking changes from the aioredis -> redis-py migration. --- bot/__main__.py | 9 +++++---- bot/exts/moderation/defcon.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bot/__main__.py b/bot/__main__.py index fc4475068..46850e78e 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -6,6 +6,7 @@ from async_rediscache import RedisSession from botcore import StartupError from botcore.site_api import APIClient from discord.ext import commands +from redis import RedisError import bot from bot import constants @@ -19,16 +20,16 @@ LOCALHOST = "127.0.0.1" async def _create_redis_session() -> RedisSession: """Create and connect to a redis session.""" redis_session = RedisSession( - address=(constants.Redis.host, constants.Redis.port), + host=constants.Redis.host, + port=constants.Redis.port, password=constants.Redis.password, - minsize=1, - maxsize=20, + max_connections=20, use_fakeredis=constants.Redis.use_fakeredis, global_namespace="bot", ) try: await redis_session.connect() - except OSError as e: + except RedisError as e: raise StartupError(e) return redis_session diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 1df79149d..7c924ff14 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -6,7 +6,6 @@ from enum import Enum from typing import Optional, Union import arrow -from aioredis import RedisError from async_rediscache import RedisCache from botcore.utils import scheduling from botcore.utils.scheduling import Scheduler @@ -14,6 +13,7 @@ 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 redis import RedisError from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles -- cgit v1.2.3 From 7782c196830098f81f39d235354636cd0d4a481d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:52:52 +0100 Subject: No longer use the removed RedisSession connection object This has been abstracted away, the correct way to do this now is to directly access the client. --- bot/exts/info/doc/_redis_cache.py | 92 ++++++++++++++--------------- tests/bot/exts/moderation/test_incidents.py | 5 +- 2 files changed, 46 insertions(+), 51 deletions(-) diff --git a/bot/exts/info/doc/_redis_cache.py b/bot/exts/info/doc/_redis_cache.py index 8e08e7ae4..0f4d663d1 100644 --- a/bot/exts/info/doc/_redis_cache.py +++ b/bot/exts/info/doc/_redis_cache.py @@ -34,55 +34,52 @@ class DocRedisCache(RedisObject): redis_key = f"{self.namespace}:{item_key(item)}" needs_expire = False - with await self._get_pool_connection() as connection: - set_expire = self._set_expires.get(redis_key) - if set_expire is None: - # An expire is only set if the key didn't exist before. - ttl = await connection.ttl(redis_key) - log.debug(f"Checked TTL for `{redis_key}`.") - - if ttl == -1: - log.warning(f"Key `{redis_key}` had no expire set.") - if ttl < 0: # not set or didn't exist - needs_expire = True - else: - log.debug(f"Key `{redis_key}` has a {ttl} TTL.") - self._set_expires[redis_key] = time.monotonic() + ttl - .1 # we need this to expire before redis - - elif time.monotonic() > set_expire: - # If we got here the key expired in redis and we can be sure it doesn't exist. + set_expire = self._set_expires.get(redis_key) + if set_expire is None: + # An expire is only set if the key didn't exist before. + ttl = await self.redis_session.client.ttl(redis_key) + log.debug(f"Checked TTL for `{redis_key}`.") + + if ttl == -1: + log.warning(f"Key `{redis_key}` had no expire set.") + if ttl < 0: # not set or didn't exist needs_expire = True - log.debug(f"Key `{redis_key}` expired in internal key cache.") + else: + log.debug(f"Key `{redis_key}` has a {ttl} TTL.") + self._set_expires[redis_key] = time.monotonic() + ttl - .1 # we need this to expire before redis - await connection.hset(redis_key, item.symbol_id, value) - if needs_expire: - self._set_expires[redis_key] = time.monotonic() + WEEK_SECONDS - await connection.expire(redis_key, WEEK_SECONDS) - log.info(f"Set {redis_key} to expire in a week.") + elif time.monotonic() > set_expire: + # If we got here the key expired in redis and we can be sure it doesn't exist. + needs_expire = True + log.debug(f"Key `{redis_key}` expired in internal key cache.") + + await self.redis_session.client.hset(redis_key, item.symbol_id, value) + if needs_expire: + self._set_expires[redis_key] = time.monotonic() + WEEK_SECONDS + await self.redis_session.client.expire(redis_key, WEEK_SECONDS) + log.info(f"Set {redis_key} to expire in a week.") @namespace_lock async def get(self, item: DocItem) -> Optional[str]: """Return the Markdown content of the symbol `item` if it exists.""" - with await self._get_pool_connection() as connection: - return await connection.hget(f"{self.namespace}:{item_key(item)}", item.symbol_id, encoding="utf8") + return await self.redis_session.client.hget(f"{self.namespace}:{item_key(item)}", item.symbol_id) @namespace_lock async def delete(self, package: str) -> bool: """Remove all values for `package`; return True if at least one key was deleted, False otherwise.""" pattern = f"{self.namespace}:{package}:*" - with await self._get_pool_connection() as connection: - package_keys = [ - package_key async for package_key in connection.iscan(match=pattern) - ] - if package_keys: - await connection.delete(*package_keys) - log.info(f"Deleted keys from redis: {package_keys}.") - self._set_expires = { - key: expire for key, expire in self._set_expires.items() if not fnmatch.fnmatchcase(key, pattern) - } - return True - return False + package_keys = [ + package_key async for package_key in self.redis_session.client.iscan(match=pattern) + ] + if package_keys: + await self.redis_session.client.delete(*package_keys) + log.info(f"Deleted keys from redis: {package_keys}.") + self._set_expires = { + key: expire for key, expire in self._set_expires.items() if not fnmatch.fnmatchcase(key, pattern) + } + return True + return False class StaleItemCounter(RedisObject): @@ -96,21 +93,20 @@ class StaleItemCounter(RedisObject): If the counter didn't exist, initialize it with 1. """ key = f"{self.namespace}:{item_key(item)}:{item.symbol_id}" - with await self._get_pool_connection() as connection: - await connection.expire(key, WEEK_SECONDS * 3) - return int(await connection.incr(key)) + await self.redis_session.client.expire(key, WEEK_SECONDS * 3) + return int(await self.redis_session.client.incr(key)) @namespace_lock async def delete(self, package: str) -> bool: """Remove all values for `package`; return True if at least one key was deleted, False otherwise.""" - with await self._get_pool_connection() as connection: - package_keys = [ - package_key async for package_key in connection.iscan(match=f"{self.namespace}:{package}:*") - ] - if package_keys: - await connection.delete(*package_keys) - return True - return False + package_keys = [ + package_key + async for package_key in self.redis_session.client.iscan(match=f"{self.namespace}:{package}:*") + ] + if package_keys: + await self.redis_session.client.delete(*package_keys) + return True + return False def item_key(item: DocItem) -> str: diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index cfe0c4b03..f60c177c5 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -283,8 +283,7 @@ class TestIncidents(unittest.IsolatedAsyncioTestCase): async def flush(self): """Flush everything from the database to prevent carry-overs between tests.""" - with await self.session.pool as connection: - await connection.flushall() + await self.session.client.flushall() async def asyncSetUp(self): # noqa: N802 self.session = RedisSession(use_fakeredis=True) @@ -293,7 +292,7 @@ class TestIncidents(unittest.IsolatedAsyncioTestCase): async def asyncTearDown(self): # noqa: N802 if self.session: - await self.session.close() + await self.session.client.close() def setUp(self): """ -- cgit v1.2.3 From f0d045e685525acca13f52e65d22a6140ffbfc05 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:53:52 +0100 Subject: Remove usages of the removed namespace_lock decorator --- bot/exts/info/doc/_redis_cache.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bot/exts/info/doc/_redis_cache.py b/bot/exts/info/doc/_redis_cache.py index 0f4d663d1..9ac8492e9 100644 --- a/bot/exts/info/doc/_redis_cache.py +++ b/bot/exts/info/doc/_redis_cache.py @@ -5,7 +5,7 @@ import fnmatch import time from typing import Optional, TYPE_CHECKING -from async_rediscache.types.base import RedisObject, namespace_lock +from async_rediscache.types.base import RedisObject from bot.log import get_logger @@ -24,7 +24,6 @@ class DocRedisCache(RedisObject): super().__init__(*args, **kwargs) self._set_expires = dict[str, float]() - @namespace_lock async def set(self, item: DocItem, value: str) -> None: """ Set the Markdown `value` for the symbol `item`. @@ -59,12 +58,10 @@ class DocRedisCache(RedisObject): await self.redis_session.client.expire(redis_key, WEEK_SECONDS) log.info(f"Set {redis_key} to expire in a week.") - @namespace_lock async def get(self, item: DocItem) -> Optional[str]: """Return the Markdown content of the symbol `item` if it exists.""" return await self.redis_session.client.hget(f"{self.namespace}:{item_key(item)}", item.symbol_id) - @namespace_lock async def delete(self, package: str) -> bool: """Remove all values for `package`; return True if at least one key was deleted, False otherwise.""" pattern = f"{self.namespace}:{package}:*" @@ -85,7 +82,6 @@ class DocRedisCache(RedisObject): class StaleItemCounter(RedisObject): """Manage increment counters for stale `DocItem`s.""" - @namespace_lock async def increment_for(self, item: DocItem) -> int: """ Increment the counter for `item` by 1, set it to expire in 3 weeks and return the new value. @@ -96,7 +92,6 @@ class StaleItemCounter(RedisObject): await self.redis_session.client.expire(key, WEEK_SECONDS * 3) return int(await self.redis_session.client.incr(key)) - @namespace_lock async def delete(self, package: str) -> bool: """Remove all values for `package`; return True if at least one key was deleted, False otherwise.""" package_keys = [ -- cgit v1.2.3 From 9c5d2e67e3ab76eeffeaa46682325bb3a81bf545 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:55:42 +0100 Subject: Convert key expiries to integers before passing to Redis If a float is given, Redis will assume the expiry is in milliseconds and must be multiplied by 1000. This is undesirable, as we are already passing the expiry in seconds. --- bot/exts/info/doc/_redis_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/doc/_redis_cache.py b/bot/exts/info/doc/_redis_cache.py index 9ac8492e9..eb06e9aef 100644 --- a/bot/exts/info/doc/_redis_cache.py +++ b/bot/exts/info/doc/_redis_cache.py @@ -12,7 +12,7 @@ from bot.log import get_logger if TYPE_CHECKING: from ._cog import DocItem -WEEK_SECONDS = datetime.timedelta(weeks=1).total_seconds() +WEEK_SECONDS = int(datetime.timedelta(weeks=1).total_seconds()) log = get_logger(__name__) -- cgit v1.2.3 From c291405fcbe92591a8f6deb5c1e0f4ba2dbf852d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 22:56:38 +0100 Subject: Update any calls to Redis 'iscan' to the new name 'scan_iter' --- bot/exts/info/doc/_redis_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/doc/_redis_cache.py b/bot/exts/info/doc/_redis_cache.py index eb06e9aef..2a2f5e4b4 100644 --- a/bot/exts/info/doc/_redis_cache.py +++ b/bot/exts/info/doc/_redis_cache.py @@ -67,7 +67,7 @@ class DocRedisCache(RedisObject): pattern = f"{self.namespace}:{package}:*" package_keys = [ - package_key async for package_key in self.redis_session.client.iscan(match=pattern) + package_key async for package_key in self.redis_session.client.scan_iter(match=pattern) ] if package_keys: await self.redis_session.client.delete(*package_keys) @@ -96,7 +96,7 @@ class StaleItemCounter(RedisObject): """Remove all values for `package`; return True if at least one key was deleted, False otherwise.""" package_keys = [ package_key - async for package_key in self.redis_session.client.iscan(match=f"{self.namespace}:{package}:*") + async for package_key in self.redis_session.client.scan_iter(match=f"{self.namespace}:{package}:*") ] if package_keys: await self.redis_session.client.delete(*package_keys) -- cgit v1.2.3 From 8ef517d9bf8d64f679fa3b071b6d723a11332548 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 23 Jul 2022 23:54:17 +0100 Subject: Add back a lock to DocRedisCache.set based on the DocItem --- bot/exts/info/doc/_redis_cache.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot/exts/info/doc/_redis_cache.py b/bot/exts/info/doc/_redis_cache.py index 2a2f5e4b4..ef9abd981 100644 --- a/bot/exts/info/doc/_redis_cache.py +++ b/bot/exts/info/doc/_redis_cache.py @@ -8,6 +8,7 @@ from typing import Optional, TYPE_CHECKING from async_rediscache.types.base import RedisObject from bot.log import get_logger +from bot.utils.lock import lock if TYPE_CHECKING: from ._cog import DocItem @@ -17,6 +18,12 @@ WEEK_SECONDS = int(datetime.timedelta(weeks=1).total_seconds()) log = get_logger(__name__) +def serialize_resource_id_from_doc_item(bound_args: dict) -> str: + """Return the redis_key of the DocItem `item` from the bound args of DocRedisCache.set.""" + item: DocItem = bound_args["item"] + return f"doc:{item_key(item)}" + + class DocRedisCache(RedisObject): """Interface for redis functionality needed by the Doc cog.""" @@ -24,6 +31,7 @@ class DocRedisCache(RedisObject): super().__init__(*args, **kwargs) self._set_expires = dict[str, float]() + @lock("DocRedisCache.set", serialize_resource_id_from_doc_item, wait=True) async def set(self, item: DocItem, value: str) -> None: """ Set the Markdown `value` for the symbol `item`. -- cgit v1.2.3 From 46da1ecf621a64e6d8f0a37572378ae363ba76a2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 25 Jul 2022 23:24:25 +0100 Subject: Stop creating futures in tests with no event loop running --- tests/bot/exts/moderation/test_silence.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 82ec138db..03b7b2fdb 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -250,8 +250,6 @@ class SilenceArgumentParserTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.bot = MockBot() self.cog = silence.Silence(self.bot) - self.cog._init_task = asyncio.Future() - self.cog._init_task.set_result(None) @autospec(silence.Silence, "send_message", pass_mocks=False) @autospec(silence.Silence, "_set_silence_overwrites", return_value=False, pass_mocks=False) @@ -413,8 +411,6 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.bot = MockBot(get_channel=lambda _: MockTextChannel()) self.cog = silence.Silence(self.bot) - self.cog._init_task = asyncio.Future() - self.cog._init_task.set_result(None) # Avoid unawaited coroutine warnings. self.cog.scheduler.schedule_later.side_effect = lambda delay, task_id, coro: coro.close() @@ -686,8 +682,6 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.bot = MockBot(get_channel=lambda _: MockTextChannel()) self.cog = silence.Silence(self.bot) - self.cog._init_task = asyncio.Future() - self.cog._init_task.set_result(None) overwrites_cache = mock.create_autospec(self.cog.previous_overwrites, spec_set=True) self.cog.previous_overwrites = overwrites_cache -- cgit v1.2.3 From ab7aafb5353c03efb6dbade30cf7ad8754c7a5aa Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 25 Jul 2022 23:50:19 +0100 Subject: noqa false-positive B023 instances This was a new lint rule added in the latest bugbear. --- bot/exts/backend/sync/_syncers.py | 4 ++-- bot/exts/filters/antispam.py | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py index 799137cb9..8976245e3 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -154,8 +154,8 @@ class UserSyncer(Syncer): def maybe_update(db_field: str, guild_value: t.Union[str, int]) -> None: # Equalize DB user and guild user attributes. - if db_user[db_field] != guild_value: - updated_fields[db_field] = guild_value + if db_user[db_field] != guild_value: # noqa: B023 + updated_fields[db_field] = guild_value # noqa: B023 guild_user = guild.get_member(db_user["id"]) if not guild_user and db_user["in_guild"]: diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index 3b925bacd..842aab384 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -185,7 +185,7 @@ class AntiSpam(Cog): # Create a list of messages that were sent in the interval that the rule cares about. latest_interesting_stamp = arrow.utcnow() - timedelta(seconds=rule_config['interval']) messages_for_rule = list( - takewhile(lambda msg: msg.created_at > latest_interesting_stamp, relevant_messages) + takewhile(lambda msg: msg.created_at > latest_interesting_stamp, relevant_messages) # noqa: B023 ) result = await rule_function(message, messages_for_rule, rule_config) diff --git a/tox.ini b/tox.ini index e864b4b3e..987b7c790 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ docstring-convention=all import-order-style=pycharm application_import_names=bot,tests exclude=.cache,.venv,.git,constants.py -ignore= +extend-ignore= B311,W503,E226,S311,T000,E731 # Missing Docstrings D100,D104,D105,D107, -- cgit v1.2.3 From ddb743856593c431bd6e4d0c440e7e0c7a3ed978 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 26 Jul 2022 00:24:26 +0100 Subject: Directly return the RedisSession on connection --- bot/__main__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/__main__.py b/bot/__main__.py index 46850e78e..e0d2e6ad5 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -28,10 +28,9 @@ async def _create_redis_session() -> RedisSession: global_namespace="bot", ) try: - await redis_session.connect() + return await redis_session.connect() except RedisError as e: raise StartupError(e) - return redis_session async def main() -> None: -- cgit v1.2.3 From 9cf3de3e9bf6725b2baa2e7adb77e058c216b332 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 26 Jul 2022 00:56:12 +0100 Subject: Remove unneeded N802 noqas pep-naming now supports these functions being in camel case. --- tests/bot/exts/moderation/test_incidents.py | 6 +++--- tests/bot/exts/moderation/test_silence.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index f60c177c5..211eb1bf8 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -285,12 +285,12 @@ class TestIncidents(unittest.IsolatedAsyncioTestCase): """Flush everything from the database to prevent carry-overs between tests.""" await self.session.client.flushall() - async def asyncSetUp(self): # noqa: N802 + async def asyncSetUp(self): self.session = RedisSession(use_fakeredis=True) await self.session.connect() await self.flush() - async def asyncTearDown(self): # noqa: N802 + async def asyncTearDown(self): if self.session: await self.session.client.close() @@ -655,7 +655,7 @@ class TestOnRawReactionAdd(TestIncidents): emoji="reaction", ) - async def asyncSetUp(self): # noqa: N802 + async def asyncSetUp(self): """ Prepare an empty task and assign it as `crawl_task`. diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 03b7b2fdb..f5caefdca 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -18,14 +18,14 @@ from tests.helpers import ( redis_session = None -def setUpModule(): # noqa: N802 +def setUpModule(): """Create and connect to the fakeredis session.""" global redis_session redis_session = RedisSession(use_fakeredis=True) asyncio.run(redis_session.connect()) -def tearDownModule(): # noqa: N802 +def tearDownModule(): """Close the fakeredis session.""" if redis_session: asyncio.run(redis_session.client.close()) -- cgit v1.2.3 From ecae820b49c06d5a838aadcccbe2fdad4fe58975 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 27 Jul 2022 18:44:16 +0100 Subject: Bump bot-core to full 8.0.0 release --- poetry.lock | 13 ++++++------- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 771416bf5..9962195d9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -113,7 +113,7 @@ lxml = ["lxml"] [[package]] name = "bot-core" -version = "8.0.0-beta.4" +version = "8.0.0" description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." category = "main" optional = false @@ -130,7 +130,7 @@ async-rediscache = ["async-rediscache[fakeredis] (==1.0.0rc2)"] [package.source] type = "url" -url = "https://github.com/python-discord/bot-core/archive/refs/tags/v8.0.0-beta.4.zip" +url = "https://github.com/python-discord/bot-core/archive/refs/tags/v8.0.0.zip" [[package]] name = "certifi" version = "2022.6.15" @@ -1071,21 +1071,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.15.1" +version = "20.16.1" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" platformdirs = ">=2,<3" -six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] [[package]] name = "wrapt" @@ -1110,7 +1109,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.10.*" -content-hash = "3b9ec0ebe5dea22a952b807abf941c2f7c3a3b97364504d289189e7e947fe34d" +content-hash = "a2b47c854b1fb55bde8618ce85d09ce6c92b48c1ae61d281ce49fc0260585090" [metadata.files] aiodns = [] diff --git a/pyproject.toml b/pyproject.toml index 917ea8c46..279543018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" python = "3.10.*" # See https://bot-core.pythondiscord.com/ for docs. -bot-core = { url = "https://github.com/python-discord/bot-core/archive/refs/tags/v8.0.0-beta.4.zip", extras = ["async-rediscache"] } +bot-core = { url = "https://github.com/python-discord/bot-core/archive/refs/tags/v8.0.0.zip", extras = ["async-rediscache"] } redis = "4.3.4" fakeredis = { version = "1.8.2", extras = ["lua"] } -- cgit v1.2.3 From 4a47c816641332fbb49f8c88c8a7720849cabf06 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 30 Jul 2022 19:28:36 +0100 Subject: Add a new test helper for managing redis sessions This helper ensures that a fresh RedisSession is given to each test case that inherits from it. --- tests/base.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/base.py b/tests/base.py index 5e304ea9d..4863a1821 100644 --- a/tests/base.py +++ b/tests/base.py @@ -4,6 +4,7 @@ from contextlib import contextmanager from typing import Dict import discord +from async_rediscache import RedisSession from discord.ext import commands from bot.log import get_logger @@ -104,3 +105,26 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await cmd.can_run(ctx) self.assertCountEqual(permissions.keys(), cm.exception.missing_permissions) + + +class RedisTestCase(unittest.IsolatedAsyncioTestCase): + """ + Use this as a base class for any test cases that require a redis session. + + This will prepare a fresh redis instance for each test function, and will + not make any assertions on its own. Tests can mutate the instance as they wish. + """ + + session = None + + async def flush(self): + """Flush everything from the redis database to prevent carry-overs between tests.""" + await self.session.client.flushall() + + async def asyncSetUp(self): + self.session = await RedisSession(use_fakeredis=True).connect() + await self.flush() + + async def asyncTearDown(self): + if self.session: + await self.session.client.close() -- cgit v1.2.3 From f044f36833e9dc003e89dd81868ea3f48a9da002 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 30 Jul 2022 19:29:27 +0100 Subject: Use RedisTestCase helper class for both Incidents and Silence test cases. --- tests/bot/exts/moderation/test_incidents.py | 19 ++----------------- tests/bot/exts/moderation/test_silence.py | 23 ++++------------------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index 211eb1bf8..97682163f 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -8,11 +8,11 @@ from unittest.mock import AsyncMock, MagicMock, Mock, call, patch import aiohttp import discord -from async_rediscache import RedisSession from bot.constants import Colours from bot.exts.moderation import incidents from bot.utils.messages import format_user +from tests.base import RedisTestCase from tests.helpers import ( MockAsyncWebhook, MockAttachment, MockBot, MockMember, MockMessage, MockReaction, MockRole, MockTextChannel, MockUser @@ -270,7 +270,7 @@ class TestAddSignals(unittest.IsolatedAsyncioTestCase): self.incident.add_reaction.assert_not_called() -class TestIncidents(unittest.IsolatedAsyncioTestCase): +class TestIncidents(RedisTestCase): """ Tests for bound methods of the `Incidents` cog. @@ -279,21 +279,6 @@ class TestIncidents(unittest.IsolatedAsyncioTestCase): the instance as they wish. """ - session = None - - async def flush(self): - """Flush everything from the database to prevent carry-overs between tests.""" - await self.session.client.flushall() - - async def asyncSetUp(self): - self.session = RedisSession(use_fakeredis=True) - await self.session.connect() - await self.flush() - - async def asyncTearDown(self): - if self.session: - await self.session.client.close() - def setUp(self): """ Prepare a fresh `Incidents` instance for each test. diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index f5caefdca..98547e2bc 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -6,30 +6,15 @@ from typing import List, Tuple from unittest import mock from unittest.mock import AsyncMock, Mock -from async_rediscache import RedisSession from discord import PermissionOverwrite from bot.constants import Channels, Guild, MODERATION_ROLES, Roles from bot.exts.moderation import silence +from tests.base import RedisTestCase from tests.helpers import ( MockBot, MockContext, MockGuild, MockMember, MockRole, MockTextChannel, MockVoiceChannel, autospec ) -redis_session = None - - -def setUpModule(): - """Create and connect to the fakeredis session.""" - global redis_session - redis_session = RedisSession(use_fakeredis=True) - asyncio.run(redis_session.connect()) - - -def tearDownModule(): - """Close the fakeredis session.""" - if redis_session: - asyncio.run(redis_session.client.close()) - # Have to subclass it because builtins can't be patched. class PatchedDatetime(datetime): @@ -104,7 +89,7 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) -class SilenceCogTests(unittest.IsolatedAsyncioTestCase): +class SilenceCogTests(RedisTestCase): """Tests for the general functionality of the Silence cog.""" @autospec(silence, "Scheduler", pass_mocks=False) @@ -244,7 +229,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(member.move_to.call_count, 1 if member == failing_member else 2) -class SilenceArgumentParserTests(unittest.IsolatedAsyncioTestCase): +class SilenceArgumentParserTests(RedisTestCase): """Tests for the silence argument parser utility function.""" def setUp(self): @@ -403,7 +388,7 @@ def voice_sync_helper(function): @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) -class SilenceTests(unittest.IsolatedAsyncioTestCase): +class SilenceTests(RedisTestCase): """Tests for the silence command and its related helper methods.""" @autospec(silence.Silence, "_reschedule", pass_mocks=False) -- cgit v1.2.3 From dc68bb28b36d61c9991ab0fbd55d3b77e694c1b3 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 4 Aug 2022 14:59:02 +0100 Subject: revert bump to markdownify version The new versions introduce conversions which causes the doc command embed to be formatted improperly --- poetry.lock | 18 +++++++++--------- pyproject.toml | 6 +++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9962195d9..1191549fc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -427,7 +427,7 @@ pycodestyle = ">=2.0.0,<3.0.0" [[package]] name = "frozenlist" -version = "1.3.0" +version = "1.3.1" description = "A list-like structure which implements collections.abc.MutableSequence" category = "main" optional = false @@ -446,7 +446,7 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.5.2" +version = "2.5.3" description = "File identification library for Python" category = "dev" optional = false @@ -517,15 +517,15 @@ source = ["Cython (>=0.29.7)"] [[package]] name = "markdownify" -version = "0.11.2" +version = "0.6.1" description = "Convert HTML to markdown." category = "main" optional = false python-versions = "*" [package.dependencies] -beautifulsoup4 = ">=4.9,<5" -six = ">=1.15,<2" +beautifulsoup4 = "*" +six = "*" [[package]] name = "mccabe" @@ -1071,7 +1071,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.1" +version = "20.16.2" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1096,11 +1096,11 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "yarl" -version = "1.7.2" +version = "1.8.1" description = "Yet another URL library" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] idna = ">=2.0" @@ -1109,7 +1109,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.10.*" -content-hash = "a2b47c854b1fb55bde8618ce85d09ce6c92b48c1ae61d281ce49fc0260585090" +content-hash = "b0dc5e1339805bf94be5f1b6a8454f8722d4eae645b8188ff62cd7b3c925f7e6" [metadata.files] aiodns = [] diff --git a/pyproject.toml b/pyproject.toml index 279543018..43eb799b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,11 @@ emoji = "2.0.0" feedparser = "6.0.10" rapidfuzz = "2.3.0" lxml = "4.9.1" -markdownify = "0.11.2" + +# 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.13.0" python-dateutil = "2.8.2" python-frontmatter = "1.0.0" -- cgit v1.2.3 From 6193ee2152d7dba63bd9d40b9b2a4f56524acbce Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 14 Aug 2022 20:35:08 +0100 Subject: Globally decode binary responses from redis to strings --- bot/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/__main__.py b/bot/__main__.py index e0d2e6ad5..02af2e9ef 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -26,6 +26,7 @@ async def _create_redis_session() -> RedisSession: max_connections=20, use_fakeredis=constants.Redis.use_fakeredis, global_namespace="bot", + decode_responses=True, ) try: return await redis_session.connect() -- cgit v1.2.3 From 1df6034a1723fd3ff1bd88047ca6a62f920767e6 Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 15 Aug 2022 12:04:33 +0100 Subject: Fix incident tests. --- tests/bot/exts/moderation/test_incidents.py | 38 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index 11fe565fc..53d98360c 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -20,6 +20,8 @@ from tests.helpers import ( MockUser ) +CURRENT_TIME = datetime.datetime(2022, 1, 1, tzinfo=datetime.timezone.utc) + class MockAsyncIterable: """ @@ -102,25 +104,32 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase): async def test_make_embed_actioned(self): """Embed is coloured green and footer contains 'Actioned' when `outcome=Signal.ACTIONED`.""" - embed, file = await incidents.make_embed(MockMessage(), incidents.Signal.ACTIONED, MockMember()) + embed, file = await incidents.make_embed( + incident=MockMessage(created_at=CURRENT_TIME), + outcome=incidents.Signal.ACTIONED, + actioned_by=MockMember() + ) self.assertEqual(embed.colour.value, Colours.soft_green) self.assertIn("Actioned", embed.footer.text) async def test_make_embed_not_actioned(self): """Embed is coloured red and footer contains 'Rejected' when `outcome=Signal.NOT_ACTIONED`.""" - embed, file = await incidents.make_embed(MockMessage(), incidents.Signal.NOT_ACTIONED, MockMember()) + embed, file = await incidents.make_embed( + incident=MockMessage(created_at=CURRENT_TIME), + outcome=incidents.Signal.NOT_ACTIONED, + actioned_by=MockMember() + ) self.assertEqual(embed.colour.value, Colours.soft_red) self.assertIn("Rejected", embed.footer.text) async def test_make_embed_content(self): """Incident content appears as embed description.""" - current_time = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) - incident = MockMessage(content="this is an incident", created_at=current_time) + incident = MockMessage(content="this is an incident", created_at=CURRENT_TIME) - reported_timestamp = discord_timestamp(current_time) - relative_timestamp = discord_timestamp(current_time, TimestampFormats.RELATIVE) + reported_timestamp = discord_timestamp(CURRENT_TIME) + relative_timestamp = discord_timestamp(CURRENT_TIME, TimestampFormats.RELATIVE) embed, file = await incidents.make_embed(incident, incidents.Signal.ACTIONED, MockMember()) @@ -133,7 +142,7 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase): """Incident's attachment is downloaded and displayed in the embed's image field.""" file = MagicMock(discord.File, filename="bigbadjoe.jpg") attachment = MockAttachment(filename="bigbadjoe.jpg") - incident = MockMessage(content="this is an incident", attachments=[attachment]) + incident = MockMessage(content="this is an incident", attachments=[attachment], created_at=CURRENT_TIME) # Patch `download_file` to return our `file` with patch("bot.exts.moderation.incidents.download_file", AsyncMock(return_value=file)): @@ -145,7 +154,7 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase): async def test_make_embed_with_attachment_fails(self): """Incident's attachment fails to download, proxy url is linked instead.""" attachment = MockAttachment(proxy_url="discord.com/bigbadjoe.jpg") - incident = MockMessage(content="this is an incident", attachments=[attachment]) + incident = MockMessage(content="this is an incident", attachments=[attachment], created_at=CURRENT_TIME) # Patch `download_file` to return None as if the download failed with patch("bot.exts.moderation.incidents.download_file", AsyncMock(return_value=None)): @@ -359,7 +368,6 @@ class TestCrawlIncidents(TestIncidents): class TestArchive(TestIncidents): """Tests for the `Incidents.archive` coroutine.""" - async def test_archive_webhook_not_found(self): """ Method recovers and returns False when the webhook is not found. @@ -369,7 +377,11 @@ class TestArchive(TestIncidents): """ self.cog_instance.bot.fetch_webhook = AsyncMock(side_effect=mock_404) self.assertFalse( - await self.cog_instance.archive(incident=MockMessage(), outcome=MagicMock(), actioned_by=MockMember()) + await self.cog_instance.archive( + incident=MockMessage(created_at=CURRENT_TIME), + outcome=MagicMock(), + actioned_by=MockMember() + ) ) async def test_archive_relays_incident(self): @@ -416,7 +428,7 @@ class TestArchive(TestIncidents): webhook = MockAsyncWebhook() self.cog_instance.bot.fetch_webhook = AsyncMock(return_value=webhook) - message_from_clyde = MockMessage(author=MockUser(display_name="clyde the great")) + message_from_clyde = MockMessage(author=MockUser(display_name="clyde the great"), created_at=CURRENT_TIME) await self.cog_instance.archive(message_from_clyde, MagicMock(incidents.Signal), MockMember()) self.assertNotIn("clyde", webhook.send.call_args.kwargs["username"]) @@ -520,7 +532,7 @@ class TestProcessEvent(TestIncidents): with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task): await self.cog_instance.process_event( reaction=incidents.Signal.ACTIONED.value, - incident=MockMessage(author=mock_member, id=123), + incident=MockMessage(author=mock_member, id=123, created_at=CURRENT_TIME), member=mock_member ) @@ -540,7 +552,7 @@ class TestProcessEvent(TestIncidents): with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task): await self.cog_instance.process_event( reaction=incidents.Signal.ACTIONED.value, - incident=MockMessage(id=123), + incident=MockMessage(id=123, created_at=CURRENT_TIME), member=MockMember(roles=[MockRole(id=1)]) ) except asyncio.TimeoutError: -- cgit v1.2.3 From fce6ea2f845889ab8eb6801e25e56ef0b7e67f3d Mon Sep 17 00:00:00 2001 From: dawnofmidnight Date: Mon, 15 Aug 2022 12:32:46 -0400 Subject: feat: command for banning compromised accounts --- bot/exts/moderation/infraction/infractions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 46fd3381c..679768268 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -1,6 +1,7 @@ import textwrap import typing as t +import arrow import discord from discord import Member from discord.ext import commands @@ -11,6 +12,7 @@ from bot.bot import Bot from bot.constants import Event from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import ensure_future_timestamp, respect_role_hierarchy +from bot.exts.filters.filtering import AUTO_BAN_DURATION, AUTO_BAN_REASON from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler from bot.log import get_logger @@ -151,6 +153,11 @@ class Infractions(InfractionScheduler, commands.Cog): ctx.send = send await infr_manage_cog.infraction_append(ctx, infraction, None, reason=f"[Clean log]({log_url})") + @command() + async def compban(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: + """Same as cleanban, but specifically for four days and with the ban reason used for compromised accounts.""" + await self.cleanban(ctx, user, duration=arrow.utcnow() + AUTO_BAN_DURATION, reason=AUTO_BAN_REASON) + @command(aliases=("vban",)) async def voiceban(self, ctx: Context) -> None: """ -- cgit v1.2.3 From 872281ea5faa44cf3659b1678174b1a713555288 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Mon, 15 Aug 2022 14:07:22 -0400 Subject: Added mod alerted notice to auto-infractions --- bot/exts/moderation/infraction/_scheduler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index c7f03b2e9..69c7d7afc 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -189,7 +189,10 @@ class InfractionScheduler: f"Infraction #{id_} actor is bot; including the reason in the confirmation message." ) if reason: - end_msg = f" (reason: {textwrap.shorten(reason, width=1500, placeholder='...')})" + end_msg = ( + f" (reason: {textwrap.shorten(reason, width=1500, placeholder='...')})" + " Moderators have been alerted for review" + ) purge = infraction.get("purge", "") -- cgit v1.2.3 From a7234a824f3d4b2d052357d73d23a6e0c5cbb1b6 Mon Sep 17 00:00:00 2001 From: dawnofmidnight Date: Mon, 15 Aug 2022 14:38:01 -0400 Subject: fix: change use of arrow to datetime and change docstring wording --- bot/exts/filters/filtering.py | 4 ++-- bot/exts/moderation/infraction/infractions.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index ca6ad0064..51877d7f0 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -2,7 +2,7 @@ import asyncio import re import unicodedata import urllib.parse -from datetime import timedelta +from datetime import datetime, timedelta from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union import arrow @@ -413,7 +413,7 @@ class Filtering(Cog): await context.invoke( context.command, msg.author, - arrow.utcnow() + AUTO_BAN_DURATION, + datetime.utcnow() + AUTO_BAN_DURATION, reason=AUTO_BAN_REASON ) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 679768268..c70ff7705 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -1,7 +1,7 @@ import textwrap import typing as t +from datetime import datetime -import arrow import discord from discord import Member from discord.ext import commands @@ -155,8 +155,8 @@ class Infractions(InfractionScheduler, commands.Cog): @command() async def compban(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: - """Same as cleanban, but specifically for four days and with the ban reason used for compromised accounts.""" - await self.cleanban(ctx, user, duration=arrow.utcnow() + AUTO_BAN_DURATION, reason=AUTO_BAN_REASON) + """Same as cleanban, but specifically with the ban reason and duration used for compromised accounts.""" + await self.cleanban(ctx, user, duration=datetime.utcnow() + AUTO_BAN_DURATION, reason=AUTO_BAN_REASON) @command(aliases=("vban",)) async def voiceban(self, ctx: Context) -> None: -- cgit v1.2.3 From e0b593318eba77d6fe93f2145b43838d6eb09278 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 15 Aug 2022 22:14:32 +0100 Subject: Correctly initialise redis tests Calling the cog_load from within the setUp function resulted in interaction with a RedisSession before it was initialised. This wasn't noticed in CI as it only error under certain concurrency timings due to xdist. To resolve this, we moved the setup and async setup logic to a base class. Co-authored-by: Hassan Abouelela --- tests/bot/exts/moderation/test_silence.py | 79 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 98547e2bc..2622f46a7 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -1,4 +1,3 @@ -import asyncio import itertools import unittest from datetime import datetime, timezone @@ -23,8 +22,24 @@ class PatchedDatetime(datetime): now = mock.create_autospec(datetime, "now") -class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): +class SilenceTest(RedisTestCase): + """A base class for Silence tests that correctly sets up the cog and redis.""" + + @autospec(silence, "Scheduler", pass_mocks=False) + @autospec(silence.Silence, "_reschedule", pass_mocks=False) + def setUp(self) -> None: + self.bot = MockBot(get_channel=lambda _id: MockTextChannel(id=_id)) + self.cog = silence.Silence(self.bot) + + @autospec(silence, "SilenceNotifier", pass_mocks=False) + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + await self.cog.cog_load() # Populate instance attributes. + + +class SilenceNotifierTests(SilenceTest): def setUp(self) -> None: + super().setUp() self.alert_channel = MockTextChannel() self.notifier = silence.SilenceNotifier(self.alert_channel) self.notifier.stop = self.notifier_stop_mock = Mock() @@ -89,34 +104,24 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) -class SilenceCogTests(RedisTestCase): +class SilenceCogTests(SilenceTest): """Tests for the general functionality of the Silence cog.""" - @autospec(silence, "Scheduler", pass_mocks=False) - def setUp(self) -> None: - self.bot = MockBot() - self.cog = silence.Silence(self.bot) - @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_cog_load_got_guild(self): """Bot got guild after it became available.""" - await self.cog.cog_load() self.bot.wait_until_guild_available.assert_awaited_once() self.bot.get_guild.assert_called_once_with(Guild.id) @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_cog_load_got_channels(self): """Got channels from bot.""" - self.bot.get_channel.side_effect = lambda id_: MockTextChannel(id=id_) - await self.cog.cog_load() self.assertEqual(self.cog._mod_alerts_channel.id, Channels.mod_alerts) @autospec(silence, "SilenceNotifier") async def test_cog_load_got_notifier(self, notifier): """Notifier was started with channel.""" - self.bot.get_channel.side_effect = lambda id_: MockTextChannel(id=id_) - await self.cog.cog_load() notifier.assert_called_once_with(MockTextChannel(id=Channels.mod_log)) self.assertEqual(self.cog.notifier, notifier.return_value) @@ -229,13 +234,9 @@ class SilenceCogTests(RedisTestCase): self.assertEqual(member.move_to.call_count, 1 if member == failing_member else 2) -class SilenceArgumentParserTests(RedisTestCase): +class SilenceArgumentParserTests(SilenceTest): """Tests for the silence argument parser utility function.""" - def setUp(self): - self.bot = MockBot() - self.cog = silence.Silence(self.bot) - @autospec(silence.Silence, "send_message", pass_mocks=False) @autospec(silence.Silence, "_set_silence_overwrites", return_value=False, pass_mocks=False) @autospec(silence.Silence, "parse_silence_args") @@ -303,17 +304,19 @@ class SilenceArgumentParserTests(RedisTestCase): @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) -class RescheduleTests(unittest.IsolatedAsyncioTestCase): +class RescheduleTests(RedisTestCase): """Tests for the rescheduling of cached unsilences.""" - @autospec(silence, "Scheduler", "SilenceNotifier", pass_mocks=False) - def setUp(self): + @autospec(silence, "Scheduler", pass_mocks=False) + def setUp(self) -> None: self.bot = MockBot() self.cog = silence.Silence(self.bot) self.cog._unsilence_wrapper = mock.create_autospec(self.cog._unsilence_wrapper) - with mock.patch.object(self.cog, "_reschedule", autospec=True): - asyncio.run(self.cog.cog_load()) # Populate instance attributes. + @autospec(silence, "SilenceNotifier", pass_mocks=False) + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + await self.cog.cog_load() # Populate instance attributes. async def test_skipped_missing_channel(self): """Did nothing because the channel couldn't be retrieved.""" @@ -388,20 +391,14 @@ def voice_sync_helper(function): @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) -class SilenceTests(RedisTestCase): +class SilenceTests(SilenceTest): """Tests for the silence command and its related helper methods.""" - @autospec(silence.Silence, "_reschedule", pass_mocks=False) - @autospec(silence, "Scheduler", "SilenceNotifier", pass_mocks=False) def setUp(self) -> None: - self.bot = MockBot(get_channel=lambda _: MockTextChannel()) - self.cog = silence.Silence(self.bot) + super().setUp() # Avoid unawaited coroutine warnings. self.cog.scheduler.schedule_later.side_effect = lambda delay, task_id, coro: coro.close() - - asyncio.run(self.cog.cog_load()) # Populate instance attributes. - self.text_channel = MockTextChannel() self.text_overwrite = PermissionOverwrite( send_messages=True, @@ -659,22 +656,13 @@ class SilenceTests(RedisTestCase): @autospec(silence.Silence, "unsilence_timestamps", pass_mocks=False) -class UnsilenceTests(unittest.IsolatedAsyncioTestCase): +class UnsilenceTests(SilenceTest): """Tests for the unsilence command and its related helper methods.""" - @autospec(silence.Silence, "_reschedule", pass_mocks=False) - @autospec(silence, "Scheduler", "SilenceNotifier", pass_mocks=False) def setUp(self) -> None: - self.bot = MockBot(get_channel=lambda _: MockTextChannel()) - self.cog = silence.Silence(self.bot) - - overwrites_cache = mock.create_autospec(self.cog.previous_overwrites, spec_set=True) - self.cog.previous_overwrites = overwrites_cache - - asyncio.run(self.cog.cog_load()) # Populate instance attributes. + super().setUp() self.cog.scheduler.__contains__.return_value = True - overwrites_cache.get.return_value = '{"send_messages": true, "add_reactions": false}' self.text_channel = MockTextChannel() self.text_overwrite = PermissionOverwrite(send_messages=False, add_reactions=False) self.text_channel.overwrites_for.return_value = self.text_overwrite @@ -683,6 +671,13 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.voice_overwrite = PermissionOverwrite(connect=True, speak=True) self.voice_channel.overwrites_for.return_value = self.voice_overwrite + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + overwrites_cache = mock.create_autospec(self.cog.previous_overwrites, spec_set=True) + self.cog.previous_overwrites = overwrites_cache + + overwrites_cache.get.return_value = '{"send_messages": true, "add_reactions": false}' + async def test_sent_correct_message(self): """Appropriate failure/success message was sent by the command.""" unsilenced_overwrite = PermissionOverwrite(send_messages=True, add_reactions=True) -- cgit v1.2.3 From e68868e253128b9eb2035e5ea1bae93e67bda4f1 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Tue, 16 Aug 2022 16:29:28 -0400 Subject: Added newlines and non-mentioned mod role --- bot/exts/moderation/infraction/_scheduler.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 69c7d7afc..0f8a55838 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -12,7 +12,7 @@ from discord.ext.commands import Context from bot import constants from bot.bot import Bot -from bot.constants import Colours +from bot.constants import Colours, Roles from bot.converters import MemberOrUser from bot.exts.moderation.infraction import _utils from bot.exts.moderation.modlog import ModLog @@ -190,8 +190,8 @@ class InfractionScheduler: ) if reason: end_msg = ( - f" (reason: {textwrap.shorten(reason, width=1500, placeholder='...')})" - " Moderators have been alerted for review" + f" (reason: {textwrap.shorten(reason, width=1500, placeholder='...')})." + f"\n\n<@&{Roles.moderators}> have been alerted for review" ) purge = infraction.get("purge", "") @@ -246,7 +246,8 @@ class InfractionScheduler: # Send a confirmation message to the invoking context. log.trace(f"Sending infraction #{id_} confirmation message.") - await ctx.send(f"{dm_result}{confirm_msg}{infr_message}.") + mentions = discord.AllowedMentions(users=[user], roles=False) + await ctx.send(f"{dm_result}{confirm_msg}{infr_message}.", allowed_mentions=mentions) # Send a log message to the mod log. # Don't use ctx.message.author for the actor; antispam only patches ctx.author. -- cgit v1.2.3 From 16e17f2155c0cf3241e2d5604e53b1e51e8ea386 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 03:42:31 -0400 Subject: Added `DurationOrExpiry` type union --- bot/converters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/converters.py b/bot/converters.py index 5800ea044..e97a25bdd 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -574,5 +574,6 @@ if t.TYPE_CHECKING: Infraction = t.Optional[dict] # noqa: F811 Expiry = t.Union[Duration, ISODateTime] +DurationOrExpiry = t.Union[DurationDelta, ISODateTime] MemberOrUser = t.Union[discord.Member, discord.User] UnambiguousMemberOrUser = t.Union[UnambiguousMember, UnambiguousUser] -- cgit v1.2.3 From 3ca6f7adf8126971ccd05ab7bffa30ad89257b1f Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 04:31:05 -0400 Subject: Refactoring for DurationOrExpiry --- bot/exts/moderation/infraction/_utils.py | 20 ++++++++++----- bot/exts/moderation/infraction/infractions.py | 34 +++++++++++++------------- bot/exts/moderation/infraction/superstarify.py | 4 +-- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 3a2485ec2..368a637c2 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,5 +1,5 @@ import typing as t -from datetime import datetime +from datetime import datetime, timezone import arrow import discord @@ -8,7 +8,7 @@ from discord.ext.commands import Context import bot from bot.constants import Colours, Icons -from bot.converters import MemberOrUser +from bot.converters import MemberOrUser, DurationOrExpiry from bot.errors import InvalidInfractedUserError from bot.log import get_logger from bot.utils import time @@ -80,7 +80,7 @@ async def post_infraction( user: MemberOrUser, infr_type: str, reason: str, - expires_at: datetime = None, + duration_or_expiry: t.Optional[DurationOrExpiry] = None, hidden: bool = False, active: bool = True, dm_sent: bool = False, @@ -92,6 +92,8 @@ async def post_infraction( log.trace(f"Posting {infr_type} infraction for {user} to the API.") + current_time = datetime.now(tz=timezone.utc) + payload = { "actor": ctx.author.id, # Don't use ctx.message.author; antispam only patches ctx.author. "hidden": hidden, @@ -99,10 +101,16 @@ async def post_infraction( "type": infr_type, "user": user.id, "active": active, - "dm_sent": dm_sent + "dm_sent": dm_sent, + "last_applied": current_time, } - if expires_at: - payload['expires_at'] = expires_at.isoformat() + + # Parse duration or expiry + if duration_or_expiry is not None: + if isinstance(duration_or_expiry, datetime): + payload['expires_at'] = duration_or_expiry.isoformat() + else: # is relativedelta + payload['expires_at'] = (current_time + duration_or_expiry).isoformat() # Try to apply the infraction. If it fails because the user doesn't exist, try to add it. for should_post_user in (True, False): diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 46fd3381c..1818997bb 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser +from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser, DurationOrExpiry from bot.decorators import ensure_future_timestamp, respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -86,7 +86,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[Expiry] = None, + duration: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] = None ) -> None: @@ -95,7 +95,7 @@ class Infractions(InfractionScheduler, commands.Cog): If duration is specified, it temporarily bans that user for the given duration. """ - await self.apply_ban(ctx, user, reason, expires_at=duration) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) @command(aliases=("cban", "purgeban", "pban")) @ensure_future_timestamp(timestamp_arg=3) @@ -103,7 +103,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[Expiry] = None, + duration: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] = None ) -> None: @@ -115,10 +115,10 @@ class Infractions(InfractionScheduler, commands.Cog): clean_cog: t.Optional[Clean] = self.bot.get_cog("Clean") if clean_cog is None: # If we can't get the clean cog, fall back to native purgeban. - await self.apply_ban(ctx, user, reason, purge_days=1, expires_at=duration) + await self.apply_ban(ctx, user, reason, purge_days=1, duration_or_expiry=duration) return - infraction = await self.apply_ban(ctx, user, reason, expires_at=duration) + infraction = await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) if not infraction or not infraction.get("id"): # Ban was unsuccessful, quit early. await ctx.send(":x: Failed to apply ban.") @@ -168,7 +168,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[Expiry] = None, + duration: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] ) -> None: @@ -177,7 +177,7 @@ class Infractions(InfractionScheduler, commands.Cog): If duration is specified, it temporarily voice mutes that user for the given duration. """ - await self.apply_voice_mute(ctx, user, reason, expires_at=duration) + await self.apply_voice_mute(ctx, user, reason, duration_or_expiry=duration) # endregion # region: Temporary infractions @@ -187,7 +187,7 @@ class Infractions(InfractionScheduler, commands.Cog): async def tempmute( self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[Expiry] = None, + duration: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] = None ) -> None: @@ -214,7 +214,7 @@ class Infractions(InfractionScheduler, commands.Cog): if duration is None: duration = await Duration().convert(ctx, "1h") - await self.apply_mute(ctx, user, reason, expires_at=duration) + await self.apply_mute(ctx, user, reason, duration_or_expiry=duration) @command(aliases=("tban",)) @ensure_future_timestamp(timestamp_arg=3) @@ -241,7 +241,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_ban(ctx, user, reason, expires_at=duration) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) @command(aliases=("tempvban", "tvban")) async def tempvoiceban(self, ctx: Context) -> None: @@ -258,7 +258,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: Expiry, + duration: DurationOrExpiry, *, reason: t.Optional[str] ) -> None: @@ -277,7 +277,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_voice_mute(ctx, user, reason, expires_at=duration) + await self.apply_voice_mute(ctx, user, reason, duration_or_expiry=duration) # endregion # region: Permanent shadow infractions @@ -305,7 +305,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: Expiry, + duration: DurationOrExpiry, *, reason: t.Optional[str] = None ) -> None: @@ -324,7 +324,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_ban(ctx, user, reason, expires_at=duration, hidden=True) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration, hidden=True) # endregion # region: Remove infractions (un- commands) @@ -428,7 +428,7 @@ class Infractions(InfractionScheduler, commands.Cog): return None # In the case of a permanent ban, we don't need get_active_infractions to tell us if one is active - is_temporary = kwargs.get("expires_at") is not None + is_temporary = kwargs.get("duration_or_expiry") is not None active_infraction = await _utils.get_active_infraction(ctx, user, "ban", is_temporary) if active_infraction: @@ -436,7 +436,7 @@ class Infractions(InfractionScheduler, commands.Cog): log.trace("Tempban ignored as it cannot overwrite an active ban.") return None - if active_infraction.get('expires_at') is None: + if active_infraction.get("duration_or_expiry") is None: log.trace("Permaban already exists, notify.") await ctx.send(f":x: User is already permanently banned (#{active_infraction['id']}).") return None diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 0e6aaa1e7..f2aab7a92 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -10,7 +10,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Duration, Expiry +from bot.converters import Duration, DurationOrExpiry from bot.decorators import ensure_future_timestamp from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -109,7 +109,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: t.Optional[Expiry], + duration: t.Optional[DurationOrExpiry], *, reason: str = '', ) -> None: -- cgit v1.2.3 From 22ec794d1b2f65da5d737f0a06fb2680834308f6 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 04:38:18 -0400 Subject: Ran isort on imports --- bot/exts/moderation/infraction/_utils.py | 2 +- bot/exts/moderation/infraction/infractions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 368a637c2..80b65dae8 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -8,7 +8,7 @@ from discord.ext.commands import Context import bot from bot.constants import Colours, Icons -from bot.converters import MemberOrUser, DurationOrExpiry +from bot.converters import DurationOrExpiry, MemberOrUser from bot.errors import InvalidInfractedUserError from bot.log import get_logger from bot.utils import time diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 1818997bb..aff5e392f 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser, DurationOrExpiry +from bot.converters import Age, Duration, DurationOrExpiry, Expiry, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import ensure_future_timestamp, respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler -- cgit v1.2.3 From 0e6242f7f4c41329e9270724a9780511f7165240 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 04:54:26 -0400 Subject: Updated tests - Refactored tests for new time duration arguments --- .../exts/moderation/infraction/test_infractions.py | 11 ++++---- tests/bot/exts/moderation/infraction/test_utils.py | 29 +++++++++++++--------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 052048053..a18a4d23b 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -79,13 +79,13 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): """Should call voice mute applying function without expiry.""" self.cog.apply_voice_mute = AsyncMock() self.assertIsNone(await self.cog.voicemute(self.cog, self.ctx, self.user, reason="foobar")) - self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at=None) + self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", duration_or_expiry=None) async def test_temporary_voice_mute(self): """Should call voice mute applying function with expiry.""" self.cog.apply_voice_mute = AsyncMock() self.assertIsNone(await self.cog.tempvoicemute(self.cog, self.ctx, self.user, "baz", reason="foobar")) - self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at="baz") + self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", duration_or_expiry="baz") async def test_voice_unmute(self): """Should call infraction pardoning function.""" @@ -189,7 +189,8 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): user = MockUser() await self.cog.voicemute(self.cog, self.ctx, user, reason=None) - post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_mute", None, active=True, expires_at=None) + post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_mute", None, active=True, + duration_or_expiry=None) apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, ANY) # Test action @@ -273,7 +274,7 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): self.user, "FooBar", purge_days=1, - expires_at=None, + duration_or_expiry=None, ) async def test_cleanban_doesnt_purge_messages_if_clean_cog_available(self): @@ -285,7 +286,7 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): self.ctx, self.user, "FooBar", - expires_at=None, + duration_or_expiry=None, ) @patch("bot.exts.moderation.infraction.infractions.Age") diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 5cf02033d..4c78c0bd8 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -318,14 +318,17 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "user": self.member.id, "active": False, "expires_at": now.isoformat(), - "dm_sent": False + "dm_sent": False, + "last_applied": datetime(2020, 1, 1).isoformat(), } - self.ctx.bot.api_client.post.return_value = "foo" - actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) + # Patch the time.now(tz=timezone.utc) function to return a specific time + with patch("bot.exts.moderation.infraction._utils.datetime.now", return_value=datetime(2020, 1, 1)): + self.ctx.bot.api_client.post.return_value = "foo" + actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) - self.assertEqual(actual, "foo") - self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + self.assertEqual(actual, "foo") + self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) async def test_unknown_error_post_infraction(self): """Should send an error message to chat when a non-400 error occurs.""" @@ -356,12 +359,14 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "type": "mute", "user": self.user.id, "active": True, - "dm_sent": False + "dm_sent": False, + "last_applied": datetime(2020, 1, 1), } - self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] - - actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") - self.assertEqual(actual, "foo") - self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) - post_user_mock.assert_awaited_once_with(self.ctx, self.user) + # Patch the time.now(tz=timezone.utc) function to return a specific time + with patch("bot.exts.moderation.infraction._utils.datetime.now", return_value=datetime(2020, 1, 1)): + self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] + actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") + self.assertEqual(actual, "foo") + self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) + post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From 035b5accf78f8623b40e0612e3c057f0ef2b93a7 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 05:04:38 -0400 Subject: Fixed test patches --- tests/bot/exts/moderation/infraction/test_utils.py | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 4c78c0bd8..def06932b 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -307,7 +307,8 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) - async def test_normal_post_infraction(self): + @patch("bot.exts.moderation.infraction._utils.datetime", wraps=datetime) + async def test_normal_post_infraction(self, mock_datetime): """Should return response from POST request if there are no errors.""" now = datetime.now() payload = { @@ -322,13 +323,13 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "last_applied": datetime(2020, 1, 1).isoformat(), } - # Patch the time.now(tz=timezone.utc) function to return a specific time - with patch("bot.exts.moderation.infraction._utils.datetime.now", return_value=datetime(2020, 1, 1)): - self.ctx.bot.api_client.post.return_value = "foo" - actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) + # Patch the datetime.now function to return a specific time + mock_datetime.now.return_value = datetime(2020, 1, 1) + self.ctx.bot.api_client.post.return_value = "foo" + actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) - self.assertEqual(actual, "foo") - self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + self.assertEqual(actual, "foo") + self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) async def test_unknown_error_post_infraction(self): """Should send an error message to chat when a non-400 error occurs.""" @@ -349,8 +350,9 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.assertIsNone(actual) post_user_mock.assert_awaited_once_with(self.ctx, self.user) + @patch("bot.exts.moderation.infraction._utils.datetime", wraps=datetime) @patch("bot.exts.moderation.infraction._utils.post_user", return_value="bar") - async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): + async def test_first_fail_second_success_user_post_infraction(self, post_user_mock, mock_datetime): """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" payload = { "actor": self.ctx.author.id, @@ -363,10 +365,10 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "last_applied": datetime(2020, 1, 1), } - # Patch the time.now(tz=timezone.utc) function to return a specific time - with patch("bot.exts.moderation.infraction._utils.datetime.now", return_value=datetime(2020, 1, 1)): - self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] - actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") - self.assertEqual(actual, "foo") - self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) - post_user_mock.assert_awaited_once_with(self.ctx, self.user) + # Patch the datetime.now function to return a specific time + mock_datetime.now.return_value = datetime(2020, 1, 1) + self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] + actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") + self.assertEqual(actual, "foo") + self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) + post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From 8db7ef5df087e43804d07dabe2037af18adcb0d6 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 05:19:30 -0400 Subject: Added isoformat for test payload --- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index def06932b..d3a908b28 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -362,7 +362,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "user": self.user.id, "active": True, "dm_sent": False, - "last_applied": datetime(2020, 1, 1), + "last_applied": datetime(2020, 1, 1).isoformat(), } # Patch the datetime.now function to return a specific time -- cgit v1.2.3 From 1ad627adbe9f35d9d8c5bedc52295d4e79c1eb6f Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 05:20:48 -0400 Subject: Updated parameter names - Changed `duration` parameter names to `duration_or_expiry` to more accurately reflect options for help --- bot/exts/moderation/infraction/infractions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index aff5e392f..12bd084df 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Age, Duration, DurationOrExpiry, Expiry, MemberOrUser, UnambiguousMemberOrUser +from bot.converters import Age, Duration, DurationOrExpiry, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import ensure_future_timestamp, respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -86,7 +86,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: t.Optional[DurationOrExpiry] = None, + duration_or_expiry: t.Optional[DurationOrExpiry] = None, *, reason: t.Optional[str] = None ) -> None: @@ -95,7 +95,7 @@ class Infractions(InfractionScheduler, commands.Cog): If duration is specified, it temporarily bans that user for the given duration. """ - await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration_or_expiry) @command(aliases=("cban", "purgeban", "pban")) @ensure_future_timestamp(timestamp_arg=3) @@ -222,7 +222,7 @@ class Infractions(InfractionScheduler, commands.Cog): self, ctx: Context, user: UnambiguousMemberOrUser, - duration: Expiry, + duration_or_expiry: DurationOrExpiry, *, reason: t.Optional[str] = None ) -> None: @@ -241,7 +241,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) + await self.apply_ban(ctx, user, reason, duration_or_expiry=duration_or_expiry) @command(aliases=("tempvban", "tvban")) async def tempvoiceban(self, ctx: Context) -> None: -- cgit v1.2.3 From 3e9ffa1ea3c7cfa7c583189fcb8675db9ee5c6d5 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 05:36:45 -0400 Subject: Updated ban command docstring - Updated docstring to be more explicit on parameter fields --- bot/exts/moderation/infraction/infractions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 12bd084df..83a947a19 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -91,9 +91,11 @@ class Infractions(InfractionScheduler, commands.Cog): reason: t.Optional[str] = None ) -> None: """ - Permanently ban a user for the given reason and stop watching them with Big Brother. + Permanently ban a `user` for the given `reason` and stop watching them with Big Brother. - If duration is specified, it temporarily bans that user for the given duration. + If a duration is specified, it temporarily bans the `user` for the given duration. + Alternatively, an ISO 8601 timestamp representing the expiry time can be provided + for `duration_or_expiry`. """ await self.apply_ban(ctx, user, reason, duration_or_expiry=duration_or_expiry) -- cgit v1.2.3 From 3280ac48a9031b727bdde69909729093593bd967 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 06:11:11 -0400 Subject: Fixed tests - Corrected datetime patching --- tests/bot/exts/moderation/infraction/test_utils.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index d3a908b28..b1f23e31c 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -307,8 +307,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) - @patch("bot.exts.moderation.infraction._utils.datetime", wraps=datetime) - async def test_normal_post_infraction(self, mock_datetime): + async def test_normal_post_infraction(self): """Should return response from POST request if there are no errors.""" now = datetime.now() payload = { @@ -320,16 +319,18 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "active": False, "expires_at": now.isoformat(), "dm_sent": False, - "last_applied": datetime(2020, 1, 1).isoformat(), } - # Patch the datetime.now function to return a specific time - mock_datetime.now.return_value = datetime(2020, 1, 1) self.ctx.bot.api_client.post.return_value = "foo" actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) self.assertEqual(actual, "foo") - self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + self.ctx.bot.api_client.post.assert_awaited_once() + await_args = str(self.ctx.bot.api_client.post.await_args) + # Check existing keys present, allow for additional keys (e.g. `last_applied`) + for key, value in payload.items(): + self.assertTrue(key in await_args) + self.assertTrue(str(value) in await_args) async def test_unknown_error_post_infraction(self): """Should send an error message to chat when a non-400 error occurs.""" -- cgit v1.2.3 From 08e344c8884b612ea0775d3141b33d0f4575d085 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 06:11:23 -0400 Subject: Correct last_applied formatting --- bot/exts/moderation/infraction/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 80b65dae8..cd82f5b2e 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -102,7 +102,7 @@ async def post_infraction( "user": user.id, "active": active, "dm_sent": dm_sent, - "last_applied": current_time, + "last_applied": current_time.isoformat(), } # Parse duration or expiry -- cgit v1.2.3 From f8e3656a74cfc0cae37cab65ed114319762f01e3 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 28 Jul 2022 18:57:01 -0400 Subject: Use `last_applied` to display duration --- bot/exts/moderation/infraction/_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index cd82f5b2e..23ae517e9 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -188,9 +188,10 @@ async def notify_infraction( expires_at = "Never" duration = "Permanent" else: + origin = arrow.get(infraction["last_applied"]) expiry = arrow.get(infraction["expires_at"]) expires_at = time.format_relative(expiry) - duration = time.humanize_delta(infraction["inserted_at"], expiry, max_units=2) + duration = time.humanize_delta(origin, expiry, max_units=2) if infraction["active"]: remaining = time.humanize_delta(expiry, arrow.utcnow(), max_units=2) -- cgit v1.2.3 From 8d788a9fdc53a7af0730dfc925ea649fd5b810c4 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sun, 31 Jul 2022 02:09:35 -0400 Subject: Added new expiry usage to apply - Added new usage of `last_applied` time for duration calculation in `apply_infraction` --- bot/exts/moderation/infraction/_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index c7f03b2e9..28aafec2a 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -137,7 +137,7 @@ class InfractionScheduler: infr_type = infraction["type"] icon = _utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] - expiry = time.format_with_duration(infraction["expires_at"]) + expiry = time.format_with_duration(infraction["expires_at"], infraction["last_applied"]) id_ = infraction['id'] if user_reason is None: -- cgit v1.2.3 From 3babbf2fa0a6a8d99e796af0cd78a7516b781898 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Mon, 1 Aug 2022 00:55:00 -0400 Subject: Added microsecond rounding for `humanize_delta` --- bot/utils/time.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index a0379c3ef..104ea026d 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -195,7 +195,11 @@ def humanize_delta( end = arrow.get(args[0]) start = arrow.get(args[1]) if len(args) == 2 else arrow.utcnow() - delta = relativedelta(end.datetime, start.datetime) + # Round microseconds + end = round_datetime(end.datetime) + start = round_datetime(start.datetime) + + delta = relativedelta(end, start) if absolute: delta = abs(delta) else: @@ -326,3 +330,14 @@ def until_expiration(expiry: Optional[Timestamp]) -> str: return "Expired" return format_relative(expiry) + + +def round_datetime(dt: datetime.datetime) -> datetime.datetime: + """ + Round a datetime object to the nearest second. + + Resulting datetime objects will have microsecond values of 0, useful for delta comparisons. + """ + if dt.microsecond >= 500000: + dt += datetime.timedelta(seconds=1) + return dt.replace(microsecond=0) -- cgit v1.2.3 From 011cfddbbb29a4555c93995ba61ecf6919a4a68e Mon Sep 17 00:00:00 2001 From: ionite34 Date: Mon, 1 Aug 2022 01:00:00 -0400 Subject: Infraction duration fallback if no `last_applied` field --- bot/exts/moderation/infraction/_scheduler.py | 8 +++++++- bot/exts/moderation/infraction/_utils.py | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 28aafec2a..cfb585bf5 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -137,9 +137,15 @@ class InfractionScheduler: infr_type = infraction["type"] icon = _utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] - expiry = time.format_with_duration(infraction["expires_at"], infraction["last_applied"]) id_ = infraction['id'] + if "last_applied" in infraction: + origin = infraction["last_applied"] + else: # Fallback for previous API versions without `last_applied` + log.trace(f"No last_applied for infraction {id_}, using inserted_at time.") + origin = infraction["inserted_at"] + expiry = time.format_with_duration(infraction["expires_at"], origin) + if user_reason is None: user_reason = reason diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 23ae517e9..470ce05e1 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -188,7 +188,12 @@ async def notify_infraction( expires_at = "Never" duration = "Permanent" else: - origin = arrow.get(infraction["last_applied"]) + if "last_applied" in infraction: + origin = infraction["last_applied"] + else: # Fallback for previous API versions without `last_applied` + log.trace(f"No last_applied for infraction {infraction}, using inserted_at time.") + origin = infraction["inserted_at"] + origin = arrow.get(origin) expiry = arrow.get(infraction["expires_at"]) expires_at = time.format_relative(expiry) duration = time.humanize_delta(origin, expiry, max_units=2) -- cgit v1.2.3 From 9fc8b5e72b0531dc0a05035b9e1f23dacaa404e9 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 6 Aug 2022 13:10:59 -0400 Subject: Changed datetime.now to arrow.utcnow - Used arrow.utcnow to reduce complexity and import --- bot/exts/moderation/infraction/_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 470ce05e1..eda56b97c 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,5 +1,5 @@ import typing as t -from datetime import datetime, timezone +from datetime import datetime import arrow import discord @@ -92,7 +92,7 @@ async def post_infraction( log.trace(f"Posting {infr_type} infraction for {user} to the API.") - current_time = datetime.now(tz=timezone.utc) + current_time = arrow.utcnow() payload = { "actor": ctx.author.id, # Don't use ctx.message.author; antispam only patches ctx.author. -- cgit v1.2.3 From 1212ac808380280c7b7625eb2a43c75c92e17d5f Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 6 Aug 2022 13:11:36 -0400 Subject: Removed `inserted_at` fallback Given API updates, the fallback is not needed --- bot/exts/moderation/infraction/_scheduler.py | 11 ++++------- bot/exts/moderation/infraction/_utils.py | 7 +------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index cfb585bf5..cb3f7149f 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -138,13 +138,10 @@ class InfractionScheduler: icon = _utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] id_ = infraction['id'] - - if "last_applied" in infraction: - origin = infraction["last_applied"] - else: # Fallback for previous API versions without `last_applied` - log.trace(f"No last_applied for infraction {id_}, using inserted_at time.") - origin = infraction["inserted_at"] - expiry = time.format_with_duration(infraction["expires_at"], origin) + expiry = time.format_with_duration( + infraction["expires_at"], + infraction["last_applied"] + ) if user_reason is None: user_reason = reason diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index eda56b97c..407c97251 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -188,12 +188,7 @@ async def notify_infraction( expires_at = "Never" duration = "Permanent" else: - if "last_applied" in infraction: - origin = infraction["last_applied"] - else: # Fallback for previous API versions without `last_applied` - log.trace(f"No last_applied for infraction {infraction}, using inserted_at time.") - origin = infraction["inserted_at"] - origin = arrow.get(origin) + origin = arrow.get(infraction["last_applied"]) expiry = arrow.get(infraction["expires_at"]) expires_at = time.format_relative(expiry) duration = time.humanize_delta(origin, expiry, max_units=2) -- cgit v1.2.3 From e74ff62122935da349d38a8c06eef14d1e3ba9aa Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 6 Aug 2022 13:47:49 -0400 Subject: Refactored test to not use datetime patch - Used new method of dict subset comparison instead of datetime patching for better compat. with argument types --- tests/bot/exts/moderation/infraction/test_utils.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index b1f23e31c..5ba0f4273 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -1,7 +1,7 @@ import unittest from collections import namedtuple from datetime import datetime -from unittest.mock import AsyncMock, MagicMock, call, patch +from unittest.mock import AsyncMock, MagicMock, patch from botcore.site_api import ResponseCodeError from discord import Embed, Forbidden, HTTPException, NotFound @@ -351,11 +351,10 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.assertIsNone(actual) post_user_mock.assert_awaited_once_with(self.ctx, self.user) - @patch("bot.exts.moderation.infraction._utils.datetime", wraps=datetime) @patch("bot.exts.moderation.infraction._utils.post_user", return_value="bar") - async def test_first_fail_second_success_user_post_infraction(self, post_user_mock, mock_datetime): + async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" - payload = { + expected = { "actor": self.ctx.author.id, "hidden": False, "reason": "Test reason", @@ -363,13 +362,17 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "user": self.user.id, "active": True, "dm_sent": False, - "last_applied": datetime(2020, 1, 1).isoformat(), } - # Patch the datetime.now function to return a specific time - mock_datetime.now.return_value = datetime(2020, 1, 1) self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") self.assertEqual(actual, "foo") - self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) + await_args = self.bot.api_client.post.await_args_list + self.assertEqual(len(await_args), 2, "Expected 2 awaits") + + # Since `last_applied` is based on current time, just check if expected is a subset of payload + for args in await_args: + payload: dict = args.kwargs["json"] + self.assertEqual(payload, payload | expected) + post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From d93afa3dbe16404a60e23acaf394affcda5aff89 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 6 Aug 2022 13:57:51 -0400 Subject: Updated previous tests to use subset method --- tests/bot/exts/moderation/infraction/test_utils.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 5ba0f4273..6c9af2555 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -310,7 +310,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): async def test_normal_post_infraction(self): """Should return response from POST request if there are no errors.""" now = datetime.now() - payload = { + expected = { "actor": self.ctx.author.id, "hidden": True, "reason": "Test reason", @@ -323,14 +323,12 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.ctx.bot.api_client.post.return_value = "foo" actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) - self.assertEqual(actual, "foo") self.ctx.bot.api_client.post.assert_awaited_once() - await_args = str(self.ctx.bot.api_client.post.await_args) - # Check existing keys present, allow for additional keys (e.g. `last_applied`) - for key, value in payload.items(): - self.assertTrue(key in await_args) - self.assertTrue(str(value) in await_args) + + # Since `last_applied` is based on current time, just check if expected is a subset of payload + payload: dict = self.ctx.bot.api_client.post.await_args_list[0].kwargs["json"] + self.assertEqual(payload, payload | expected) async def test_unknown_error_post_infraction(self): """Should send an error message to chat when a non-400 error occurs.""" -- cgit v1.2.3 From 08a6bbc306d1ec54998b690eeeb258548e8e08fa Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 13 Aug 2022 15:45:52 -0400 Subject: Corrected test use of utcnow Corrected test case to use `datetime.utcnow()` to be consistent with target --- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 6c9af2555..29dadf372 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -309,7 +309,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): async def test_normal_post_infraction(self): """Should return response from POST request if there are no errors.""" - now = datetime.now() + now = datetime.utcnow() expected = { "actor": self.ctx.author.id, "hidden": True, -- cgit v1.2.3 From 1946ad455408c26916d3c5f6a68d886c25ddb93b Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 13 Aug 2022 15:48:54 -0400 Subject: Updated infractions display for updates - Added new infraction delta calculations to updated infractions. - Updates of infraction durations now also update the `last_applied` field. - `inserted_at` is now sent by the bot client to denote the original unmodified infraction application time --- bot/exts/moderation/infraction/_utils.py | 9 +++-- bot/exts/moderation/infraction/management.py | 24 ++++++++++---- bot/utils/time.py | 49 +++++++++++++++++++++------- 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 407c97251..5e708d7fe 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -12,6 +12,7 @@ from bot.converters import DurationOrExpiry, MemberOrUser from bot.errors import InvalidInfractedUserError from bot.log import get_logger from bot.utils import time +from bot.utils.time import unpack_duration log = get_logger(__name__) @@ -102,15 +103,13 @@ async def post_infraction( "user": user.id, "active": active, "dm_sent": dm_sent, + "inserted_at": current_time.isoformat(), "last_applied": current_time.isoformat(), } - # Parse duration or expiry if duration_or_expiry is not None: - if isinstance(duration_or_expiry, datetime): - payload['expires_at'] = duration_or_expiry.isoformat() - else: # is relativedelta - payload['expires_at'] = (current_time + duration_or_expiry).isoformat() + _, expiry = unpack_duration(duration_or_expiry, current_time) + payload["expires_at"] = expiry.isoformat() # Try to apply the infraction. If it fails because the user doesn't exist, try to add it. for should_post_user in (True, False): diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index a7d7a844a..6ef382119 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -2,6 +2,7 @@ import re import textwrap import typing as t +import arrow import discord from discord.ext import commands from discord.ext.commands import Context @@ -9,7 +10,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Expiry, Infraction, MemberOrUser, Snowflake, UnambiguousUser +from bot.converters import DurationOrExpiry, Infraction, MemberOrUser, Snowflake, UnambiguousUser from bot.decorators import ensure_future_timestamp from bot.errors import InvalidInfraction from bot.exts.moderation.infraction import _utils @@ -20,6 +21,7 @@ from bot.pagination import LinePaginator from bot.utils import messages, time from bot.utils.channel import is_mod_channel from bot.utils.members import get_or_fetch_member +from bot.utils.time import unpack_duration log = get_logger(__name__) @@ -89,7 +91,7 @@ class ModManagement(commands.Cog): self, ctx: Context, infraction: Infraction, - duration: t.Union[Expiry, t.Literal["p", "permanent"], None], + duration: t.Union[DurationOrExpiry, t.Literal["p", "permanent"], None], *, reason: str = None ) -> None: @@ -129,7 +131,7 @@ class ModManagement(commands.Cog): self, ctx: Context, infraction: Infraction, - duration: t.Union[Expiry, t.Literal["p", "permanent"], None], + duration: t.Union[DurationOrExpiry, t.Literal["p", "permanent"], None], *, reason: str = None ) -> None: @@ -172,8 +174,11 @@ class ModManagement(commands.Cog): request_data['expires_at'] = None confirm_messages.append("marked as permanent") elif duration is not None: - request_data['expires_at'] = duration.isoformat() - expiry = time.format_with_duration(duration) + origin, expiry = unpack_duration(duration) + # Update `last_applied` if expiry changes. + request_data['last_applied'] = origin.isoformat() + request_data['expires_at'] = expiry.isoformat() + expiry = time.format_with_duration(expiry, origin) confirm_messages.append(f"set to expire on {expiry}") else: confirm_messages.append("expiry unchanged") @@ -380,7 +385,10 @@ class ModManagement(commands.Cog): user = infraction["user"] expires_at = infraction["expires_at"] inserted_at = infraction["inserted_at"] + last_applied = infraction["last_applied"] created = time.discord_timestamp(inserted_at) + applied = time.discord_timestamp(last_applied) + duration_edited = arrow.get(last_applied) > arrow.get(inserted_at) dm_sent = infraction["dm_sent"] # Format the user string. @@ -400,7 +408,11 @@ class ModManagement(commands.Cog): if expires_at is None: duration = "*Permanent*" else: - duration = time.humanize_delta(inserted_at, expires_at) + duration = time.humanize_delta(last_applied, expires_at) + + # Notice if infraction expiry was edited. + if duration_edited: + duration += f" (edited {applied})" # Format `dm_sent` if dm_sent is None: diff --git a/bot/utils/time.py b/bot/utils/time.py index 104ea026d..820ac2929 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -1,12 +1,18 @@ +from __future__ import annotations + import datetime import re +from copy import copy from enum import Enum from time import struct_time -from typing import Literal, Optional, Union, overload +from typing import Literal, Optional, TYPE_CHECKING, Union, overload import arrow from dateutil.relativedelta import relativedelta +if TYPE_CHECKING: + from bot.converters import DurationOrExpiry + _DURATION_REGEX = re.compile( r"((?P\d+?) ?(years|year|Y|y) ?)?" r"((?P\d+?) ?(months|month|m) ?)?" @@ -194,12 +200,8 @@ def humanize_delta( elif len(args) <= 2: end = arrow.get(args[0]) start = arrow.get(args[1]) if len(args) == 2 else arrow.utcnow() + delta = round_delta(relativedelta(end.datetime, start.datetime)) - # Round microseconds - end = round_datetime(end.datetime) - start = round_datetime(start.datetime) - - delta = relativedelta(end, start) if absolute: delta = abs(delta) else: @@ -332,12 +334,35 @@ def until_expiration(expiry: Optional[Timestamp]) -> str: return format_relative(expiry) -def round_datetime(dt: datetime.datetime) -> datetime.datetime: +def unpack_duration( + duration_or_expiry: DurationOrExpiry, + origin: Optional[Union[datetime.datetime, arrow.Arrow]] = None +) -> tuple[datetime.datetime, datetime.datetime]: + """ + Unpacks a DurationOrExpiry into a tuple of (origin, expiry). + + The `origin` defaults to the current UTC time at function call. + """ + if origin is None: + origin = datetime.datetime.now(tz=datetime.timezone.utc) + + if isinstance(origin, arrow.Arrow): + origin = origin.datetime + + if isinstance(duration_or_expiry, relativedelta): + return origin, origin + duration_or_expiry + else: + return origin, duration_or_expiry + + +def round_delta(delta: relativedelta) -> relativedelta: """ - Round a datetime object to the nearest second. + Rounds `delta` to the nearest second. - Resulting datetime objects will have microsecond values of 0, useful for delta comparisons. + Returns a copy with microsecond values of 0. """ - if dt.microsecond >= 500000: - dt += datetime.timedelta(seconds=1) - return dt.replace(microsecond=0) + delta = copy(delta) + if delta.microseconds >= 500000: + delta += relativedelta(seconds=1) + delta.microseconds = 0 + return delta -- cgit v1.2.3 From 6a87a38d4e44b1afe617cdfa0cc5027aad8a3933 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Sat, 13 Aug 2022 17:00:41 -0400 Subject: Removed unused datetime import --- bot/exts/moderation/infraction/_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 5e708d7fe..5f2529c6b 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,5 +1,4 @@ import typing as t -from datetime import datetime import arrow import discord -- cgit v1.2.3 From c9d5aac1d71bdbee672355e75ee8e655892c8b6c Mon Sep 17 00:00:00 2001 From: ionite34 Date: Tue, 16 Aug 2022 17:35:44 -0400 Subject: Added article to automute message --- bot/exts/moderation/infraction/_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 0f8a55838..280b0fb0c 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -191,7 +191,7 @@ class InfractionScheduler: if reason: end_msg = ( f" (reason: {textwrap.shorten(reason, width=1500, placeholder='...')})." - f"\n\n<@&{Roles.moderators}> have been alerted for review" + f"\n\nThe <@&{Roles.moderators}> have been alerted for review" ) purge = infraction.get("purge", "") -- cgit v1.2.3 From 4eaecb32830aa122cd53656de562caf20b79e33d Mon Sep 17 00:00:00 2001 From: dawnofmidnight Date: Wed, 17 Aug 2022 17:02:25 -0400 Subject: fix: replace datetime.utcnow() with arrow --- bot/exts/filters/filtering.py | 4 ++-- bot/exts/moderation/infraction/infractions.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 51877d7f0..e4df0b1fd 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -2,7 +2,7 @@ import asyncio import re import unicodedata import urllib.parse -from datetime import datetime, timedelta +from datetime import timedelta from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union import arrow @@ -413,7 +413,7 @@ class Filtering(Cog): await context.invoke( context.command, msg.author, - datetime.utcnow() + AUTO_BAN_DURATION, + (arrow.utcnow() + AUTO_BAN_DURATION).datetime, reason=AUTO_BAN_REASON ) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index c70ff7705..08a3609a7 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -1,7 +1,7 @@ import textwrap import typing as t -from datetime import datetime +import arrow import discord from discord import Member from discord.ext import commands @@ -156,7 +156,7 @@ class Infractions(InfractionScheduler, commands.Cog): @command() async def compban(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: """Same as cleanban, but specifically with the ban reason and duration used for compromised accounts.""" - await self.cleanban(ctx, user, duration=datetime.utcnow() + AUTO_BAN_DURATION, reason=AUTO_BAN_REASON) + await self.cleanban(ctx, user, duration=(arrow.utcnow() + AUTO_BAN_DURATION).datetime, reason=AUTO_BAN_REASON) @command(aliases=("vban",)) async def voiceban(self, ctx: Context) -> None: -- cgit v1.2.3 From 85d8ae441c0f4e34fac8fd4323ca7da9dd507582 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 17 Aug 2022 17:07:05 -0400 Subject: feat: add reason argument to pardon commands --- bot/exts/moderation/infraction/_scheduler.py | 22 ++++++++++++++++++---- bot/exts/moderation/infraction/infractions.py | 12 ++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index c7f03b2e9..d733c3936 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -271,6 +271,7 @@ class InfractionScheduler: ctx: Context, infr_type: str, user: MemberOrUser, + pardon_reason: t.Optional[str] = None, *, send_msg: bool = True, notify: bool = True @@ -278,6 +279,9 @@ class InfractionScheduler: """ Prematurely end an infraction for a user and log the action in the mod log. + If `pardon_reason` is None, then the infraction object will not receive + appended text explaining why the infraction was pardoned. + If `send_msg` is True, then a pardoning confirmation message will be sent to the context channel. Otherwise, no such message will be sent. @@ -302,7 +306,7 @@ class InfractionScheduler: return # Deactivate the infraction and cancel its scheduled expiration task. - log_text = await self.deactivate_infraction(response[0], send_log=False, notify=notify) + log_text = await self.deactivate_infraction(response[0], pardon_reason, send_log=False, notify=notify) log_text["Member"] = messages.format_user(user) log_text["Actor"] = ctx.author.mention @@ -355,6 +359,7 @@ class InfractionScheduler: async def deactivate_infraction( self, infraction: _utils.Infraction, + pardon_reason: t.Optional[str] = None, *, send_log: bool = True, notify: bool = True @@ -362,6 +367,9 @@ class InfractionScheduler: """ Deactivate an active infraction and return a dictionary of lines to send in a mod log. + If `pardon_reason` is None, then the infraction object will not receive + appended text explaining why the infraction was pardoned. + The infraction is removed from Discord, marked as inactive in the database, and has its expiration task cancelled. If `send_log` is True, a mod log is sent for the deactivation of the infraction. @@ -386,7 +394,7 @@ class InfractionScheduler: "Reason": infraction["reason"], "Created": time.format_with_duration(infraction["inserted_at"], infraction["expires_at"]), } - + try: log.trace("Awaiting the pardon action coroutine.") returned_log = await self._pardon_action(infraction, notify) @@ -430,13 +438,19 @@ class InfractionScheduler: except ResponseCodeError: log.exception(f"Failed to fetch watch status for user {user_id}") log_text["Watching"] = "Unknown - failed to fetch watch status." - + try: # Mark infraction as inactive in the database. log.trace(f"Marking infraction #{id_} as inactive in the database.") + + data = {"active": False} + + if pardon_reason is not None: + data["reason"] = infraction["reason"] + f" | Pardoned: {pardon_reason}" + await self.bot.api_client.patch( f"bot/infractions/{id_}", - json={"active": False} + json=data ) except ResponseCodeError as e: log.exception(f"Failed to deactivate infraction #{id_} ({type_})") diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 46fd3381c..a94da0d7c 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -330,14 +330,14 @@ class Infractions(InfractionScheduler, commands.Cog): # region: Remove infractions (un- commands) @command() - async def unmute(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: + async def unmute(self, ctx: Context, user: UnambiguousMemberOrUser, *, pardon_reason: t.Optional[str] = None) -> None: """Prematurely end the active mute infraction for the user.""" - await self.pardon_infraction(ctx, "mute", user) + await self.pardon_infraction(ctx, "mute", user, pardon_reason) @command() - async def unban(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: + async def unban(self, ctx: Context, user: UnambiguousMemberOrUser, *, pardon_reason: t.Optional[str] = None) -> None: """Prematurely end the active ban infraction for the user.""" - await self.pardon_infraction(ctx, "ban", user) + await self.pardon_infraction(ctx, "ban", user, pardon_reason) @command(aliases=("uvban",)) async def unvoiceban(self, ctx: Context) -> None: @@ -349,9 +349,9 @@ class Infractions(InfractionScheduler, commands.Cog): await ctx.send(":x: This command is not yet implemented. Maybe you meant to use `unvoicemute`?") @command(aliases=("uvmute",)) - async def unvoicemute(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: + async def unvoicemute(self, ctx: Context, user: UnambiguousMemberOrUser, *, pardon_reason: t.Optional[str] = None) -> None: """Prematurely end the active voice mute infraction for the user.""" - await self.pardon_infraction(ctx, "voice_mute", user) + await self.pardon_infraction(ctx, "voice_mute", user, pardon_reason) # endregion # region: Base apply functions -- cgit v1.2.3 From 6ca8e4822665832609208bd02f2201d5e584479c Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 17 Aug 2022 17:11:35 -0400 Subject: fix: lint --- bot/exts/moderation/infraction/_scheduler.py | 4 ++-- bot/exts/moderation/infraction/infractions.py | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index d733c3936..a6846e239 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -394,7 +394,7 @@ class InfractionScheduler: "Reason": infraction["reason"], "Created": time.format_with_duration(infraction["inserted_at"], infraction["expires_at"]), } - + try: log.trace("Awaiting the pardon action coroutine.") returned_log = await self._pardon_action(infraction, notify) @@ -438,7 +438,7 @@ class InfractionScheduler: except ResponseCodeError: log.exception(f"Failed to fetch watch status for user {user_id}") log_text["Watching"] = "Unknown - failed to fetch watch status." - + try: # Mark infraction as inactive in the database. log.trace(f"Marking infraction #{id_} as inactive in the database.") diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index a94da0d7c..8e5ee8200 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -330,12 +330,24 @@ class Infractions(InfractionScheduler, commands.Cog): # region: Remove infractions (un- commands) @command() - async def unmute(self, ctx: Context, user: UnambiguousMemberOrUser, *, pardon_reason: t.Optional[str] = None) -> None: + async def unmute( + self, + ctx: Context, + user: UnambiguousMemberOrUser, + *, + pardon_reason: t.Optional[str] = None + ) -> None: """Prematurely end the active mute infraction for the user.""" await self.pardon_infraction(ctx, "mute", user, pardon_reason) @command() - async def unban(self, ctx: Context, user: UnambiguousMemberOrUser, *, pardon_reason: t.Optional[str] = None) -> None: + async def unban( + self, + ctx: Context, + user: UnambiguousMemberOrUser, + *, + pardon_reason: t.Optional[str] = None + ) -> None: """Prematurely end the active ban infraction for the user.""" await self.pardon_infraction(ctx, "ban", user, pardon_reason) @@ -349,7 +361,13 @@ class Infractions(InfractionScheduler, commands.Cog): await ctx.send(":x: This command is not yet implemented. Maybe you meant to use `unvoicemute`?") @command(aliases=("uvmute",)) - async def unvoicemute(self, ctx: Context, user: UnambiguousMemberOrUser, *, pardon_reason: t.Optional[str] = None) -> None: + async def unvoicemute( + self, + ctx: Context, + user: UnambiguousMemberOrUser, + *, + pardon_reason: t.Optional[str] = None + ) -> None: """Prematurely end the active voice mute infraction for the user.""" await self.pardon_infraction(ctx, "voice_mute", user, pardon_reason) -- cgit v1.2.3 From e7188bcead99c74207b61d4f1e05f061c843c423 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 17 Aug 2022 17:17:55 -0400 Subject: fix: docstrings --- bot/exts/moderation/infraction/_scheduler.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index a6846e239..b0a2e3359 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -279,7 +279,7 @@ class InfractionScheduler: """ Prematurely end an infraction for a user and log the action in the mod log. - If `pardon_reason` is None, then the infraction object will not receive + If `pardon_reason` is None, then the database will not receive appended text explaining why the infraction was pardoned. If `send_msg` is True, then a pardoning confirmation message will be sent to @@ -367,12 +367,13 @@ class InfractionScheduler: """ Deactivate an active infraction and return a dictionary of lines to send in a mod log. - If `pardon_reason` is None, then the infraction object will not receive - appended text explaining why the infraction was pardoned. - The infraction is removed from Discord, marked as inactive in the database, and has its - expiration task cancelled. If `send_log` is True, a mod log is sent for the - deactivation of the infraction. + expiration task cancelled. + + If `pardon_reason` is None, then the database will not receive + appended text explaining why the infraction was pardoned. + + If `send_log` is True, a mod log is sent for the deactivation of the infraction. If `notify` is True, notify the user of the pardon via DM where applicable. -- cgit v1.2.3 From 1928ab280257d375d5da53f626d25f8a0c84ea18 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 17 Aug 2022 17:43:41 -0400 Subject: fix: trailing whitespace --- bot/exts/moderation/infraction/_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index b0a2e3359..557fda25a 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -372,7 +372,7 @@ class InfractionScheduler: If `pardon_reason` is None, then the database will not receive appended text explaining why the infraction was pardoned. - + If `send_log` is True, a mod log is sent for the deactivation of the infraction. If `notify` is True, notify the user of the pardon via DM where applicable. -- cgit v1.2.3 From 9434646723e3ab38543576194f2b5fc0f2e680e0 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 17 Aug 2022 18:02:21 -0400 Subject: add: test for reasoned and reasonless pardons --- tests/bot/exts/moderation/infraction/test_infractions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 052048053..c9006588c 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -90,8 +90,14 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): async def test_voice_unmute(self): """Should call infraction pardoning function.""" self.cog.pardon_infraction = AsyncMock() + self.assertIsNone(await self.cog.unvoicemute(self.cog, self.ctx, self.user, pardon_reason="foobar")) + self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user, "foobar") + + async def test_voice_unmute_reasonless(self): + """Should call infraction pardoning function without a pardon reason.""" + self.cog.pardon_infraction = AsyncMock() self.assertIsNone(await self.cog.unvoicemute(self.cog, self.ctx, self.user)) - self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user) + self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user, None) @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") -- cgit v1.2.3 From ab4c5e9ec2d65df0c3035ef7ca75bec8a518322c Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 18 Aug 2022 11:16:22 -0400 Subject: change: make unban require pardon reason --- bot/exts/moderation/infraction/infractions.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 8e5ee8200..59d8519b7 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -341,13 +341,7 @@ class Infractions(InfractionScheduler, commands.Cog): await self.pardon_infraction(ctx, "mute", user, pardon_reason) @command() - async def unban( - self, - ctx: Context, - user: UnambiguousMemberOrUser, - *, - pardon_reason: t.Optional[str] = None - ) -> None: + async def unban(self, ctx: Context, user: UnambiguousMemberOrUser, *, pardon_reason: str) -> None: """Prematurely end the active ban infraction for the user.""" await self.pardon_infraction(ctx, "ban", user, pardon_reason) -- cgit v1.2.3 From 06f9f48d2ec947f447824cda05d30f3fef9a3ef1 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Thu, 18 Aug 2022 11:24:54 -0400 Subject: Made DM duration remaining optional with resend --- bot/exts/moderation/infraction/_utils.py | 12 +++++++----- bot/exts/moderation/infraction/management.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 5f2529c6b..02e3ae1aa 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -170,13 +170,16 @@ async def send_active_infraction_message(ctx: Context, infraction: Infraction) - async def notify_infraction( infraction: Infraction, user: MemberOrUser, - reason: t.Optional[str] = None + reason: t.Optional[str] = None, + resend: bool = False ) -> bool: """ DM a user about their new infraction and return True if the DM is successful. `reason` can be used to override what is in `infraction`. Otherwise, this data will be retrieved from `infraction`. + + If `resend` is True, the DM will include both the duration and remaining time. """ infr_id = infraction["id"] infr_type = infraction["type"].replace("_", " ").title() @@ -191,11 +194,10 @@ async def notify_infraction( expires_at = time.format_relative(expiry) duration = time.humanize_delta(origin, expiry, max_units=2) - if infraction["active"]: + if infraction["active"] and resend: remaining = time.humanize_delta(expiry, arrow.utcnow(), max_units=2) - if duration != remaining: - duration += f" ({remaining} remaining)" - else: + duration += f" ({remaining} remaining)" + elif not infraction["active"]: expires_at += " (Inactive)" log.trace(f"Sending {user} a DM about their {infr_type} infraction.") diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 6ef382119..090fbcdbd 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -79,7 +79,7 @@ class ModManagement(commands.Cog): reason = infraction["reason"] or "No reason provided." reason += "\n\n**This is a re-sent message for a previously applied infraction which may have been edited.**" - if await _utils.notify_infraction(infraction, member, reason): + if await _utils.notify_infraction(infraction, member, reason, resend=True): await ctx.send(f":incoming_envelope: Resent DM for infraction `{id_}`.") else: await ctx.send(f"{constants.Emojis.failmail} Failed to resend DM for infraction `{id_}`.") -- cgit v1.2.3 From 8767bf6d73bf1e61f8047a4090930fcf0c08f42c Mon Sep 17 00:00:00 2001 From: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Date: Thu, 18 Aug 2022 19:02:05 +0300 Subject: Check if channel.guild is None --- bot/exts/moderation/modlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 67991730e..efa87ce25 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -552,7 +552,7 @@ class ModLog(Cog, name="ModLog"): channel = self.bot.get_channel(channel_id) # Ignore not found channels, DMs, and messages outside of the main guild. - if not channel or not hasattr(channel, "guild") or channel.guild.id != GuildConstant.id: + if not channel or channel.guild is None or channel.guild.id != GuildConstant.id: return True # Look at the parent channel of a thread. -- cgit v1.2.3 From 2e34dbd385b592b76f69c87154d412ba91c00901 Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 18 Aug 2022 15:14:02 -0400 Subject: fix: add check to prevent NoneType from passing into str concatenation --- bot/exts/moderation/infraction/_scheduler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 557fda25a..864aff9b2 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -443,11 +443,15 @@ class InfractionScheduler: try: # Mark infraction as inactive in the database. log.trace(f"Marking infraction #{id_} as inactive in the database.") - - data = {"active": False} - + + data = {"active": False, "reason": ""} + if pardon_reason is not None: - data["reason"] = infraction["reason"] + f" | Pardoned: {pardon_reason}" + # Append pardon reason to infraction in database. + if (punish_reason := infraction["reason"]) is not None: + data["reason"] = punish_reason + " | " + + data["reason"] += f"Pardoned: {pardon_reason}" await self.bot.api_client.patch( f"bot/infractions/{id_}", -- cgit v1.2.3 From e64572fcfa91b7785b1ac37d5fd54b9e5f479401 Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 18 Aug 2022 15:15:36 -0400 Subject: fix: lint (again) --- bot/exts/moderation/infraction/_scheduler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 864aff9b2..4d06c6b16 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -443,14 +443,14 @@ class InfractionScheduler: try: # Mark infraction as inactive in the database. log.trace(f"Marking infraction #{id_} as inactive in the database.") - + data = {"active": False, "reason": ""} - + if pardon_reason is not None: # Append pardon reason to infraction in database. if (punish_reason := infraction["reason"]) is not None: data["reason"] = punish_reason + " | " - + data["reason"] += f"Pardoned: {pardon_reason}" await self.bot.api_client.patch( -- cgit v1.2.3 From dada405211eac996196cdfb0496f4ff22f9a656a Mon Sep 17 00:00:00 2001 From: arl Date: Thu, 18 Aug 2022 19:01:22 -0400 Subject: fix: don't include replied mentions in mention filter (#2017) Co-authored-by: Izan Co-authored-by: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/rules/mentions.py | 56 +++++++++++++++++++++++++++++++++----- tests/bot/rules/test_mentions.py | 58 ++++++++++++++++++++++++++++++++++++---- tests/helpers.py | 22 +++++++++++++++ 3 files changed, 124 insertions(+), 12 deletions(-) diff --git a/bot/rules/mentions.py b/bot/rules/mentions.py index 6f5addad1..ca1d0c01c 100644 --- a/bot/rules/mentions.py +++ b/bot/rules/mentions.py @@ -1,23 +1,65 @@ from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from discord import DeletedReferencedMessage, Member, Message, MessageType, NotFound + +import bot +from bot.log import get_logger + +log = get_logger(__name__) async def apply( last_message: Message, recent_messages: List[Message], config: Dict[str, int] ) -> Optional[Tuple[str, Iterable[Member], Iterable[Message]]]: - """Detects total mentions exceeding the limit sent by a single user.""" + """ + Detects total mentions exceeding the limit sent by a single user. + + Excludes mentions that are bots, themselves, or replied users. + + In very rare cases, may not be able to determine a + mention was to a reply, in which case it is not ignored. + """ relevant_messages = tuple( msg for msg in recent_messages if msg.author == last_message.author ) + # We use `msg.mentions` here as that is supplied by the api itself, to determine who was mentioned. + # Additionally, `msg.mentions` includes the user replied to, even if the mention doesn't occur in the body. + # In order to exclude users who are mentioned as a reply, we check if the msg has a reference + # + # While we could use regex to parse the message content, and get a list of + # the mentions, that solution is very prone to breaking. + # We would need to deal with codeblocks, escaping markdown, and any discrepancies between + # our implementation and discord's markdown parser which would cause false positives or false negatives. + total_recent_mentions = 0 + for msg in relevant_messages: + # We check if the message is a reply, and if it is try to get the author + # since we ignore mentions of a user that we're replying to + reply_author = None - total_recent_mentions = sum( - not user.bot - for msg in relevant_messages - for user in msg.mentions - ) + if msg.type == MessageType.reply: + ref = msg.reference + + if not (resolved := ref.resolved): + # It is possible, in a very unusual situation, for a message to have a reference + # that is both not in the cache, and deleted while running this function. + # In such a situation, this will throw an error which we catch. + try: + resolved = await bot.instance.get_partial_messageable(resolved.channel_id).fetch_message( + resolved.message_id + ) + except NotFound: + log.info('Could not fetch the reference message as it has been deleted.') + + if resolved and not isinstance(resolved, DeletedReferencedMessage): + reply_author = resolved.author + + for user in msg.mentions: + # Don't count bot or self mentions, or the user being replied to (if applicable) + if user.bot or user in {msg.author, reply_author}: + continue + total_recent_mentions += 1 if total_recent_mentions > config['max']: return ( diff --git a/tests/bot/rules/test_mentions.py b/tests/bot/rules/test_mentions.py index f8805ac48..e1f904917 100644 --- a/tests/bot/rules/test_mentions.py +++ b/tests/bot/rules/test_mentions.py @@ -1,15 +1,32 @@ -from typing import Iterable +from typing import Iterable, Optional + +import discord from bot.rules import mentions from tests.bot.rules import DisallowedCase, RuleTest -from tests.helpers import MockMember, MockMessage +from tests.helpers import MockMember, MockMessage, MockMessageReference -def make_msg(author: str, total_user_mentions: int, total_bot_mentions: int = 0) -> MockMessage: - """Makes a message with `total_mentions` mentions.""" +def make_msg( + author: str, + total_user_mentions: int, + total_bot_mentions: int = 0, + *, + reference: Optional[MockMessageReference] = None +) -> MockMessage: + """Makes a message from `author` with `total_user_mentions` user mentions and `total_bot_mentions` bot mentions.""" user_mentions = [MockMember() for _ in range(total_user_mentions)] bot_mentions = [MockMember(bot=True) for _ in range(total_bot_mentions)] - return MockMessage(author=author, mentions=user_mentions+bot_mentions) + + mentions = user_mentions + bot_mentions + if reference is not None: + # For the sake of these tests we assume that all references are mentions. + mentions.append(reference.resolved.author) + msg_type = discord.MessageType.reply + else: + msg_type = discord.MessageType.default + + return MockMessage(author=author, mentions=mentions, reference=reference, type=msg_type) class TestMentions(RuleTest): @@ -56,6 +73,16 @@ class TestMentions(RuleTest): ("bob",), 3, ), + DisallowedCase( + [make_msg("bob", 3, reference=MockMessageReference())], + ("bob",), + 3, + ), + DisallowedCase( + [make_msg("bob", 3, reference=MockMessageReference(reference_author_is_bot=True))], + ("bob",), + 3 + ) ) await self.run_disallowed(cases) @@ -71,6 +98,27 @@ class TestMentions(RuleTest): await self.run_allowed(cases) + async def test_ignore_reply_mentions(self): + """Messages with an allowed amount of mentions in the content, also containing reply mentions.""" + cases = ( + [ + make_msg("bob", 2, reference=MockMessageReference()) + ], + [ + make_msg("bob", 2, reference=MockMessageReference(reference_author_is_bot=True)) + ], + [ + make_msg("bob", 2, reference=MockMessageReference()), + make_msg("bob", 0, reference=MockMessageReference()) + ], + [ + make_msg("bob", 2, reference=MockMessageReference(reference_author_is_bot=True)), + make_msg("bob", 0, reference=MockMessageReference(reference_author_is_bot=True)) + ] + ) + + await self.run_allowed(cases) + def relevant_messages(self, case: DisallowedCase) -> Iterable[MockMessage]: last_message = case.recent_messages[0] return tuple( diff --git a/tests/helpers.py b/tests/helpers.py index 17214553c..687e15b96 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -492,6 +492,28 @@ class MockAttachment(CustomMockMixin, unittest.mock.MagicMock): spec_set = attachment_instance +message_reference_instance = discord.MessageReference( + message_id=unittest.mock.MagicMock(id=1), + channel_id=unittest.mock.MagicMock(id=2), + guild_id=unittest.mock.MagicMock(id=3) +) + + +class MockMessageReference(CustomMockMixin, unittest.mock.MagicMock): + """ + A MagicMock subclass to mock MessageReference objects. + + Instances of this class will follow the specification of `discord.MessageReference` instances. + For more information, see the `MockGuild` docstring. + """ + spec_set = message_reference_instance + + def __init__(self, *, reference_author_is_bot: bool = False, **kwargs): + super().__init__(**kwargs) + referenced_msg_author = MockMember(name="bob", bot=reference_author_is_bot) + self.resolved = MockMessage(author=referenced_msg_author) + + class MockMessage(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock Message objects. -- cgit v1.2.3 From ea0f1af039e3fafea8456abd50d60de93ab8fca2 Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 18 Aug 2022 21:12:13 -0400 Subject: fix: remove chance of empty string overriding database infraction reason --- bot/exts/moderation/infraction/_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 4d06c6b16..bc944aa8e 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -444,7 +444,7 @@ class InfractionScheduler: # Mark infraction as inactive in the database. log.trace(f"Marking infraction #{id_} as inactive in the database.") - data = {"active": False, "reason": ""} + data = {"active": False} if pardon_reason is not None: # Append pardon reason to infraction in database. -- cgit v1.2.3 From 79ee2b0d574090e7f2c6006325742f1d7f9bb469 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 19 Aug 2022 15:10:13 +0100 Subject: fix "isistance" typo --- tests/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f3040b305..b2686b1d0 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -14,7 +14,7 @@ class DiscordMocksTests(unittest.TestCase): """Test if the default initialization of MockRole results in the correct object.""" role = helpers.MockRole() - # The `spec` argument makes sure `isistance` checks with `discord.Role` pass + # The `spec` argument makes sure `isinstance` checks with `discord.Role` pass self.assertIsInstance(role, discord.Role) self.assertEqual(role.name, "role") -- cgit v1.2.3 From ddd829fc235b0484064fce0c4924e980659ec2bb Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 19 Aug 2022 15:12:59 +0100 Subject: add support for keywords when using the "rules" command. This doesn't change the way the rules command originally worked and keeps the priority to rule numbers. But in case a number (or numbers) is not supplied, it will try to find a rule that maps to a the supplied keyword. --- bot/exts/info/information.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index e7d17c971..d53f4e323 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -518,12 +518,12 @@ class Information(Cog): await self.send_raw_content(ctx, message, json=True) @command(aliases=("rule",)) - async def rules(self, ctx: Context, rules: Greedy[int]) -> None: + async def rules(self, ctx: Context, rules: Greedy[int], keyword: Optional[str]) -> None: """Provides a link to all rules or, if specified, displays specific rule(s).""" rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") - if not rules: - # Rules were not submitted. Return the default description. + if not rules and not keyword: + # Neither rules nor keywords were submitted. Return the default description. rules_embed.description = ( "The rules and guidelines that apply to this community can be found on" " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" @@ -547,7 +547,21 @@ class Information(Cog): for rule in rules: self.bot.stats.incr(f"rule_uses.{rule}") - final_rules = tuple(f"**{pick}.** {full_rules[pick - 1]}" for pick in rules) + if rules: + final_rules = tuple(f"**{pick}.** {full_rules[pick - 1][0]}" for pick in rules) + else: + final_rules = tuple(f"**{pick + 1}** {full_rules[pick][0]}" for pick, rule in enumerate(full_rules) + if keyword in rule[1]) + + if not rules and not final_rules: + # This would mean that we didn't find a match for the used keyword + rules_embed.description = ( + f"There are currently no rules that correspond to keyword: {keyword}." + " If you think it should be added, please ask our admins and they'll take care of the rest." + ) + + await ctx.send(embed=rules_embed) + return await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From bf0a5172857cb4a9027d68cd8d0b735565952ce4 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 19 Aug 2022 13:17:04 -0400 Subject: fix: data dictionary guarantees reason key existence if pardon reason exists --- bot/exts/moderation/infraction/_scheduler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index bc944aa8e..db1204a8d 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -447,6 +447,7 @@ class InfractionScheduler: data = {"active": False} if pardon_reason is not None: + data["reason"] = "" # Append pardon reason to infraction in database. if (punish_reason := infraction["reason"]) is not None: data["reason"] = punish_reason + " | " -- cgit v1.2.3 From dcd946b32c199cba04b9d0399c1d26d4d3acff51 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Fri, 19 Aug 2022 14:16:37 -0400 Subject: Duration for DM changed to Edited flag --- bot/exts/moderation/infraction/_utils.py | 15 ++++++--------- bot/exts/moderation/infraction/management.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 02e3ae1aa..c03081b07 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -44,8 +44,8 @@ LONGEST_EXTRAS = max(len(INFRACTION_APPEAL_SERVER_FOOTER), len(INFRACTION_APPEAL INFRACTION_DESCRIPTION_TEMPLATE = ( "**Type:** {type}\n" - "**Expires:** {expires}\n" "**Duration:** {duration}\n" + "**Expires:** {expires}\n" "**Reason:** {reason}\n" ) @@ -170,16 +170,13 @@ async def send_active_infraction_message(ctx: Context, infraction: Infraction) - async def notify_infraction( infraction: Infraction, user: MemberOrUser, - reason: t.Optional[str] = None, - resend: bool = False + reason: t.Optional[str] = None ) -> bool: """ DM a user about their new infraction and return True if the DM is successful. `reason` can be used to override what is in `infraction`. Otherwise, this data will be retrieved from `infraction`. - - If `resend` is True, the DM will include both the duration and remaining time. """ infr_id = infraction["id"] infr_type = infraction["type"].replace("_", " ").title() @@ -194,12 +191,12 @@ async def notify_infraction( expires_at = time.format_relative(expiry) duration = time.humanize_delta(origin, expiry, max_units=2) - if infraction["active"] and resend: - remaining = time.humanize_delta(expiry, arrow.utcnow(), max_units=2) - duration += f" ({remaining} remaining)" - elif not infraction["active"]: + if not infraction["active"]: expires_at += " (Inactive)" + if infraction["inserted_at"] != infraction["last_applied"]: + duration += " (Edited)" + log.trace(f"Sending {user} a DM about their {infr_type} infraction.") if reason is None: diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 090fbcdbd..6ef382119 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -79,7 +79,7 @@ class ModManagement(commands.Cog): reason = infraction["reason"] or "No reason provided." reason += "\n\n**This is a re-sent message for a previously applied infraction which may have been edited.**" - if await _utils.notify_infraction(infraction, member, reason, resend=True): + if await _utils.notify_infraction(infraction, member, reason): await ctx.send(f":incoming_envelope: Resent DM for infraction `{id_}`.") else: await ctx.send(f"{constants.Emojis.failmail} Failed to resend DM for infraction `{id_}`.") -- cgit v1.2.3 From 0c64708ed660024f80c7f615ff95421fe353aec5 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Battiston Date: Sun, 28 Aug 2022 19:15:13 -0300 Subject: Remove unnecessary loggin --- bot/exts/moderation/infraction/infractions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 05cc74a03..2a325da77 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -125,8 +125,6 @@ class Infractions(InfractionScheduler, commands.Cog): infraction = await self.apply_ban(ctx, user, reason, duration_or_expiry=duration) if not infraction or not infraction.get("id"): # Ban was unsuccessful, quit early. - await ctx.send(":x: Failed to apply ban.") - log.error("Failed to apply ban to user %d", user.id) return # Calling commands directly skips discord.py's convertors, so we need to convert args manually. -- cgit v1.2.3 From 357ed80e2a0f35172ca91ceda7a353a4a8f765dd Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Thu, 8 Sep 2022 23:11:41 +0100 Subject: send the list of pre-defined keywords along with each invoked rule --- bot/exts/info/information.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index d53f4e323..54fa62d31 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -521,6 +521,7 @@ class Information(Cog): async def rules(self, ctx: Context, rules: Greedy[int], keyword: Optional[str]) -> None: """Provides a link to all rules or, if specified, displays specific rule(s).""" rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") + keyword = keyword.lower() if not rules and not keyword: # Neither rules nor keywords were submitted. Return the default description. @@ -548,9 +549,13 @@ class Information(Cog): self.bot.stats.incr(f"rule_uses.{rule}") if rules: - final_rules = tuple(f"**{pick}.** {full_rules[pick - 1][0]}" for pick in rules) + final_rules = tuple(f"**{pick}.** {full_rules[pick - 1][0]}\n\n" + f"You can also invoke this rule with the following keywords: " + f"{', '.join(full_rules[pick -1][1])}" for pick in rules) else: - final_rules = tuple(f"**{pick + 1}** {full_rules[pick][0]}" for pick, rule in enumerate(full_rules) + final_rules = tuple(f"**{pick + 1}** {full_rules[pick][0]}\n\n" + f"You can also invoke this rule with the following keywords: " + f"{', '.join(full_rules[pick][1])}" for pick, rule in enumerate(full_rules) if keyword in rule[1]) if not rules and not final_rules: -- cgit v1.2.3 From f924dd24a6846dcc00f96c7158111cfffc092014 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Thu, 8 Sep 2022 23:17:25 +0100 Subject: send the "no-match" error message in pure text format --- bot/exts/info/information.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 54fa62d31..9dd0a6d45 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -560,12 +560,9 @@ class Information(Cog): if not rules and not final_rules: # This would mean that we didn't find a match for the used keyword - rules_embed.description = ( + await ctx.send( f"There are currently no rules that correspond to keyword: {keyword}." - " If you think it should be added, please ask our admins and they'll take care of the rest." - ) - - await ctx.send(embed=rules_embed) + "If you think it should be added, please ask our admins and they'll take care of the rest.") return await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From 3fa905b14a8ac8e23159d487b2046d7ca62c7afb Mon Sep 17 00:00:00 2001 From: wookie184 Date: Sat, 10 Sep 2022 14:43:42 +0100 Subject: Fix error in help channels cog which assumed an embed would have a description --- bot/exts/help_channels/_channel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index d9cebf215..cfe774f4c 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -183,7 +183,8 @@ async def ensure_cached_claimant(channel: discord.TextChannel) -> None: 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 - if match := CLAIMED_BY_RE.match(message.embeds[0].description): + 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 -- cgit v1.2.3 From 350df44731875c4e8b28fc509d83b431cfefbc5c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 1 Sep 2022 21:31:33 +0100 Subject: Use venvs with poetry in Dockerfile This is required due to a regression in poetry, see https://github.com/HassanAbouelela/actions/pull/7 --- Dockerfile | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5bb400658..20b099a0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,26 @@ FROM --platform=linux/amd64 python:3.10-slim # Set pip to have no saved cache -ENV PIP_NO_CACHE_DIR=false \ - POETRY_VIRTUALENVS_CREATE=false +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + POETRY_VERSION=1.1.15 \ + POETRY_HOME="/opt/poetry" \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_NO_INTERACTION=1 \ + INSTALL_DIR="/opt/dependencies" \ + APP_DIR="/bot" +ENV PATH="$POETRY_HOME/bin:/$INSTALL_DIR/.venv/bin:$PATH" -# Install poetry -RUN pip install -U poetry +RUN apt-get update \ + && apt-get -y upgrade \ + && apt-get install --no-install-recommends -y curl \ + && apt-get clean && rm -rf /var/lib/apt/lists/* -# Create the working directory -WORKDIR /bot +RUN curl -sSL https://install.python-poetry.org | python # Install project dependencies +WORKDIR $INSTALL_DIR COPY pyproject.toml poetry.lock ./ RUN poetry install --no-dev @@ -22,6 +31,7 @@ ARG git_sha="development" ENV GIT_SHA=$git_sha # Copy the source code in last to optimize rebuilding the image +WORKDIR $APP_DIR COPY . . ENTRYPOINT ["python3"] -- cgit v1.2.3 From e12e5911f971beae0c07c12f9cf632bbaf4e6629 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 1 Sep 2022 21:31:59 +0100 Subject: Use HassanAbouelela/setup-python for CI --- .github/workflows/lint-test.yml | 62 ++++------------------------------------- 1 file changed, 5 insertions(+), 57 deletions(-) diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index 2b3dd5b4f..a331659e6 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -33,57 +33,16 @@ jobs: REDDIT_SECRET: ham REDIS_PASSWORD: '' - # Configure pip to cache dependencies and do a user install - PIP_NO_CACHE_DIR: false - PIP_USER: 1 - - # Make sure package manager does not use virtualenv - POETRY_VIRTUALENVS_CREATE: false - - # Specify explicit paths for python dependencies and the pre-commit - # environment so we know which directories to cache - POETRY_CACHE_DIR: ${{ github.workspace }}/.cache/py-user-base - PYTHONUSERBASE: ${{ github.workspace }}/.cache/py-user-base - PRE_COMMIT_HOME: ${{ github.workspace }}/.cache/pre-commit-cache - - # See https://github.com/pre-commit/pre-commit/issues/2178#issuecomment-1002163763 - # for why we set this. - SETUPTOOLS_USE_DISTUTILS: stdlib - steps: - - name: Add custom PYTHONUSERBASE to PATH - run: echo '${{ env.PYTHONUSERBASE }}/bin/' >> $GITHUB_PATH - - name: Checkout repository uses: actions/checkout@v2 - - name: Setup python - id: python - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - # This step caches our Python dependencies. To make sure we - # only restore a cache when the dependencies, the python version, - # the runner operating system, and the dependency location haven't - # changed, we create a cache key that is a composite of those states. - # - # Only when the context is exactly the same, we will restore the cache. - - name: Python Dependency Caching - uses: actions/cache@v2 - id: python_cache + - name: Install Python Dependencies + uses: HassanAbouelela/actions/setup-python@setup-python_v1.3.1 with: - path: ${{ env.PYTHONUSERBASE }} - key: "python-0-${{ runner.os }}-${{ env.PYTHONUSERBASE }}-\ - ${{ steps.python.outputs.python-version }}-\ - ${{ hashFiles('./pyproject.toml', './poetry.lock') }}" - - # Install our dependencies if we did not restore a dependency cache - - name: Install dependencies using poetry - if: steps.python_cache.outputs.cache-hit != 'true' - run: | - pip install poetry - poetry install + # Set dev=true to install flake8 extensions, which are dev dependencies + dev: true + python_version: '3.10' # Check all of our non-dev dependencies are compatible with the MIT license. # If you added a new dependencies that is being rejected, @@ -94,17 +53,6 @@ jobs: pip-licenses --allow-only="$ALLOWED_LICENSE" \ --package $(poetry export -f requirements.txt --without-hashes | sed "s/==.*//g" | tr "\n" " ") - # This step caches our pre-commit environment. To make sure we - # do create a new environment when our pre-commit setup changes, - # we create a cache key based on relevant factors. - - name: Pre-commit Environment Caching - uses: actions/cache@v2 - with: - path: ${{ env.PRE_COMMIT_HOME }} - key: "precommit-0-${{ runner.os }}-${{ env.PRE_COMMIT_HOME }}-\ - ${{ steps.python.outputs.python-version }}-\ - ${{ hashFiles('./.pre-commit-config.yaml') }}" - # We will not run `flake8` here, as we will use a separate flake8 # action. As pre-commit does not support user installs, we set # PIP_USER=0 to not do a user install. -- cgit v1.2.3 From 0b495e757c909d6a5802ffa729cdc9d3e4069978 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 10 Sep 2022 19:57:16 +0100 Subject: Bump poetry in Docker and lint to 1.2.0 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 20b099a0b..d0687475e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM --platform=linux/amd64 python:3.10-slim # Set pip to have no saved cache ENV PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=on \ - POETRY_VERSION=1.1.15 \ + POETRY_VERSION=1.2.0 \ POETRY_HOME="/opt/poetry" \ POETRY_VIRTUALENVS_IN_PROJECT=true \ POETRY_NO_INTERACTION=1 \ -- cgit v1.2.3 From c1ce6839ae4a944ee4207d8d283a738f64937b3a Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 11 Sep 2022 09:30:31 +0100 Subject: accept keywords and rule numbers in any order --- bot/exts/info/information.py | 58 +++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 9dd0a6d45..3dd12c005 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -8,7 +8,7 @@ from typing import Any, DefaultDict, Mapping, Optional, 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, Greedy, Paginator, command, group, has_any_role +from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role from discord.utils import escape_markdown from bot import constants @@ -518,12 +518,23 @@ class Information(Cog): await self.send_raw_content(ctx, message, json=True) @command(aliases=("rule",)) - async def rules(self, ctx: Context, rules: Greedy[int], keyword: Optional[str]) -> None: - """Provides a link to all rules or, if specified, displays specific rule(s).""" + async def rules(self, ctx: Context, *args: Optional[str]) -> None: + """ + Provides a link to all rules or, if specified, displays specific rules(s). + + It accepts either rule numbers or particular keywords that map to a particular rule. + Rule numbers and keywords can be sent in any order. + """ rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") - keyword = keyword.lower() + keywords, rules = [], [] + + for word in args: + if word.isdigit(): + rules.append(int(word)) + else: + keywords.append(word.lower()) - if not rules and not keyword: + if not rules and not keywords: # Neither rules nor keywords were submitted. Return the default description. rules_embed.description = ( "The rules and guidelines that apply to this community can be found on" @@ -538,30 +549,39 @@ class Information(Cog): # Remove duplicates and sort the rule indices rules = sorted(set(rules)) + # Remove duplicate keywords + keywords = set(keywords) invalid = ", ".join(str(index) for index in rules if index < 1 or index > len(full_rules)) - if invalid: + if invalid and not keywords: await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ...")) return - for rule in rules: - self.bot.stats.incr(f"rule_uses.{rule}") + final_rules = set() - if rules: - final_rules = tuple(f"**{pick}.** {full_rules[pick - 1][0]}\n\n" - f"You can also invoke this rule with the following keywords: " - f"{', '.join(full_rules[pick -1][1])}" for pick in rules) - else: - final_rules = tuple(f"**{pick + 1}** {full_rules[pick][0]}\n\n" - f"You can also invoke this rule with the following keywords: " - f"{', '.join(full_rules[pick][1])}" for pick, rule in enumerate(full_rules) - if keyword in rule[1]) + for pick in rules: + self.bot.stats.incr(f"rule_uses.{pick}") + final_rules.add( + f"**{pick}.** {full_rules[pick - 1][0]}\n\n" + f"You can also invoke this rule with the following keywords: " + f"{', '.join(full_rules[pick -1][1])}") + + for keyword in keywords: + for pick, rule in enumerate(full_rules): + if keyword not in rule[1]: + continue + + self.bot.stats.incr(f"rule_uses.{pick + 1}") + final_rules.add( + f"**{pick + 1}.** {full_rules[pick][0]}\n\n" + f"You can also invoke this rule with the following keywords: " + f"{', '.join(full_rules[pick][1])}") if not rules and not final_rules: - # This would mean that we didn't find a match for the used keyword + # This would mean that only keywords where used and no match for them was found await ctx.send( - f"There are currently no rules that correspond to keyword: {keyword}." + f"There are currently no rules that correspond to keywords: {[', '.join(keywords)]}." "If you think it should be added, please ask our admins and they'll take care of the rest.") return -- cgit v1.2.3 From 6b3b18078d8f8a636c5c5908f41f9075ab1ebfac Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 11 Sep 2022 10:29:35 +0100 Subject: add missing blank line --- bot/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/constants.py b/bot/constants.py index db98e6f47..68a96876f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -397,6 +397,7 @@ class Categories(metaclass=YAMLGetter): # 2021 Summer Code Jam summer_code_jam: int + class Channels(metaclass=YAMLGetter): section = "guild" subsection = "channels" -- cgit v1.2.3 From 8350f3641ff13da8f4d4849a2e44627f485e6a93 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 11 Sep 2022 10:35:41 +0100 Subject: suggest adding a keyword in dev & meta channels --- bot/exts/info/information.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 3dd12c005..5f996508f 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -19,7 +19,7 @@ from bot.errors import NonExistentRoleError from bot.log import get_logger from bot.pagination import LinePaginator from bot.utils import time -from bot.utils.channel import is_mod_channel, is_staff_channel +from bot.utils.channel import get_or_fetch_channel, is_mod_channel, is_staff_channel from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check from bot.utils.members import get_or_fetch_member @@ -580,9 +580,12 @@ class Information(Cog): if not rules and not final_rules: # This would mean that only keywords where used and no match for them was found + dev_contrib_channel = await get_or_fetch_channel(constants.Channels.dev_contrib) + meta_channel = await get_or_fetch_channel(constants.Channels.meta) await ctx.send( - f"There are currently no rules that correspond to keywords: {[', '.join(keywords)]}." - "If you think it should be added, please ask our admins and they'll take care of the rest.") + f"There are currently no rules that correspond to keywords: **{', '.join(keywords)}**.\n" + f"If you think it should be added, please suggest it in either " + f"{meta_channel.mention} or {dev_contrib_channel.mention}") return await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From 03a3a03d39099b09ed62337b075a9ada43353144 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 12 Sep 2022 21:37:04 +0100 Subject: Don't use fake in-project venvs for poetry Instead let poetry install the venv for the project in the right place, leading to a more 'traditional' poetry setup. --- Dockerfile | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index d0687475e..9cf9c7b27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,15 @@ FROM --platform=linux/amd64 python:3.10-slim -# Set pip to have no saved cache -ENV PIP_NO_CACHE_DIR=1 \ - PIP_DISABLE_PIP_VERSION_CHECK=on \ - POETRY_VERSION=1.2.0 \ - POETRY_HOME="/opt/poetry" \ - POETRY_VIRTUALENVS_IN_PROJECT=true \ - POETRY_NO_INTERACTION=1 \ - INSTALL_DIR="/opt/dependencies" \ - APP_DIR="/bot" - -ENV PATH="$POETRY_HOME/bin:/$INSTALL_DIR/.venv/bin:$PATH" +# Define Git SHA build argument for sentry +ARG git_sha="development" + +ENV POETRY_VERSION=1.2.0 \ + POETRY_HOME="/opt/poetry" \ + POETRY_NO_INTERACTION=1 \ + APP_DIR="/bot" \ + GIT_SHA=$git_sha + +ENV PATH="$POETRY_HOME/bin:$PATH" RUN apt-get update \ && apt-get -y upgrade \ @@ -20,19 +19,12 @@ RUN apt-get update \ RUN curl -sSL https://install.python-poetry.org | python # Install project dependencies -WORKDIR $INSTALL_DIR +WORKDIR $APP_DIR COPY pyproject.toml poetry.lock ./ RUN poetry install --no-dev -# Define Git SHA build argument -ARG git_sha="development" - -# Set Git SHA environment variable for Sentry -ENV GIT_SHA=$git_sha - # Copy the source code in last to optimize rebuilding the image -WORKDIR $APP_DIR COPY . . -ENTRYPOINT ["python3"] -CMD ["-m", "bot"] +ENTRYPOINT ["poetry"] +CMD ["run", "python", "-m", "bot"] -- cgit v1.2.3 From 71b1802b0f91b855443fa5ed01d9134944fd6f38 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 13 Sep 2022 00:25:36 +0100 Subject: Ignore mounted in-project venvs on startup Poetry's virtualenvs.in-project config deafults to None, meaning it will use in-project venvs if it finds one, otherwise it will use the cache dir. In dev we mount the entire root project directory to /bot. This means if the host's venv in in the project dir, this will get mounted and prioritised by poetry run. If the host is on a non-linux OS this will cause poetry to fail to boot. --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9cf9c7b27..da65bbf3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,12 @@ FROM --platform=linux/amd64 python:3.10-slim # Define Git SHA build argument for sentry ARG git_sha="development" +# POETRY_VIRTUALENVS_IN_PROJECT is required to ensure in-projects venvs mounted from the host in dev +# don't get prioritised by `poetry run` ENV POETRY_VERSION=1.2.0 \ POETRY_HOME="/opt/poetry" \ POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_IN_PROJECT=false \ APP_DIR="/bot" \ GIT_SHA=$git_sha @@ -21,7 +24,7 @@ RUN curl -sSL https://install.python-poetry.org | python # Install project dependencies WORKDIR $APP_DIR COPY pyproject.toml poetry.lock ./ -RUN poetry install --no-dev +RUN poetry install --without dev # Copy the source code in last to optimize rebuilding the image COPY . . -- cgit v1.2.3 From 2db6a241bde75e6cd36f3fe0f32a3530187a827a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 13 Sep 2022 00:27:53 +0100 Subject: Specify the path for poetry venvs Without this the venv would be created in /root/.cache and the nonn-root user that prod runs under would not have access to it. --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index da65bbf3b..65ca8ce51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,8 @@ ARG git_sha="development" # POETRY_VIRTUALENVS_IN_PROJECT is required to ensure in-projects venvs mounted from the host in dev # don't get prioritised by `poetry run` ENV POETRY_VERSION=1.2.0 \ - POETRY_HOME="/opt/poetry" \ + POETRY_HOME="/opt/poetry/home" \ + POETRY_CACHE_DIR="/opt/poetry/cache" \ POETRY_NO_INTERACTION=1 \ POETRY_VIRTUALENVS_IN_PROJECT=false \ APP_DIR="/bot" \ -- cgit v1.2.3 From 9c85342f4449f2116fb36ee1500ad06a94c1e14c Mon Sep 17 00:00:00 2001 From: Izan Date: Wed, 14 Sep 2022 20:56:30 +0100 Subject: Update docstrings & comment. --- bot/exts/utils/reminders.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index f0da37f3b..c65785314 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -214,7 +214,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 embeds. + Differs from botcore.utils.commands.clean_text_or_reply as allows for messages with no content. """ content = None if reference := ctx.message.reference: @@ -398,20 +398,7 @@ class Reminders(Cog): @remind_group.group(name="edit", aliases=("change", "modify"), invoke_without_command=True) async def edit_reminder_group(self, ctx: Context) -> None: - """ - Commands for modifying your current reminders. - - The `expiration` duration supports the following symbols for each unit of time: - - years: `Y`, `y`, `year`, `years` - - months: `m`, `month`, `months` - - weeks: `w`, `W`, `week`, `weeks` - - days: `d`, `D`, `day`, `days` - - hours: `H`, `h`, `hour`, `hours` - - minutes: `M`, `minute`, `minutes` - - seconds: `S`, `s`, `second`, `seconds` - - For example, to edit a reminder to expire in 3 days and 1 minute, you can do `!remind edit duration 1234 3d1M`. - """ + """Commands for modifying your current reminders.""" await ctx.send_help(ctx.command) @edit_reminder_group.command(name="duration", aliases=("time",)) @@ -434,11 +421,15 @@ class Reminders(Cog): @edit_reminder_group.command(name="content", aliases=("reason",)) async def edit_reminder_content(self, ctx: Context, id_: int, *, content: t.Optional[str] = None) -> None: - """Edit one of your reminder's content.""" + """ + Edit one of your reminder's content. + + You can either supply the new content yourself, or reply to a message to use its content. + """ if not content: content = await self.try_get_content_from_reply(ctx) if not content: - # Couldn't get content from reply + # Message doesn't have a reply to get content from return await self.edit_reminder(ctx, id_, {"content": content}) -- cgit v1.2.3 From 74d2b2153e5f6e53f26c9386898868b2e87aa0fb Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 19 Aug 2022 15:10:13 +0100 Subject: fix "isistance" typo --- tests/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f3040b305..b2686b1d0 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -14,7 +14,7 @@ class DiscordMocksTests(unittest.TestCase): """Test if the default initialization of MockRole results in the correct object.""" role = helpers.MockRole() - # The `spec` argument makes sure `isistance` checks with `discord.Role` pass + # The `spec` argument makes sure `isinstance` checks with `discord.Role` pass self.assertIsInstance(role, discord.Role) self.assertEqual(role.name, "role") -- cgit v1.2.3 From b1fcf6c9ac1b71658a8a8484029352f1e7d59d3d Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 19 Aug 2022 15:12:59 +0100 Subject: add support for keywords when using the "rules" command. This doesn't change the way the rules command originally worked and keeps the priority to rule numbers. But in case a number (or numbers) is not supplied, it will try to find a rule that maps to a the supplied keyword. --- bot/exts/info/information.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index e7d17c971..d53f4e323 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -518,12 +518,12 @@ class Information(Cog): await self.send_raw_content(ctx, message, json=True) @command(aliases=("rule",)) - async def rules(self, ctx: Context, rules: Greedy[int]) -> None: + async def rules(self, ctx: Context, rules: Greedy[int], keyword: Optional[str]) -> None: """Provides a link to all rules or, if specified, displays specific rule(s).""" rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") - if not rules: - # Rules were not submitted. Return the default description. + if not rules and not keyword: + # Neither rules nor keywords were submitted. Return the default description. rules_embed.description = ( "The rules and guidelines that apply to this community can be found on" " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" @@ -547,7 +547,21 @@ class Information(Cog): for rule in rules: self.bot.stats.incr(f"rule_uses.{rule}") - final_rules = tuple(f"**{pick}.** {full_rules[pick - 1]}" for pick in rules) + if rules: + final_rules = tuple(f"**{pick}.** {full_rules[pick - 1][0]}" for pick in rules) + else: + final_rules = tuple(f"**{pick + 1}** {full_rules[pick][0]}" for pick, rule in enumerate(full_rules) + if keyword in rule[1]) + + if not rules and not final_rules: + # This would mean that we didn't find a match for the used keyword + rules_embed.description = ( + f"There are currently no rules that correspond to keyword: {keyword}." + " If you think it should be added, please ask our admins and they'll take care of the rest." + ) + + await ctx.send(embed=rules_embed) + return await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From 276dff8d08c8dc1e434a18a0a42966e3b1089186 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Thu, 8 Sep 2022 23:11:41 +0100 Subject: send the list of pre-defined keywords along with each invoked rule --- bot/exts/info/information.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index d53f4e323..54fa62d31 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -521,6 +521,7 @@ class Information(Cog): async def rules(self, ctx: Context, rules: Greedy[int], keyword: Optional[str]) -> None: """Provides a link to all rules or, if specified, displays specific rule(s).""" rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") + keyword = keyword.lower() if not rules and not keyword: # Neither rules nor keywords were submitted. Return the default description. @@ -548,9 +549,13 @@ class Information(Cog): self.bot.stats.incr(f"rule_uses.{rule}") if rules: - final_rules = tuple(f"**{pick}.** {full_rules[pick - 1][0]}" for pick in rules) + final_rules = tuple(f"**{pick}.** {full_rules[pick - 1][0]}\n\n" + f"You can also invoke this rule with the following keywords: " + f"{', '.join(full_rules[pick -1][1])}" for pick in rules) else: - final_rules = tuple(f"**{pick + 1}** {full_rules[pick][0]}" for pick, rule in enumerate(full_rules) + final_rules = tuple(f"**{pick + 1}** {full_rules[pick][0]}\n\n" + f"You can also invoke this rule with the following keywords: " + f"{', '.join(full_rules[pick][1])}" for pick, rule in enumerate(full_rules) if keyword in rule[1]) if not rules and not final_rules: -- cgit v1.2.3 From 1ba79145834dc5363d1aed95e7728562353ef755 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Thu, 8 Sep 2022 23:17:25 +0100 Subject: send the "no-match" error message in pure text format --- bot/exts/info/information.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 54fa62d31..9dd0a6d45 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -560,12 +560,9 @@ class Information(Cog): if not rules and not final_rules: # This would mean that we didn't find a match for the used keyword - rules_embed.description = ( + await ctx.send( f"There are currently no rules that correspond to keyword: {keyword}." - " If you think it should be added, please ask our admins and they'll take care of the rest." - ) - - await ctx.send(embed=rules_embed) + "If you think it should be added, please ask our admins and they'll take care of the rest.") return await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From 1e286207fdfbfc0124845fc3620068db3f5657e6 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 11 Sep 2022 09:30:31 +0100 Subject: accept keywords and rule numbers in any order --- bot/exts/info/information.py | 58 +++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 9dd0a6d45..3dd12c005 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -8,7 +8,7 @@ from typing import Any, DefaultDict, Mapping, Optional, 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, Greedy, Paginator, command, group, has_any_role +from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role from discord.utils import escape_markdown from bot import constants @@ -518,12 +518,23 @@ class Information(Cog): await self.send_raw_content(ctx, message, json=True) @command(aliases=("rule",)) - async def rules(self, ctx: Context, rules: Greedy[int], keyword: Optional[str]) -> None: - """Provides a link to all rules or, if specified, displays specific rule(s).""" + async def rules(self, ctx: Context, *args: Optional[str]) -> None: + """ + Provides a link to all rules or, if specified, displays specific rules(s). + + It accepts either rule numbers or particular keywords that map to a particular rule. + Rule numbers and keywords can be sent in any order. + """ rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") - keyword = keyword.lower() + keywords, rules = [], [] + + for word in args: + if word.isdigit(): + rules.append(int(word)) + else: + keywords.append(word.lower()) - if not rules and not keyword: + if not rules and not keywords: # Neither rules nor keywords were submitted. Return the default description. rules_embed.description = ( "The rules and guidelines that apply to this community can be found on" @@ -538,30 +549,39 @@ class Information(Cog): # Remove duplicates and sort the rule indices rules = sorted(set(rules)) + # Remove duplicate keywords + keywords = set(keywords) invalid = ", ".join(str(index) for index in rules if index < 1 or index > len(full_rules)) - if invalid: + if invalid and not keywords: await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ...")) return - for rule in rules: - self.bot.stats.incr(f"rule_uses.{rule}") + final_rules = set() - if rules: - final_rules = tuple(f"**{pick}.** {full_rules[pick - 1][0]}\n\n" - f"You can also invoke this rule with the following keywords: " - f"{', '.join(full_rules[pick -1][1])}" for pick in rules) - else: - final_rules = tuple(f"**{pick + 1}** {full_rules[pick][0]}\n\n" - f"You can also invoke this rule with the following keywords: " - f"{', '.join(full_rules[pick][1])}" for pick, rule in enumerate(full_rules) - if keyword in rule[1]) + for pick in rules: + self.bot.stats.incr(f"rule_uses.{pick}") + final_rules.add( + f"**{pick}.** {full_rules[pick - 1][0]}\n\n" + f"You can also invoke this rule with the following keywords: " + f"{', '.join(full_rules[pick -1][1])}") + + for keyword in keywords: + for pick, rule in enumerate(full_rules): + if keyword not in rule[1]: + continue + + self.bot.stats.incr(f"rule_uses.{pick + 1}") + final_rules.add( + f"**{pick + 1}.** {full_rules[pick][0]}\n\n" + f"You can also invoke this rule with the following keywords: " + f"{', '.join(full_rules[pick][1])}") if not rules and not final_rules: - # This would mean that we didn't find a match for the used keyword + # This would mean that only keywords where used and no match for them was found await ctx.send( - f"There are currently no rules that correspond to keyword: {keyword}." + f"There are currently no rules that correspond to keywords: {[', '.join(keywords)]}." "If you think it should be added, please ask our admins and they'll take care of the rest.") return -- cgit v1.2.3 From 1fef952458f1d5282db3c707f203739f3698802c Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 11 Sep 2022 10:29:35 +0100 Subject: add missing blank line --- bot/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/constants.py b/bot/constants.py index db98e6f47..68a96876f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -397,6 +397,7 @@ class Categories(metaclass=YAMLGetter): # 2021 Summer Code Jam summer_code_jam: int + class Channels(metaclass=YAMLGetter): section = "guild" subsection = "channels" -- cgit v1.2.3 From ae0146c6437afca5b97ac0de909efab000977b0f Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 11 Sep 2022 10:35:41 +0100 Subject: suggest adding a keyword in dev & meta channels --- bot/exts/info/information.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 3dd12c005..5f996508f 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -19,7 +19,7 @@ from bot.errors import NonExistentRoleError from bot.log import get_logger from bot.pagination import LinePaginator from bot.utils import time -from bot.utils.channel import is_mod_channel, is_staff_channel +from bot.utils.channel import get_or_fetch_channel, is_mod_channel, is_staff_channel from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check from bot.utils.members import get_or_fetch_member @@ -580,9 +580,12 @@ class Information(Cog): if not rules and not final_rules: # This would mean that only keywords where used and no match for them was found + dev_contrib_channel = await get_or_fetch_channel(constants.Channels.dev_contrib) + meta_channel = await get_or_fetch_channel(constants.Channels.meta) await ctx.send( - f"There are currently no rules that correspond to keywords: {[', '.join(keywords)]}." - "If you think it should be added, please ask our admins and they'll take care of the rest.") + f"There are currently no rules that correspond to keywords: **{', '.join(keywords)}**.\n" + f"If you think it should be added, please suggest it in either " + f"{meta_channel.mention} or {dev_contrib_channel.mention}") return await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From 3dbb53fd11cf688289d6294210dbca3eef70a538 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 16 Sep 2022 17:04:49 +0100 Subject: use the cached channels instead of fetching them with each command execution --- bot/exts/info/information.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 5f996508f..67515bc57 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -19,7 +19,7 @@ from bot.errors import NonExistentRoleError 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, is_mod_channel, is_staff_channel +from bot.utils.channel import is_mod_channel, is_staff_channel from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check from bot.utils.members import get_or_fetch_member @@ -580,12 +580,10 @@ class Information(Cog): if not rules and not final_rules: # This would mean that only keywords where used and no match for them was found - dev_contrib_channel = await get_or_fetch_channel(constants.Channels.dev_contrib) - meta_channel = await get_or_fetch_channel(constants.Channels.meta) await ctx.send( f"There are currently no rules that correspond to keywords: **{', '.join(keywords)}**.\n" f"If you think it should be added, please suggest it in either " - f"{meta_channel.mention} or {dev_contrib_channel.mention}") + f"<#{constants.Channels.meta}> or <#{constants.Channels.dev_contrib}>") return await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From 4a8a0f63531585353c27a54539a9d5349799ef8a Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 16 Sep 2022 17:40:32 +0100 Subject: fix typo in docstrings --- bot/exts/info/information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 67515bc57..27a7b501d 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -520,7 +520,7 @@ class Information(Cog): @command(aliases=("rule",)) async def rules(self, ctx: Context, *args: Optional[str]) -> None: """ - Provides a link to all rules or, if specified, displays specific rules(s). + Provides a link to all rules or, if specified, displays specific rule(s). It accepts either rule numbers or particular keywords that map to a particular rule. Rule numbers and keywords can be sent in any order. -- cgit v1.2.3 From 32913523c0e34a91b621f6304ae95f8ba0728e14 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 16 Sep 2022 17:42:10 +0100 Subject: replace .isdigit predicate with a try except block --- bot/exts/info/information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 27a7b501d..b66c68f87 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -529,9 +529,9 @@ class Information(Cog): keywords, rules = [], [] for word in args: - if word.isdigit(): + try: rules.append(int(word)) - else: + except ValueError: keywords.append(word.lower()) if not rules and not keywords: -- cgit v1.2.3 From 95124cafd0b23c7e4bb2c0c19b3a043f17184ef2 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 16 Sep 2022 17:44:56 +0100 Subject: rename rules to rule_numbers --- bot/exts/info/information.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index b66c68f87..c706fa4cd 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -526,15 +526,15 @@ class Information(Cog): Rule numbers and keywords can be sent in any order. """ rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") - keywords, rules = [], [] + keywords, rule_numbers = [], [] for word in args: try: - rules.append(int(word)) + rule_numbers.append(int(word)) except ValueError: keywords.append(word.lower()) - if not rules and not keywords: + if not rule_numbers and not keywords: # Neither rules nor keywords were submitted. Return the default description. rules_embed.description = ( "The rules and guidelines that apply to this community can be found on" @@ -548,11 +548,11 @@ class Information(Cog): full_rules = await self.bot.api_client.get("rules", params={"link_format": "md"}) # Remove duplicates and sort the rule indices - rules = sorted(set(rules)) + rule_numbers = sorted(set(rule_numbers)) # Remove duplicate keywords keywords = set(keywords) - invalid = ", ".join(str(index) for index in rules if index < 1 or index > len(full_rules)) + invalid = ", ".join(str(index) for index in rule_numbers if index < 1 or index > len(full_rules)) if invalid and not keywords: await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ...")) @@ -560,7 +560,7 @@ class Information(Cog): final_rules = set() - for pick in rules: + for pick in rule_numbers: self.bot.stats.incr(f"rule_uses.{pick}") final_rules.add( f"**{pick}.** {full_rules[pick - 1][0]}\n\n" @@ -578,7 +578,7 @@ class Information(Cog): f"You can also invoke this rule with the following keywords: " f"{', '.join(full_rules[pick][1])}") - if not rules and not final_rules: + if not rule_numbers and not final_rules: # This would mean that only keywords where used and no match for them was found await ctx.send( f"There are currently no rules that correspond to keywords: **{', '.join(keywords)}**.\n" -- cgit v1.2.3 From 8028690d4eae27e57dfe1429b8067eabaa94eef9 Mon Sep 17 00:00:00 2001 From: Aleksey Zasorin Date: Fri, 16 Sep 2022 10:42:16 -0700 Subject: Removed "redis_ready" from additional_spec_asyncs in MockBot (#2275) The attribute was removed from Bot in fc05849 --- tests/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers.py b/tests/helpers.py index 687e15b96..a4b919dcb 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -317,7 +317,7 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock): guild_id=1, intents=discord.Intents.all(), ) - additional_spec_asyncs = ("wait_for", "redis_ready") + additional_spec_asyncs = ("wait_for",) def __init__(self, **kwargs) -> None: super().__init__(**kwargs) -- cgit v1.2.3 From 1c1650001f58e31721c79096642033fc005f8c82 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Fri, 16 Sep 2022 21:08:44 +0100 Subject: remove help message that displays the available keywords per rule --- bot/exts/info/information.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index c706fa4cd..42385edfb 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -562,10 +562,7 @@ class Information(Cog): for pick in rule_numbers: self.bot.stats.incr(f"rule_uses.{pick}") - final_rules.add( - f"**{pick}.** {full_rules[pick - 1][0]}\n\n" - f"You can also invoke this rule with the following keywords: " - f"{', '.join(full_rules[pick -1][1])}") + final_rules.add(f"**{pick}.** {full_rules[pick - 1][0]}") for keyword in keywords: for pick, rule in enumerate(full_rules): @@ -573,10 +570,7 @@ class Information(Cog): continue self.bot.stats.incr(f"rule_uses.{pick + 1}") - final_rules.add( - f"**{pick + 1}.** {full_rules[pick][0]}\n\n" - f"You can also invoke this rule with the following keywords: " - f"{', '.join(full_rules[pick][1])}") + final_rules.add(f"**{pick + 1}.** {full_rules[pick][0]}") if not rule_numbers and not final_rules: # This would mean that only keywords where used and no match for them was found -- cgit v1.2.3 From 343858f55ab8cbb482c70b8ffd3c8e48ab35d3b1 Mon Sep 17 00:00:00 2001 From: Mohammad Ibrahim <74553450+Ibrahim2750mi@users.noreply.github.com> Date: Sat, 17 Sep 2022 18:28:48 +0530 Subject: Removed italics from the help command (#2272) * removed asterisk from embed description * removed italics from line 334, 375 and 415 * pagination.py, L239 added italics --- bot/exts/info/help.py | 8 ++++---- bot/pagination.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 282f8c97a..48f840e51 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -307,7 +307,7 @@ class CustomHelpCommand(HelpCommand): # Remove line breaks from docstrings, if not used to separate paragraphs. # Allow overriding this behaviour via putting \u2003 at the start of a line. formatted_doc = re.sub("(? Date: Sat, 17 Sep 2022 19:24:37 +0100 Subject: Address Review - Convert `ids` to a set to remove duplicates - Limit the amount of reminders you can delete at once to 5 in order to prevent API spam --- bot/exts/utils/reminders.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 51fdd7873..4f7e1c255 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -465,9 +465,13 @@ class Reminders(Cog): @remind_group.command("delete", aliases=("remove", "cancel")) async def delete_reminder(self, ctx: Context, ids: Greedy[int]) -> None: - """Delete one of your active reminders.""" + """Delete up to (and including) 5 of your active reminders.""" + if len(ids) > 5: + await send_denial(ctx, "You can only delete a maximum of 5 reminders at once.") + return + deleted_ids = [] - for id_ in ids: + for id_ in set(ids): try: reminder_deleted = await self._delete_reminder(ctx, id_) except LockedResourceError: -- cgit v1.2.3 From 4dc9a952a71d03959e302434e8e9941e9bd3b577 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 18 Sep 2022 02:07:12 +0400 Subject: Use Python Poetry Base Action (#2277) --- Dockerfile | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 65ca8ce51..205b66209 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,11 @@ -FROM --platform=linux/amd64 python:3.10-slim +FROM --platform=linux/amd64 ghcr.io/chrislovering/python-poetry-base:3.10-slim # Define Git SHA build argument for sentry ARG git_sha="development" - -# POETRY_VIRTUALENVS_IN_PROJECT is required to ensure in-projects venvs mounted from the host in dev -# don't get prioritised by `poetry run` -ENV POETRY_VERSION=1.2.0 \ - POETRY_HOME="/opt/poetry/home" \ - POETRY_CACHE_DIR="/opt/poetry/cache" \ - POETRY_NO_INTERACTION=1 \ - POETRY_VIRTUALENVS_IN_PROJECT=false \ - APP_DIR="/bot" \ - GIT_SHA=$git_sha - -ENV PATH="$POETRY_HOME/bin:$PATH" - -RUN apt-get update \ - && apt-get -y upgrade \ - && apt-get install --no-install-recommends -y curl \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -RUN curl -sSL https://install.python-poetry.org | python +ENV GIT_SHA=$git_sha # Install project dependencies -WORKDIR $APP_DIR +WORKDIR /bot COPY pyproject.toml poetry.lock ./ RUN poetry install --without dev -- cgit v1.2.3 From e883168727b74fe754f51d16d8742262daccddc6 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 15:21:57 +0100 Subject: stop matching as soon as an invalid kw is encountered --- bot/exts/info/information.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 42385edfb..1ba8cf87e 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -546,33 +546,37 @@ class Information(Cog): return full_rules = await self.bot.api_client.get("rules", params={"link_format": "md"}) + available_keywords = set() + + for _, rule_keywords in full_rules: + available_keywords = available_keywords | set(rule_keywords) # Remove duplicates and sort the rule indices rule_numbers = sorted(set(rule_numbers)) - # Remove duplicate keywords - keywords = set(keywords) + # Remove duplicate keywords and preserve the order of initial keywords + keywords = list(dict.fromkeys(keywords)) invalid = ", ".join(str(index) for index in rule_numbers if index < 1 or index > len(full_rules)) - if invalid and not keywords: + if invalid: await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ...")) return - final_rules = set() - - for pick in rule_numbers: - self.bot.stats.incr(f"rule_uses.{pick}") - final_rules.add(f"**{pick}.** {full_rules[pick - 1][0]}") + final_rule_numbers = [] + final_rule_numbers.extend(rule_numbers) for keyword in keywords: + if keyword not in available_keywords: + break + for pick, rule in enumerate(full_rules): if keyword not in rule[1]: continue - self.bot.stats.incr(f"rule_uses.{pick + 1}") - final_rules.add(f"**{pick + 1}.** {full_rules[pick][0]}") + final_rule_numbers.append(pick + 1) + break - if not rule_numbers and not final_rules: + if not rule_numbers and not final_rule_numbers: # This would mean that only keywords where used and no match for them was found await ctx.send( f"There are currently no rules that correspond to keywords: **{', '.join(keywords)}**.\n" @@ -580,6 +584,13 @@ class Information(Cog): f"<#{constants.Channels.meta}> or <#{constants.Channels.dev_contrib}>") return + for rule_number in final_rule_numbers: + self.bot.stats.incr(f"rule_uses.{rule_number}") + + final_rule_numbers.sort() + final_rule_numbers = set(final_rule_numbers) + final_rules = [f"**{rule_number}.** {full_rules[rule_number - 1][0]}" for rule_number in final_rule_numbers] + await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From c17ca61dcbe63b6f458e31c7d113de4a48cc3b45 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 15:41:27 +0100 Subject: stop matching keywords when they're invalid upon triaging the rule numbers & keywords --- bot/exts/info/information.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 1ba8cf87e..cd1f0f957 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -528,11 +528,19 @@ class Information(Cog): rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") keywords, rule_numbers = [], [] + full_rules = await self.bot.api_client.get("rules", params={"link_format": "md"}) + available_keywords = set() + + for _, rule_keywords in full_rules: + available_keywords = available_keywords | set(rule_keywords) + for word in args: try: rule_numbers.append(int(word)) except ValueError: - keywords.append(word.lower()) + if (kw := word.lower()) not in available_keywords: + break + keywords.append(kw) if not rule_numbers and not keywords: # Neither rules nor keywords were submitted. Return the default description. @@ -545,12 +553,6 @@ class Information(Cog): await ctx.send(embed=rules_embed) return - full_rules = await self.bot.api_client.get("rules", params={"link_format": "md"}) - available_keywords = set() - - for _, rule_keywords in full_rules: - available_keywords = available_keywords | set(rule_keywords) - # Remove duplicates and sort the rule indices rule_numbers = sorted(set(rule_numbers)) # Remove duplicate keywords and preserve the order of initial keywords @@ -566,15 +568,10 @@ class Information(Cog): final_rule_numbers.extend(rule_numbers) for keyword in keywords: - if keyword not in available_keywords: - break - for pick, rule in enumerate(full_rules): - if keyword not in rule[1]: - continue - - final_rule_numbers.append(pick + 1) - break + if keyword in rule[1]: + final_rule_numbers.append(pick + 1) + break if not rule_numbers and not final_rule_numbers: # This would mean that only keywords where used and no match for them was found @@ -584,12 +581,13 @@ class Information(Cog): f"<#{constants.Channels.meta}> or <#{constants.Channels.dev_contrib}>") return - for rule_number in final_rule_numbers: - self.bot.stats.incr(f"rule_uses.{rule_number}") - + final_rules = [] final_rule_numbers.sort() final_rule_numbers = set(final_rule_numbers) - final_rules = [f"**{rule_number}.** {full_rules[rule_number - 1][0]}" for rule_number in final_rule_numbers] + + for rule_number in final_rule_numbers: + self.bot.stats.incr(f"rule_uses.{rule_number}") + final_rules.append(f"**{rule_number}.** {full_rules[rule_number - 1][0]}") await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) -- cgit v1.2.3 From 3119585850d01957d074862d25d01b78eb7fc438 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 16:11:10 +0100 Subject: rename index to rule_number --- bot/exts/info/information.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index cd1f0f957..52cc79aa0 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -558,7 +558,9 @@ class Information(Cog): # Remove duplicate keywords and preserve the order of initial keywords keywords = list(dict.fromkeys(keywords)) - invalid = ", ".join(str(index) for index in rule_numbers if index < 1 or index > len(full_rules)) + invalid = ", ".join( + str(rule_number) for rule_number in rule_numbers + if rule_number < 1 or rule_number > len(full_rules)) if invalid: await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ...")) -- cgit v1.2.3 From 144c036b6448cc77940e6c071728191b5c705cbb Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 17:58:23 +0100 Subject: rename pick to rule_number --- bot/exts/info/information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 52cc79aa0..c9ea025ed 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -570,9 +570,9 @@ class Information(Cog): final_rule_numbers.extend(rule_numbers) for keyword in keywords: - for pick, rule in enumerate(full_rules): + for rule_number, rule in enumerate(full_rules): if keyword in rule[1]: - final_rule_numbers.append(pick + 1) + final_rule_numbers.append(rule_number + 1) break if not rule_numbers and not final_rule_numbers: -- cgit v1.2.3 From 4d33f8ee7fcac5ffbc6957da5ac103268fcb30e3 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 18:01:44 +0100 Subject: remove unreachable code --- bot/exts/info/information.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index c9ea025ed..a0674f758 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -575,14 +575,6 @@ class Information(Cog): final_rule_numbers.append(rule_number + 1) break - if not rule_numbers and not final_rule_numbers: - # This would mean that only keywords where used and no match for them was found - await ctx.send( - f"There are currently no rules that correspond to keywords: **{', '.join(keywords)}**.\n" - f"If you think it should be added, please suggest it in either " - f"<#{constants.Channels.meta}> or <#{constants.Channels.dev_contrib}>") - return - final_rules = [] final_rule_numbers.sort() final_rule_numbers = set(final_rule_numbers) -- cgit v1.2.3 From d0dcceefd680629a2f5f9ff956ad7241727421a2 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 18:04:16 +0100 Subject: remove duplicate final rule numbers then sort --- bot/exts/info/information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index a0674f758..cbee1a519 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -576,8 +576,8 @@ class Information(Cog): break final_rules = [] - final_rule_numbers.sort() final_rule_numbers = set(final_rule_numbers) + final_rule_numbers = sorted(list(final_rule_numbers)) for rule_number in final_rule_numbers: self.bot.stats.incr(f"rule_uses.{rule_number}") -- cgit v1.2.3 From 3fd28f44eb8857c6409e739eab9cdc307d1febe9 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 18:07:24 +0100 Subject: remove useless initial sorting of keywords --- bot/exts/info/information.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index cbee1a519..b453a87cb 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -555,8 +555,6 @@ class Information(Cog): # Remove duplicates and sort the rule indices rule_numbers = sorted(set(rule_numbers)) - # Remove duplicate keywords and preserve the order of initial keywords - keywords = list(dict.fromkeys(keywords)) invalid = ", ".join( str(rule_number) for rule_number in rule_numbers -- cgit v1.2.3 From 9a867416c10d1d7543574bcb4642f645c6f98b29 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 18:11:45 +0100 Subject: enumerate full_rules with a start index of 1 --- bot/exts/info/information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index b453a87cb..e09083f90 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -568,9 +568,9 @@ class Information(Cog): final_rule_numbers.extend(rule_numbers) for keyword in keywords: - for rule_number, rule in enumerate(full_rules): + for rule_number, rule in enumerate(full_rules, start=1): if keyword in rule[1]: - final_rule_numbers.append(rule_number + 1) + final_rule_numbers.append(rule_number) break final_rules = [] -- cgit v1.2.3 From 44d1e790681f9430836694273f93a30994643a9b Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 18:30:46 +0100 Subject: replace the keywords set with a dict that maps each keyword to its rule number --- bot/exts/info/information.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index e09083f90..315d93f1d 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -529,16 +529,17 @@ class Information(Cog): keywords, rule_numbers = [], [] full_rules = await self.bot.api_client.get("rules", params={"link_format": "md"}) - available_keywords = set() + keyword_to_rule_number = dict() - for _, rule_keywords in full_rules: - available_keywords = available_keywords | set(rule_keywords) + for rule_number, (_, rule_keywords) in enumerate(full_rules, start=1): + 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 available_keywords: + if (kw := word.lower()) not in keyword_to_rule_number: break keywords.append(kw) @@ -568,10 +569,7 @@ class Information(Cog): final_rule_numbers.extend(rule_numbers) for keyword in keywords: - for rule_number, rule in enumerate(full_rules, start=1): - if keyword in rule[1]: - final_rule_numbers.append(rule_number) - break + final_rule_numbers.append(keyword_to_rule_number.get(keyword)) final_rules = [] final_rule_numbers = set(final_rule_numbers) -- cgit v1.2.3 From f65b49497fb402a732b6d723b5f2c1f8fd2f983a Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 18 Sep 2022 15:36:42 -0400 Subject: Moved `escape_markdown` after Truthy check (#2279) --- bot/exts/info/pypi.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 2d387df3d..bac7d2389 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -54,11 +54,12 @@ class PyPi(Cog): embed.url = info["package_url"] embed.colour = next(PYPI_COLOURS) - summary = escape_markdown(info["summary"]) + # Summary can be None if not provided by the package + summary: str | None = info["summary"] # Summary could be completely empty, or just whitespace. if summary and not summary.isspace(): - embed.description = summary + embed.description = escape_markdown(summary) else: embed.description = "No summary provided." -- cgit v1.2.3 From f95e4164406c76d2d0290cd05a2811ca875b3e23 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 21:56:46 +0100 Subject: remove redundant use of list transformation when sorting final rule numbers --- bot/exts/info/information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 315d93f1d..7034ca596 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -573,7 +573,7 @@ class Information(Cog): final_rules = [] final_rule_numbers = set(final_rule_numbers) - final_rule_numbers = sorted(list(final_rule_numbers)) + final_rule_numbers = sorted(final_rule_numbers) for rule_number in final_rule_numbers: self.bot.stats.incr(f"rule_uses.{rule_number}") -- cgit v1.2.3 From 032ef6512a30bb38193a9c4d01682ce67ac97697 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 22:07:18 +0100 Subject: subscribe directly to the keyword_to_rule_number dict instead of using .get --- bot/exts/info/information.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 7034ca596..ad5230a9c 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -566,10 +566,11 @@ class Information(Cog): return final_rule_numbers = [] + final_rule_numbers.extend(rule_numbers) for keyword in keywords: - final_rule_numbers.append(keyword_to_rule_number.get(keyword)) + final_rule_numbers.append(keyword_to_rule_number[keyword]) final_rules = [] final_rule_numbers = set(final_rule_numbers) -- cgit v1.2.3 From 987871da795bdfaf879f3fca5ce939761791296b Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 18 Sep 2022 22:11:02 +0100 Subject: determine final_rule_numbers value by subscribing to the keyword_to_rule_number using the matched keywords --- bot/exts/info/information.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index ad5230a9c..29b7b6fa3 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -565,16 +565,9 @@ class Information(Cog): await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ...")) return - final_rule_numbers = [] - - final_rule_numbers.extend(rule_numbers) - - for keyword in keywords: - final_rule_numbers.append(keyword_to_rule_number[keyword]) - final_rules = [] - final_rule_numbers = set(final_rule_numbers) - final_rule_numbers = sorted(final_rule_numbers) + final_rule_numbers = {keyword_to_rule_number[keyword] for keyword in keywords} + final_rule_numbers.update(rule_numbers) for rule_number in final_rule_numbers: self.bot.stats.incr(f"rule_uses.{rule_number}") -- cgit v1.2.3 From ae6b500ec4724a1f7e959b2b8a1eae2074407cee Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 19 Sep 2022 13:03:03 +0100 Subject: sort the list of final rule numbers --- bot/exts/info/information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 29b7b6fa3..7e27ed31c 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -569,7 +569,7 @@ class Information(Cog): final_rule_numbers = {keyword_to_rule_number[keyword] for keyword in keywords} final_rule_numbers.update(rule_numbers) - for rule_number in final_rule_numbers: + for rule_number in sorted(final_rule_numbers): self.bot.stats.incr(f"rule_uses.{rule_number}") final_rules.append(f"**{rule_number}.** {full_rules[rule_number - 1][0]}") -- cgit v1.2.3 From a7663c56c360e5f6799edce67265f0d6415c8be4 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 19 Sep 2022 16:46:54 +0100 Subject: return final list of rule numbers to be sent --- bot/exts/info/information.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 7e27ed31c..58f0764d8 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -3,7 +3,7 @@ import pprint import textwrap from collections import defaultdict from textwrap import shorten -from typing import Any, DefaultDict, Mapping, Optional, Tuple, Union +from typing import Any, DefaultDict, List, Mapping, Optional, Tuple, Union import rapidfuzz from botcore.site_api import ResponseCodeError @@ -518,7 +518,7 @@ class Information(Cog): await self.send_raw_content(ctx, message, json=True) @command(aliases=("rule",)) - async def rules(self, ctx: Context, *args: Optional[str]) -> None: + async def rules(self, ctx: Context, *args: Optional[str]) -> Optional[List[int]]: """ Provides a link to all rules or, if specified, displays specific rule(s). @@ -575,6 +575,8 @@ class Information(Cog): await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) + return final_rule_numbers + async def setup(bot: Bot) -> None: """Load the Information cog.""" -- cgit v1.2.3 From 738f2ee0e8e321d01e867a1d3f963d6991e6f22a Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 19 Sep 2022 16:50:30 +0100 Subject: add test that checks for the sent content if one invalid index is present in the input --- tests/bot/exts/info/test_information.py | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index d896b7652..c14453bd8 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -2,6 +2,7 @@ import textwrap import unittest import unittest.mock from datetime import datetime +from textwrap import shorten import discord @@ -573,3 +574,48 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase): create_embed.assert_called_once_with(ctx, self.target, False) ctx.send.assert_called_once() + + +class RuleCommandTests(unittest.IsolatedAsyncioTestCase): + """Tests for the `!rule` command.""" + + def setUp(self) -> None: + """Set up steps executed before each test is run.""" + self.bot = helpers.MockBot() + self.cog = information.Information(self.bot) + self.ctx = helpers.MockContext(author=helpers.MockMember(id=1, name="Bellaluma")) + self.full_rules = [ + ( + "First rule", + ["first", "number_one"] + ), + ( + "Second rule", + ["second", "number_two"] + ), + ( + "Third rule", + ["third", "number_three"] + ) + ] + self.bot.api_client.get.return_value = self.full_rules + + 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, )) + ] + + for raw_user_input, extracted_rule_numbers in test_cases: + invalid = ", ".join( + 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) + + self.assertEqual( + self.ctx.send.call_args, + unittest.mock.call(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ..."))) + self.assertEqual(None, final_rule_numbers) -- cgit v1.2.3 From 511724a704b2ef87dad8c24006cb7250aa964b35 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 19 Sep 2022 17:20:15 +0100 Subject: fix wrong type hint of the rules function return value --- bot/exts/info/information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 58f0764d8..d4f0ce008 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -3,7 +3,7 @@ import pprint import textwrap from collections import defaultdict from textwrap import shorten -from typing import Any, DefaultDict, List, Mapping, Optional, Tuple, Union +from typing import Any, DefaultDict, Mapping, Optional, Set, Tuple, Union import rapidfuzz from botcore.site_api import ResponseCodeError @@ -518,7 +518,7 @@ class Information(Cog): await self.send_raw_content(ctx, message, json=True) @command(aliases=("rule",)) - async def rules(self, ctx: Context, *args: Optional[str]) -> Optional[List[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). -- cgit v1.2.3 From 45b27ed9ac6a1ca55547defd1922fafcc7ac1ab9 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 19 Sep 2022 17:32:47 +0100 Subject: test the cases where default rules message is supposed to be sent --- tests/bot/exts/info/test_information.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index c14453bd8..61e8895b9 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -619,3 +619,34 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): self.ctx.send.call_args, unittest.mock.call(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ..."))) self.assertEqual(None, final_rule_numbers) + + async def test_return_correct_rule_numberstest_return_correct_rule_numbers(self): + + test_cases = [ + (("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: + final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *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), + ] + + description = ( + "The rules and guidelines that apply to this community can be found on" + " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" + " all members of the community to have read and understood these." + ) + + for raw_user_input, expected_matched_rule_numbers in test_cases: + final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) + embed = self.ctx.send.call_args.kwargs['embed'] + self.assertEqual(description, embed.description) + self.assertEqual(expected_matched_rule_numbers, final_rule_numbers) -- cgit v1.2.3 From 2e25073f807e02a72fe4cf2bbfa5cefe5024c8d7 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 19 Sep 2022 20:23:33 +0100 Subject: fix redundant test name --- tests/bot/exts/info/test_information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 61e8895b9..626c12c86 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -620,7 +620,7 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): unittest.mock.call(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ..."))) self.assertEqual(None, final_rule_numbers) - async def test_return_correct_rule_numberstest_return_correct_rule_numbers(self): + async def test_return_correct_rule_numbers(self): test_cases = [ (("1", "2", "first"), {1, 2}), -- cgit v1.2.3 From ae0c94b1d4dc676519301fe67a21425e5649effe Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 19 Sep 2022 22:57:54 +0100 Subject: add DEFAULT_RULES_DESCRIPTION to avoid duplication --- bot/constants.py | 6 ++++++ bot/exts/info/information.py | 8 ++------ tests/bot/exts/info/test_information.py | 9 ++------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 68a96876f..b1d392822 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -774,3 +774,9 @@ ERROR_REPLIES = [ "Noooooo!!", "I can't believe you've done this", ] + +DEFAULT_RULES_DESCRIPTION = ( + "The rules and guidelines that apply to this community can be found on" + " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" + " all members of the community to have read and understood these." +) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index d4f0ce008..a4816450f 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -13,6 +13,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot +from bot.constants import DEFAULT_RULES_DESCRIPTION from bot.converters import MemberOrUser from bot.decorators import in_whitelist from bot.errors import NonExistentRoleError @@ -545,12 +546,7 @@ class Information(Cog): if not rule_numbers and not keywords: # Neither rules nor keywords were submitted. Return the default description. - rules_embed.description = ( - "The rules and guidelines that apply to this community can be found on" - " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" - " all members of the community to have read and understood these." - ) - + rules_embed.description = DEFAULT_RULES_DESCRIPTION await ctx.send(embed=rules_embed) return diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 626c12c86..c0cfac430 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -7,6 +7,7 @@ from textwrap import shorten import discord from bot import constants +from bot.constants import DEFAULT_RULES_DESCRIPTION from bot.exts.info import information from bot.utils.checks import InWhitelistCheckFailure from tests import helpers @@ -639,14 +640,8 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): (("hello", "999"), None), ] - description = ( - "The rules and guidelines that apply to this community can be found on" - " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" - " all members of the community to have read and understood these." - ) - for raw_user_input, expected_matched_rule_numbers in test_cases: final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) embed = self.ctx.send.call_args.kwargs['embed'] - self.assertEqual(description, embed.description) + self.assertEqual(DEFAULT_RULES_DESCRIPTION, embed.description) self.assertEqual(expected_matched_rule_numbers, final_rule_numbers) -- cgit v1.2.3 From 06b2cde5e4cf7f5a9b914e4f4d1599d949773ecc Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 19 Sep 2022 23:01:01 +0100 Subject: use subTest to isolate assertions --- tests/bot/exts/info/test_information.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index c0cfac430..157c7141b 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -610,16 +610,17 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): ] for raw_user_input, extracted_rule_numbers in test_cases: - invalid = ", ".join( - str(rule_number) for rule_number in extracted_rule_numbers - if rule_number < 1 or rule_number > len(self.full_rules)) + with self.subTest(identifier=raw_user_input): + invalid = ", ".join( + 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, *raw_user_input) - self.assertEqual( - self.ctx.send.call_args, - unittest.mock.call(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ..."))) - self.assertEqual(None, final_rule_numbers) + self.assertEqual( + self.ctx.send.call_args, + unittest.mock.call(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ..."))) + self.assertEqual(None, final_rule_numbers) async def test_return_correct_rule_numbers(self): @@ -630,8 +631,9 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): ] for raw_user_input, expected_matched_rule_numbers in test_cases: - final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) - self.assertEqual(expected_matched_rule_numbers, final_rule_numbers) + with self.subTest(identifier=raw_user_input): + final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *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 = [ @@ -641,7 +643,8 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): ] for raw_user_input, expected_matched_rule_numbers in test_cases: - final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) - embed = self.ctx.send.call_args.kwargs['embed'] - self.assertEqual(DEFAULT_RULES_DESCRIPTION, embed.description) - self.assertEqual(expected_matched_rule_numbers, final_rule_numbers) + with self.subTest(identifier=raw_user_input): + final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) + embed = self.ctx.send.call_args.kwargs['embed'] + self.assertEqual(DEFAULT_RULES_DESCRIPTION, embed.description) + self.assertEqual(expected_matched_rule_numbers, final_rule_numbers) -- cgit v1.2.3 From edc9c4089d2f7f7cb70b813b3cfdcf52ab834714 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Tue, 20 Sep 2022 08:36:29 +0100 Subject: move DEFAULT_RULES_DESCRIPTION under information.py --- bot/constants.py | 6 ------ bot/exts/info/information.py | 7 ++++++- tests/bot/exts/info/test_information.py | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index b1d392822..68a96876f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -774,9 +774,3 @@ ERROR_REPLIES = [ "Noooooo!!", "I can't believe you've done this", ] - -DEFAULT_RULES_DESCRIPTION = ( - "The rules and guidelines that apply to this community can be found on" - " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" - " all members of the community to have read and understood these." -) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index a4816450f..2592e093d 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -13,7 +13,6 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.constants import DEFAULT_RULES_DESCRIPTION from bot.converters import MemberOrUser from bot.decorators import in_whitelist from bot.errors import NonExistentRoleError @@ -26,6 +25,12 @@ from bot.utils.members import get_or_fetch_member log = get_logger(__name__) +DEFAULT_RULES_DESCRIPTION = ( + "The rules and guidelines that apply to this community can be found on" + " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" + " all members of the community to have read and understood these." +) + class Information(Cog): """A cog with commands for generating embeds with server info, such as server stats and user info.""" diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 157c7141b..9f5143c01 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -7,7 +7,6 @@ from textwrap import shorten import discord from bot import constants -from bot.constants import DEFAULT_RULES_DESCRIPTION from bot.exts.info import information from bot.utils.checks import InWhitelistCheckFailure from tests import helpers @@ -646,5 +645,5 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase): with self.subTest(identifier=raw_user_input): final_rule_numbers = await self.cog.rules(self.cog, self.ctx, *raw_user_input) embed = self.ctx.send.call_args.kwargs['embed'] - self.assertEqual(DEFAULT_RULES_DESCRIPTION, embed.description) + self.assertEqual(information.DEFAULT_RULES_DESCRIPTION, embed.description) self.assertEqual(expected_matched_rule_numbers, final_rule_numbers) -- cgit v1.2.3 From 6fd9bae6da0986cd28e33dc0ef30599295c61835 Mon Sep 17 00:00:00 2001 From: Izan Date: Tue, 20 Sep 2022 22:51:40 +0100 Subject: Display mention & str of the mentionable object in `!remind list`. Added so that the user still gets information on what the mention is, when the mention doesn't render (due to client cache etc.) --- bot/exts/utils/reminders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 4f7e1c255..a7bcc7dae 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -353,7 +353,7 @@ class Reminders(Cog): mentions = ", ".join([ # Both Role and User objects have the `mention` attribute - mentionable.mention async for mentionable in self.get_mentionables(mentions) + f"{mentionable.mention} ({mentionable})" async for mentionable in self.get_mentionables(mentions) ]) mention_string = f"\n**Mentions:** {mentions}" if mentions else "" -- cgit v1.2.3