aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2022-06-07 22:39:55 +0100
committerGravatar GitHub <[email protected]>2022-06-07 22:39:55 +0100
commitcea65012e6bff8cc8daf1f1dab89b3137ba6f4d5 (patch)
tree9d908f3c997469d0f07de17776bffdf1bc2b8bde
parentMake small wording and style changes (diff)
parentMerge pull request #1936 from python-discord/feature/1903/fix-pin-inconsistency (diff)
Merge branch 'main' into improve-pastebin-error-handling
-rw-r--r--bot/exts/backend/branding/_cog.py130
-rw-r--r--bot/exts/backend/branding/_repository.py11
-rw-r--r--bot/exts/backend/error_handler.py34
-rw-r--r--bot/exts/filters/filtering.py3
-rw-r--r--bot/exts/help_channels/_caches.py4
-rw-r--r--bot/exts/help_channels/_cog.py14
-rw-r--r--bot/exts/help_channels/_message.py42
-rw-r--r--bot/exts/info/doc/_cog.py1
-rw-r--r--bot/exts/info/doc/_html.py3
-rw-r--r--bot/exts/info/doc/_parsing.py3
-rw-r--r--bot/exts/info/doc/_redis_cache.py40
-rw-r--r--bot/exts/info/tags.py6
-rw-r--r--bot/exts/moderation/clean.py57
-rw-r--r--bot/exts/moderation/modlog.py47
-rw-r--r--bot/exts/utils/thread_bumper.py39
-rw-r--r--poetry.lock257
-rw-r--r--pyproject.toml4
-rw-r--r--tests/bot/exts/backend/test_error_handler.py9
18 files changed, 401 insertions, 303 deletions
diff --git a/bot/exts/backend/branding/_cog.py b/bot/exts/backend/branding/_cog.py
index e55aa1995..ff2704835 100644
--- a/bot/exts/backend/branding/_cog.py
+++ b/bot/exts/backend/branding/_cog.py
@@ -1,6 +1,7 @@
import asyncio
import contextlib
import random
+import types
import typing as t
from datetime import timedelta
from enum import Enum
@@ -104,19 +105,24 @@ class Branding(commands.Cog):
"""
# RedisCache[
- # "daemon_active": bool | If True, daemon starts on start-up. Controlled via commands.
- # "event_path": str | Current event's path in the branding repo.
- # "event_description": str | Current event's Markdown description.
- # "event_duration": str | Current event's human-readable date range.
- # "banner_hash": str | SHA of the currently applied banner.
- # "icons_hash": str | Compound SHA of all icons in current rotation.
- # "last_rotation_timestamp": float | POSIX UTC timestamp.
+ # "daemon_active": bool | If True, daemon starts on start-up. Controlled via commands.
+ # "event_path": str | Current event's path in the branding repo.
+ # "event_description": str | Current event's Markdown description.
+ # "event_duration": str | Current event's human-readable date range.
+ # "banners_hash": str | Compound SHA of all banners in the current rotation.
+ # "icons_hash": str | Compound SHA of all icons in current rotation.
+ # "last_icon_rotation_timestamp": float | POSIX UTC timestamp.
+ # "last_banner_rotation_timestamp": float | POSIX UTC timestamp.
# ]
cache_information = RedisCache()
- # Icons in current rotation. Keys (str) are download URLs, values (int) track the amount of times each
- # icon has been used in the current rotation.
- cache_icons = RedisCache()
+ # Icons and banners in current rotation.
+ # Keys (str) are download URLs, values (int) track the amount of times each
+ # asset has been used in the current rotation.
+ asset_caches = types.MappingProxyType({
+ AssetType.ICON: RedisCache(namespace="Branding.icon_cache"),
+ AssetType.BANNER: RedisCache(namespace="Branding.banner_cache")
+ })
# All available event names & durations. Cached by the daemon nightly; read by the calendar command.
cache_events = RedisCache()
@@ -164,107 +170,92 @@ class Branding(commands.Cog):
log.trace("Asset uploaded successfully.")
return True
- async def apply_banner(self, banner: RemoteObject) -> bool:
+ async def rotate_assets(self, asset_type: AssetType) -> bool:
"""
- Apply `banner` to the guild and cache its hash if successful.
+ Choose and apply the next-up asset in rotation.
- Banners should always be applied via this method to ensure that the last hash is cached.
-
- Return a boolean indicating whether the application was successful.
- """
- success = await self.apply_asset(AssetType.BANNER, banner.download_url)
-
- if success:
- await self.cache_information.set("banner_hash", banner.sha)
-
- return success
-
- async def rotate_icons(self) -> bool:
- """
- Choose and apply the next-up icon in rotation.
-
- We keep track of the amount of times each icon has been used. The values in `cache_icons` can be understood
- to be iteration IDs. When an icon is chosen & applied, we bump its count, pushing it into the next iteration.
+ We keep track of the amount of times each asset has been used. The values in the cache can be understood
+ to be iteration IDs. When an asset is chosen & applied, we bump its count, pushing it into the next iteration.
Once the current iteration (lowest count in the cache) depletes, we move onto the next iteration.
- In the case that there is only 1 icon in the rotation and has already been applied, do nothing.
+ In the case that there is only 1 asset in the rotation and has already been applied, do nothing.
- Return a boolean indicating whether a new icon was applied successfully.
+ Return a boolean indicating whether a new asset was applied successfully.
"""
- log.debug("Rotating icons.")
+ log.debug(f"Rotating {asset_type.value}s.")
- state = await self.cache_icons.to_dict()
- log.trace(f"Total icons in rotation: {len(state)}.")
+ state = await self.asset_caches[asset_type].to_dict()
+ log.trace(f"Total {asset_type.value}s in rotation: {len(state)}.")
if not state: # This would only happen if rotation not initiated, but we can handle gracefully.
- log.warning("Attempted icon rotation with an empty icon cache. This indicates wrong logic.")
+ log.warning(f"Attempted {asset_type.value} rotation with an empty cache. This indicates wrong logic.")
return False
if len(state) == 1 and 1 in state.values():
- log.debug("Aborting icon rotation: only 1 icon is available and has already been applied.")
+ log.debug(f"Aborting {asset_type.value} rotation: only 1 asset is available and has already been applied.")
return False
current_iteration = min(state.values()) # Choose iteration to draw from.
options = [download_url for download_url, times_used in state.items() if times_used == current_iteration]
- log.trace(f"Choosing from {len(options)} icons in iteration {current_iteration}.")
- next_icon = random.choice(options)
+ log.trace(f"Choosing from {len(options)} {asset_type.value}s in iteration {current_iteration}.")
+ next_asset = random.choice(options)
- success = await self.apply_asset(AssetType.ICON, next_icon)
+ success = await self.apply_asset(asset_type, next_asset)
if success:
- await self.cache_icons.increment(next_icon) # Push the icon into the next iteration.
+ await self.asset_caches[asset_type].increment(next_asset) # Push the asset into the next iteration.
timestamp = Arrow.utcnow().timestamp()
- await self.cache_information.set("last_rotation_timestamp", timestamp)
+ await self.cache_information.set(f"last_{asset_type.value}_rotation_timestamp", timestamp)
return success
- async def maybe_rotate_icons(self) -> None:
+ async def maybe_rotate_assets(self, asset_type: AssetType) -> None:
"""
- Call `rotate_icons` if the configured amount of time has passed since last rotation.
+ Call `rotate_assets` if the configured amount of time has passed since last rotation.
We offset the calculated time difference into the future to avoid off-by-a-little-bit errors. Because there
is work to be done before the timestamp is read and written, the next read will likely commence slightly
under 24 hours after the last write.
"""
- log.debug("Checking whether it's time for icons to rotate.")
+ log.debug(f"Checking whether it's time for {asset_type.value}s to rotate.")
- last_rotation_timestamp = await self.cache_information.get("last_rotation_timestamp")
+ last_rotation_timestamp = await self.cache_information.get(f"last_{asset_type.value}_rotation_timestamp")
if last_rotation_timestamp is None: # Maiden case ~ never rotated.
- await self.rotate_icons()
+ await self.rotate_assets(asset_type)
return
last_rotation = Arrow.utcfromtimestamp(last_rotation_timestamp)
difference = (Arrow.utcnow() - last_rotation) + timedelta(minutes=5)
- log.trace(f"Icons last rotated at {last_rotation} (difference: {difference}).")
+ log.trace(f"{asset_type.value.title()}s last rotated at {last_rotation} (difference: {difference}).")
if difference.days >= BrandingConfig.cycle_frequency:
- await self.rotate_icons()
+ await self.rotate_assets(asset_type)
- async def initiate_icon_rotation(self, available_icons: t.List[RemoteObject]) -> None:
+ async def initiate_rotation(self, asset_type: AssetType, available_assets: list[RemoteObject]) -> None:
"""
- Set up a new icon rotation.
+ Set up a new asset rotation.
- This function should be called whenever available icons change. This is generally the case when we enter
+ This function should be called whenever available asset groups change. This is generally the case when we enter
a new event, but potentially also when the assets of an on-going event change. In such cases, a reset
- of `cache_icons` is necessary, because it contains download URLs which may have gotten stale.
+ of the cache is necessary, because it contains download URLs which may have gotten stale.
- This function does not upload a new icon!
+ This function does not upload a new asset!
"""
- log.debug("Initiating new icon rotation.")
+ log.debug(f"Initiating new {asset_type.value} rotation.")
- await self.cache_icons.clear()
+ await self.asset_caches[asset_type].clear()
- new_state = {icon.download_url: 0 for icon in available_icons}
- await self.cache_icons.update(new_state)
+ new_state = {asset.download_url: 0 for asset in available_assets}
+ await self.asset_caches[asset_type].update(new_state)
- log.trace(f"Icon rotation initiated for {len(new_state)} icons.")
+ log.trace(f"{asset_type.value.title()} rotation initiated for {len(new_state)} assets.")
- await self.cache_information.set("icons_hash", compound_hash(available_icons))
+ await self.cache_information.set(f"{asset_type.value}s_hash", compound_hash(available_assets))
async def send_info_embed(self, channel_id: int, *, is_notification: bool) -> None:
"""
@@ -316,10 +307,12 @@ class Branding(commands.Cog):
"""
log.info(f"Entering event: '{event.path}'.")
- banner_success = await self.apply_banner(event.banner) # Only one asset ~ apply directly.
+ # Prepare and apply new icon and banner rotations
+ await self.initiate_rotation(AssetType.ICON, event.icons)
+ await self.initiate_rotation(AssetType.BANNER, event.banners)
- await self.initiate_icon_rotation(event.icons) # Prepare a new rotation.
- icon_success = await self.rotate_icons() # Apply an icon from the new rotation.
+ icon_success = await self.rotate_assets(AssetType.ICON)
+ banner_success = await self.rotate_assets(AssetType.BANNER)
# This will only be False in the case of a manual same-event re-synchronisation.
event_changed = event.path != await self.cache_information.get("event_path")
@@ -454,16 +447,19 @@ class Branding(commands.Cog):
log.trace("Daemon main: event has not changed, checking for change in assets.")
- if new_event.banner.sha != await self.cache_information.get("banner_hash"):
+ if compound_hash(new_event.banners) != await self.cache_information.get("banners_hash"):
log.debug("Daemon main: detected banner change.")
- await self.apply_banner(new_event.banner)
+ await self.initiate_rotation(AssetType.BANNER, new_event.banners)
+ await self.rotate_assets(AssetType.BANNER)
+ else:
+ await self.maybe_rotate_assets(AssetType.BANNER)
if compound_hash(new_event.icons) != await self.cache_information.get("icons_hash"):
log.debug("Daemon main: detected icon change.")
- await self.initiate_icon_rotation(new_event.icons)
- await self.rotate_icons()
+ await self.initiate_rotation(AssetType.ICON, new_event.icons)
+ await self.rotate_assets(AssetType.ICON)
else:
- await self.maybe_rotate_icons()
+ await self.maybe_rotate_assets(AssetType.ICON)
@tasks.loop(hours=24)
async def daemon_loop(self) -> None:
diff --git a/bot/exts/backend/branding/_repository.py b/bot/exts/backend/branding/_repository.py
index d88ea67f3..e14f0a1ef 100644
--- a/bot/exts/backend/branding/_repository.py
+++ b/bot/exts/backend/branding/_repository.py
@@ -64,8 +64,8 @@ class Event(t.NamedTuple):
path: str # Path from repo root where event lives. This is the event's identity.
meta: MetaFile
- banner: RemoteObject
- icons: t.List[RemoteObject]
+ banners: list[RemoteObject]
+ icons: list[RemoteObject]
def __str__(self) -> str:
return f"<Event at '{self.path}'>"
@@ -163,21 +163,24 @@ class BrandingRepository:
"""
contents = await self.fetch_directory(directory.path)
- missing_assets = {"meta.md", "banner.png", "server_icons"} - contents.keys()
+ missing_assets = {"meta.md", "server_icons", "banners"} - contents.keys()
if missing_assets:
raise BrandingMisconfiguration(f"Directory is missing following assets: {missing_assets}")
server_icons = await self.fetch_directory(contents["server_icons"].path, types=("file",))
+ banners = await self.fetch_directory(contents["banners"].path, types=("file",))
if len(server_icons) == 0:
raise BrandingMisconfiguration("Found no server icons!")
+ if len(banners) == 0:
+ raise BrandingMisconfiguration("Found no server banners!")
meta_bytes = await self.fetch_file(contents["meta.md"].download_url)
meta_file = self.parse_meta_file(meta_bytes)
- return Event(directory.path, meta_file, contents["banner.png"], list(server_icons.values()))
+ return Event(directory.path, meta_file, list(banners.values()), list(server_icons.values()))
async def get_events(self) -> t.List[Event]:
"""
diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py
index 5391a7f15..761991488 100644
--- a/bot/exts/backend/error_handler.py
+++ b/bot/exts/backend/error_handler.py
@@ -1,3 +1,4 @@
+import copy
import difflib
from botcore.site_api import ResponseCodeError
@@ -65,6 +66,8 @@ class ErrorHandler(Cog):
if isinstance(e, errors.CommandNotFound) and not getattr(ctx, "invoked_from_error_handler", False):
if await self.try_silence(ctx):
return
+ if await self.try_run_eval(ctx):
+ return
await self.try_get_tag(ctx) # Try to look for a tag with the command's name
elif isinstance(e, errors.UserInputError):
log.debug(debug_message)
@@ -179,6 +182,30 @@ class ErrorHandler(Cog):
if not any(role.id in MODERATION_ROLES for role in ctx.author.roles):
await self.send_command_suggestion(ctx, ctx.invoked_with)
+ async def try_run_eval(self, ctx: Context) -> bool:
+ """
+ Attempt to run eval command with backticks directly after command.
+
+ For example: !eval```print("hi")```
+
+ Return True if command was invoked, else False
+ """
+ msg = copy.copy(ctx.message)
+
+ command, sep, end = msg.content.partition("```")
+ msg.content = command + " " + sep + end
+ new_ctx = await self.bot.get_context(msg)
+
+ eval_command = self.bot.get_command("eval")
+ if eval_command is None or new_ctx.command != eval_command:
+ return False
+
+ log.debug("Running fixed eval command.")
+ new_ctx.invoked_from_error_handler = True
+ await self.bot.invoke(new_ctx)
+
+ return True
+
async def send_command_suggestion(self, ctx: Context, command_name: str) -> None:
"""Sends user similar commands if any can be found."""
# No similar tag found, or tag on cooldown -
@@ -284,8 +311,11 @@ class ErrorHandler(Cog):
await ctx.send("There does not seem to be anything matching your query.")
ctx.bot.stats.incr("errors.api_error_404")
elif e.status == 400:
- content = await e.response.json()
- log.debug(f"API responded with 400 for command {ctx.command}: %r.", content)
+ log.error(
+ "API responded with 400 for command %s: %r.",
+ ctx.command,
+ e.response_json or e.response_text,
+ )
await ctx.send("According to the API, your request is malformed.")
ctx.bot.stats.incr("errors.api_error_400")
elif 500 <= e.status < 600:
diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py
index 21c155902..70f59c1ee 100644
--- a/bot/exts/filters/filtering.py
+++ b/bot/exts/filters/filtering.py
@@ -1,6 +1,7 @@
import asyncio
import re
import unicodedata
+import urllib.parse
from datetime import timedelta
from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union
@@ -565,6 +566,7 @@ class Filtering(Cog):
If any are detected, a dictionary of invite data is returned, with a key per invite.
If none are detected, False is returned.
+ If we are unable to process an invite, True is returned.
Attempts to catch some of common ways to try to cheat the system.
"""
@@ -577,6 +579,7 @@ class Filtering(Cog):
invites = [m.group("invite") for m in DISCORD_INVITE.finditer(text)]
invite_data = dict()
for invite in invites:
+ invite = urllib.parse.quote_plus(invite.rstrip("/"))
if invite in invite_data:
continue
diff --git a/bot/exts/help_channels/_caches.py b/bot/exts/help_channels/_caches.py
index 8d45c2466..937c4ab57 100644
--- a/bot/exts/help_channels/_caches.py
+++ b/bot/exts/help_channels/_caches.py
@@ -17,10 +17,6 @@ claimant_last_message_times = RedisCache(namespace="HelpChannels.claimant_last_m
# RedisCache[discord.TextChannel.id, UtcPosixTimestamp]
non_claimant_last_message_times = RedisCache(namespace="HelpChannels.non_claimant_last_message_times")
-# This cache maps a help channel to original question message in same channel.
-# RedisCache[discord.TextChannel.id, discord.Message.id]
-question_messages = RedisCache(namespace="HelpChannels.question_messages")
-
# This cache keeps track of the dynamic message ID for
# the continuously updated message in the #How-to-get-help channel.
dynamic_message = RedisCache(namespace="HelpChannels.dynamic_message")
diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index bef6f3709..f1351013a 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -394,17 +394,17 @@ class HelpChannels(commands.Cog):
log.trace("Making a channel available.")
channel = await self.get_available_candidate()
- log.info(f"Making #{channel} ({channel.id}) available.")
+ channel_str = f"#{channel} ({channel.id})"
+ log.info(f"Making {channel_str} available.")
await _message.send_available_message(channel)
- log.trace(f"Moving #{channel} ({channel.id}) to the Available category.")
+ log.trace(f"Moving {channel_str} to the Available category.")
# Unpin any previously stuck pins
- log.trace(f"Looking for pins stuck in #{channel} ({channel.id}).")
- for message in await channel.pins():
- await _message.pin_wrapper(message.id, channel, pin=False)
- log.debug(f"Removed a stuck pin from #{channel} ({channel.id}). ID: {message.id}")
+ log.trace(f"Looking for pins stuck in {channel_str}.")
+ if stuck_pins := await _message.unpin_all(channel):
+ log.debug(f"Removed {stuck_pins} stuck pins from {channel_str}.")
await _channel.move_to_bottom(
channel=channel,
@@ -480,7 +480,7 @@ class HelpChannels(commands.Cog):
else:
await members.handle_role_change(claimant, claimant.remove_roles, self.cooldown_role)
- await _message.unpin(channel)
+ await _message.unpin_all(channel)
await _stats.report_complete_session(channel.id, closed_on)
await self.move_to_dormant(channel)
diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py
index 128fa847b..00d57ea40 100644
--- a/bot/exts/help_channels/_message.py
+++ b/bot/exts/help_channels/_message.py
@@ -214,9 +214,8 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar
async def pin(message: discord.Message) -> None:
- """Pin an initial question `message` and store it in a cache."""
- if await pin_wrapper(message.id, message.channel, pin=True):
- await _caches.question_messages.set(message.channel.id, message.id)
+ """Pin an initial question `message`."""
+ await _pin_wrapper(message, pin=True)
async def send_available_message(channel: discord.TextChannel) -> None:
@@ -240,13 +239,14 @@ async def send_available_message(channel: discord.TextChannel) -> None:
await channel.send(embed=embed)
-async def unpin(channel: discord.TextChannel) -> None:
- """Unpin the initial question message sent in `channel`."""
- msg_id = await _caches.question_messages.pop(channel.id)
- if msg_id is None:
- log.debug(f"#{channel} ({channel.id}) doesn't have a message pinned.")
- else:
- await pin_wrapper(msg_id, channel, pin=False)
+async def unpin_all(channel: discord.TextChannel) -> int:
+ """Unpin all pinned messages in `channel` and return the amount of unpinned messages."""
+ count = 0
+ for message in await channel.pins():
+ if await _pin_wrapper(message, pin=False):
+ count += 1
+
+ return count
def _match_bot_embed(message: t.Optional[discord.Message], description: str) -> bool:
@@ -261,30 +261,26 @@ def _match_bot_embed(message: t.Optional[discord.Message], description: str) ->
return message.author == bot.instance.user and bot_msg_desc.strip() == description.strip()
-async def pin_wrapper(msg_id: int, channel: discord.TextChannel, *, pin: bool) -> bool:
+async def _pin_wrapper(message: discord.Message, *, pin: bool) -> bool:
"""
- Pin message `msg_id` in `channel` if `pin` is True or unpin if it's False.
+ Pin `message` if `pin` is True or unpin if it's False.
Return True if successful and False otherwise.
"""
- channel_str = f"#{channel} ({channel.id})"
- if pin:
- func = bot.instance.http.pin_message
- verb = "pin"
- else:
- func = bot.instance.http.unpin_message
- verb = "unpin"
+ channel_str = f"#{message.channel} ({message.channel.id})"
+ func = message.pin if pin else message.unpin
try:
- await func(channel.id, msg_id)
+ await func()
except discord.HTTPException as e:
if e.code == 10008:
- log.debug(f"Message {msg_id} in {channel_str} doesn't exist; can't {verb}.")
+ log.debug(f"Message {message.id} in {channel_str} doesn't exist; can't {func.__name__}.")
else:
log.exception(
- f"Error {verb}ning message {msg_id} in {channel_str}: {e.status} ({e.code})"
+ f"Error {func.__name__}ning message {message.id} in {channel_str}: "
+ f"{e.status} ({e.code})"
)
return False
else:
- log.trace(f"{verb.capitalize()}ned message {msg_id} in {channel_str}.")
+ log.trace(f"{func.__name__.capitalize()}ned message {message.id} in {channel_str}.")
return True
diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py
index dece44063..cbc329a06 100644
--- a/bot/exts/info/doc/_cog.py
+++ b/bot/exts/info/doc/_cog.py
@@ -464,5 +464,4 @@ class DocCog(commands.Cog):
async def cog_unload(self) -> None:
"""Clear scheduled inventories, queued symbols and cleanup task on cog unload."""
self.inventory_scheduler.cancel_all()
- self.init_refresh_task.cancel()
await self.item_fetcher.clear()
diff --git a/bot/exts/info/doc/_html.py b/bot/exts/info/doc/_html.py
index ca0a0ac4a..c101ec250 100644
--- a/bot/exts/info/doc/_html.py
+++ b/bot/exts/info/doc/_html.py
@@ -129,6 +129,9 @@ def get_signatures(start_signature: PageElement) -> List[str]:
start_signature,
*_find_next_siblings_until_tag(start_signature, ("dd",), limit=2),
)[-MAX_SIGNATURE_AMOUNT:]:
+ for tag in element.find_all("a", class_="headerlink", recursive=False):
+ tag.decompose()
+
signature = _UNWANTED_SIGNATURE_SYMBOLS_RE.sub("", element.text)
if signature:
diff --git a/bot/exts/info/doc/_parsing.py b/bot/exts/info/doc/_parsing.py
index 6ab38eb3d..8ce9ea3a1 100644
--- a/bot/exts/info/doc/_parsing.py
+++ b/bot/exts/info/doc/_parsing.py
@@ -255,4 +255,5 @@ def get_symbol_markdown(soup: BeautifulSoup, symbol_data: DocItem) -> Optional[s
else:
signature = get_signatures(symbol_heading)
description = get_dd_description(symbol_heading)
- return _create_markdown(signature, description, symbol_data.url).replace("ΒΆ", "").strip()
+
+ return _create_markdown(signature, description, symbol_data.url).strip()
diff --git a/bot/exts/info/doc/_redis_cache.py b/bot/exts/info/doc/_redis_cache.py
index 107f2344f..8e08e7ae4 100644
--- a/bot/exts/info/doc/_redis_cache.py
+++ b/bot/exts/info/doc/_redis_cache.py
@@ -1,22 +1,28 @@
from __future__ import annotations
import datetime
+import fnmatch
+import time
from typing import Optional, TYPE_CHECKING
from async_rediscache.types.base import RedisObject, namespace_lock
+from bot.log import get_logger
+
if TYPE_CHECKING:
from ._cog import DocItem
WEEK_SECONDS = datetime.timedelta(weeks=1).total_seconds()
+log = get_logger(__name__)
+
class DocRedisCache(RedisObject):
"""Interface for redis functionality needed by the Doc cog."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self._set_expires = set()
+ self._set_expires = dict[str, float]()
@namespace_lock
async def set(self, item: DocItem, value: str) -> None:
@@ -29,16 +35,30 @@ class DocRedisCache(RedisObject):
needs_expire = False
with await self._get_pool_connection() as connection:
- if redis_key not in self._set_expires:
+ 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.
- # If this is the first time setting values for this key check if it exists and add it to
- # `_set_expires` to prevent redundant checks for subsequent uses with items from the same page.
- self._set_expires.add(redis_key)
- needs_expire = not await connection.exists(redis_key)
+ 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.
+ needs_expire = True
+ log.debug(f"Key `{redis_key}` expired in internal key cache.")
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.")
@namespace_lock
async def get(self, item: DocItem) -> Optional[str]:
@@ -49,12 +69,18 @@ class DocRedisCache(RedisObject):
@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=f"{self.namespace}:{package}:*")
+ 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
diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py
index e89ffafb1..5d7467caf 100644
--- a/bot/exts/info/tags.py
+++ b/bot/exts/info/tags.py
@@ -278,7 +278,11 @@ class Tags(Cog):
if tag is None and tag_identifier.group is not None:
# Try exact match with only the name
- tag = self.tags.get(TagIdentifier(None, tag_identifier.group))
+ name_only_identifier = TagIdentifier(None, tag_identifier.group)
+ tag = self.tags.get(name_only_identifier)
+ if tag:
+ # Ensure the correct tag information is sent to statsd
+ tag_identifier = name_only_identifier
if tag is None and len(filtered_tags) == 1:
tag_identifier = filtered_tags[0][0]
diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py
index 0f14f515e..39eff9757 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, User, errors
+from discord import Colour, Message, NotFound, TextChannel, Thread, User, errors
from discord.ext.commands import Cog, Context, Converter, Greedy, group, has_any_role
from discord.ext.commands.converter import TextChannelConverter
from discord.ext.commands.errors import BadArgument
@@ -130,8 +130,8 @@ class Clean(Cog):
else:
if channels == "*":
channels = {
- channel for channel in ctx.guild.channels
- if isinstance(channel, TextChannel)
+ channel for channel in ctx.guild.channels + ctx.guild.threads
+ if isinstance(channel, (TextChannel, Thread))
# Assume that non-public channels are not needed to optimize for speed.
and channel.permissions_for(ctx.guild.default_role).view_channel
}
@@ -443,7 +443,7 @@ class Clean(Cog):
if log_url and is_mod_channel(ctx.channel):
try:
await ctx.reply(success_message)
- except errors.NotFound:
+ except errors.HTTPException:
await ctx.send(success_message)
elif log_url:
if mods := self.bot.get_channel(Channels.mods):
@@ -486,34 +486,40 @@ class Clean(Cog):
await self._clean_messages(ctx, channels, bots_only, users, regex, first_limit, second_limit)
- @clean_group.command(name="user", aliases=["users"])
- async def clean_user(
+ @clean_group.command(name="users", aliases=["user"])
+ async def clean_users(
self,
ctx: Context,
- user: User,
+ users: Greedy[User],
message_or_time: CleanLimit,
*,
channels: CleanChannels = None
) -> None:
"""
- Delete messages posted by the provided user, stop cleaning after reaching `message_or_time`.
+ Delete messages posted by the provided users, stop cleaning after reaching `message_or_time`.
`message_or_time` can be either a message to stop at (exclusive), a timedelta for max message age, or an ISO
datetime.
- If a message is specified, `channels` cannot be specified.
+ If a message is specified the cleanup will be limited to the channel the message is in.
+
+ If a timedelta or an ISO datetime is specified, `channels` can be specified to clean across multiple channels.
+ An asterisk can also be used to designate cleanup across all channels.
"""
- await self._clean_messages(ctx, users=[user], channels=channels, first_limit=message_or_time)
+ await self._clean_messages(ctx, users=users, channels=channels, first_limit=message_or_time)
@clean_group.command(name="bots", aliases=["bot"])
async def clean_bots(self, ctx: Context, message_or_time: CleanLimit, *, channels: CleanChannels = None) -> None:
"""
- Delete all messages posted by a bot, stop cleaning after traversing `traverse` messages.
+ Delete all messages posted by a bot, stop cleaning after reaching `message_or_time`.
`message_or_time` can be either a message to stop at (exclusive), a timedelta for max message age, or an ISO
datetime.
- If a message is specified, `channels` cannot be specified.
+ If a message is specified the cleanup will be limited to the channel the message is in.
+
+ If a timedelta or an ISO datetime is specified, `channels` can be specified to clean across multiple channels.
+ An asterisk can also be used to designate cleanup across all channels.
"""
await self._clean_messages(ctx, bots_only=True, channels=channels, first_limit=message_or_time)
@@ -531,11 +537,19 @@ class Clean(Cog):
`message_or_time` can be either a message to stop at (exclusive), a timedelta for max message age, or an ISO
datetime.
- If a message is specified, `channels` cannot be specified.
- The pattern must be provided enclosed in backticks.
- If the pattern contains spaces, it still needs to be enclosed in double quotes on top of that.
- For example: `[0-9]`
+ If a message is specified the cleanup will be limited to the channel the message is in.
+
+ If a timedelta or an ISO datetime is specified, `channels` can be specified to clean across multiple channels.
+ An asterisk can also be used to designate cleanup across all channels.
+
+ The `regex` pattern must be provided enclosed in backticks.
+
+ For example: \\`[0-9]\\`.
+
+ If the `regex` pattern contains spaces, it still needs to be enclosed in double quotes on top of that.
+
+ For example: "\\`[0-9]\\`".
"""
await self._clean_messages(ctx, regex=regex, channels=channels, first_limit=message_or_time)
@@ -550,7 +564,11 @@ class Clean(Cog):
Delete all messages until a certain limit.
A limit can be either a message, and ISO date-time string, or a time delta.
- If a message is specified, `channel` cannot be specified.
+
+ If a message is specified the cleanup will be limited to the channel the message is in.
+
+ If a timedelta or an ISO datetime is specified, `channels` can be specified to clean across multiple channels.
+ An asterisk can also be used to designate cleanup across all channels.
"""
await self._clean_messages(
ctx,
@@ -573,7 +591,10 @@ class Clean(Cog):
A limit can be either a message, and ISO date-time string, or a time delta.
If two messages are specified, they both must be in the same channel.
- If a message is specified, `channel` cannot be specified.
+ The cleanup will be limited to the channel the messages are in.
+
+ If two timedeltas or ISO datetimes are specified, `channels` can be specified to clean across multiple channels.
+ An asterisk can also be used to designate cleanup across all channels.
"""
await self._clean_messages(
ctx,
diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py
index 80f68e442..67991730e 100644
--- a/bot/exts/moderation/modlog.py
+++ b/bot/exts/moderation/modlog.py
@@ -6,12 +6,14 @@ from datetime import datetime, timezone
from itertools import zip_longest
import discord
+from botcore.site_api import ResponseCodeError
from dateutil.relativedelta import relativedelta
from deepdiff import DeepDiff
from discord import Colour, Message, Thread
from discord.abc import GuildChannel
from discord.ext.commands import Cog, Context
from discord.utils import escape_markdown, format_dt, snowflake_time
+from sentry_sdk import add_breadcrumb
from bot.bot import Bot
from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs
@@ -53,24 +55,35 @@ class ModLog(Cog, name="ModLog"):
if attachments is None:
attachments = []
- response = await self.bot.api_client.post(
- 'bot/deleted-messages',
- json={
- 'actor': actor_id,
- 'creation': datetime.now(timezone.utc).isoformat(),
- 'deletedmessage_set': [
- {
- 'id': message.id,
- 'author': message.author.id,
- 'channel_id': message.channel.id,
- 'content': message.content.replace("\0", ""), # Null chars cause 400.
- 'embeds': [embed.to_dict() for embed in message.embeds],
- 'attachments': attachment,
- }
- for message, attachment in zip_longest(messages, attachments, fillvalue=[])
- ]
+ deletedmessage_set = [
+ {
+ "id": message.id,
+ "author": message.author.id,
+ "channel_id": message.channel.id,
+ "content": message.content.replace("\0", ""), # Null chars cause 400.
+ "embeds": [embed.to_dict() for embed in message.embeds],
+ "attachments": attachment,
}
- )
+ for message, attachment in zip_longest(messages, attachments, fillvalue=[])
+ ]
+
+ try:
+ response = await self.bot.api_client.post(
+ "bot/deleted-messages",
+ json={
+ "actor": actor_id,
+ "creation": datetime.now(timezone.utc).isoformat(),
+ "deletedmessage_set": deletedmessage_set,
+ }
+ )
+ except ResponseCodeError as e:
+ add_breadcrumb(
+ category="api_error",
+ message=str(e),
+ level="error",
+ data=deletedmessage_set,
+ )
+ raise
return f"{URLs.site_logs_view}/{response['id']}"
diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py
index 743919d4e..a2f208484 100644
--- a/bot/exts/utils/thread_bumper.py
+++ b/bot/exts/utils/thread_bumper.py
@@ -1,7 +1,6 @@
import typing as t
import discord
-from async_rediscache import RedisCache
from botcore.site_api import ResponseCodeError
from discord.ext import commands
@@ -18,12 +17,26 @@ THREAD_BUMP_ENDPOINT = "bot/bumped-threads"
class ThreadBumper(commands.Cog):
"""Cog that allow users to add the current thread to a list that get reopened on archive."""
- # RedisCache[discord.Thread.id, "sentinel"]
- threads_to_bump = RedisCache()
-
def __init__(self, bot: Bot):
self.bot = bot
+ async def thread_exists_in_site(self, thread_id: int) -> bool:
+ """Return whether the given thread_id exists in the site api's bump list."""
+ # If the thread exists, site returns a 204 with no content.
+ # Due to this, `api_client.request()` cannot be used, as it always attempts to decode the response as json.
+ # Instead, call the site manually using the api_client's session, to use the auth token logic in the wrapper.
+
+ async with self.bot.api_client.session.get(
+ f"{self.bot.api_client._url_for(THREAD_BUMP_ENDPOINT)}/{thread_id}"
+ ) as response:
+ if response.status == 204:
+ return True
+ elif response.status == 404:
+ return False
+ else:
+ # A status other than 204/404 is undefined behaviour from site. Raise error for investigation.
+ raise ResponseCodeError(response, response.text())
+
async def unarchive_threads_not_manually_archived(self, threads: list[discord.Thread]) -> None:
"""
Iterate through and unarchive any threads that weren't manually archived recently.
@@ -89,12 +102,7 @@ class ThreadBumper(commands.Cog):
else:
raise commands.BadArgument("You must provide a thread, or run this command within a thread.")
- try:
- await self.bot.api_client.get(f"{THREAD_BUMP_ENDPOINT}/{thread.id}")
- except ResponseCodeError as e:
- if e.status != 404:
- raise
- else:
+ if await self.thread_exists_in_site(thread.id):
raise commands.BadArgument("This thread is already in the bump list.")
await self.bot.api_client.post(THREAD_BUMP_ENDPOINT, data={"thread_id": thread.id})
@@ -109,9 +117,7 @@ class ThreadBumper(commands.Cog):
else:
raise commands.BadArgument("You must provide a thread, or run this command within a thread.")
- try:
- await self.bot.api_client.get(f"{THREAD_BUMP_ENDPOINT}/{thread.id}")
- except ResponseCodeError:
+ if not await self.thread_exists_in_site(thread.id):
raise commands.BadArgument("This thread is not in the bump list.")
await self.bot.api_client.delete(f"{THREAD_BUMP_ENDPOINT}/{thread.id}")
@@ -137,12 +143,7 @@ class ThreadBumper(commands.Cog):
if not after.archived:
return
- try:
- await self.bot.api_client.get(f"{THREAD_BUMP_ENDPOINT}/{after.id}")
- except ResponseCodeError as e:
- if e.status != 404:
- raise
- else:
+ if await self.thread_exists_in_site(after.id):
await self.unarchive_threads_not_manually_archived([after])
async def cog_check(self, ctx: commands.Context) -> bool:
diff --git a/poetry.lock b/poetry.lock
index 8281394e6..7e74cecdd 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -141,14 +141,14 @@ async-rediscache = ["async-rediscache[fakeredis] (==0.2.0)"]
[package.source]
type = "url"
-url = "https://github.com/python-discord/bot-core/archive/refs/tags/v6.4.0.zip"
+url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.0.0.zip"
[[package]]
name = "certifi"
-version = "2021.10.8"
+version = "2022.5.18.1"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.6"
[[package]]
name = "cffi"
@@ -297,16 +297,16 @@ testing = ["pre-commit"]
[[package]]
name = "fakeredis"
-version = "1.7.1"
+version = "1.7.5"
description = "Fake implementation of redis API for testing purposes."
category = "main"
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.7"
[package.dependencies]
lupa = {version = "*", optional = true, markers = "extra == \"lua\""}
packaging = "*"
-redis = "<4.2.0"
+redis = "<=4.3.1"
six = ">=1.12"
sortedcontainers = "*"
@@ -327,7 +327,7 @@ sgmllib3k = "*"
[[package]]
name = "filelock"
-version = "3.6.0"
+version = "3.7.0"
description = "A platform independent file lock."
category = "main"
optional = false
@@ -478,7 +478,7 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve
[[package]]
name = "identify"
-version = "2.4.12"
+version = "2.5.1"
description = "File identification library for Python"
category = "dev"
optional = false
@@ -549,15 +549,15 @@ source = ["Cython (>=0.29.7)"]
[[package]]
name = "markdownify"
-version = "0.10.3"
+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"
@@ -686,14 +686,14 @@ virtualenv = ">=20.0.8"
[[package]]
name = "psutil"
-version = "5.9.0"
+version = "5.9.1"
description = "Cross-platform lib for process and system monitoring in Python."
category = "dev"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
-test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
+test = ["ipaddress", "mock", "enum34", "pywin32", "wmi"]
[[package]]
name = "ptable"
@@ -765,7 +765,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyparsing"
-version = "3.0.8"
+version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "main"
optional = false
@@ -909,13 +909,14 @@ full = ["numpy"]
[[package]]
name = "redis"
-version = "4.1.4"
+version = "4.3.1"
description = "Python client for Redis database and key-value store"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
+async-timeout = ">=4.0.2"
deprecated = ">=1.2.3"
packaging = ">=20.4"
@@ -1129,7 +1130,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)",
[[package]]
name = "wrapt"
-version = "1.14.0"
+version = "1.14.1"
description = "Module for decorators, wrappers and monkey patching."
category = "main"
optional = false
@@ -1150,7 +1151,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "3.9.*"
-content-hash = "a07f619c75f8133982984eb506ad350144829f10c704421f09b3dbe72cd037d8"
+content-hash = "953529931f133865df736f9a6f96f59c64336963ef9e6ce6c959e6bd8c73792c"
[metadata.files]
aiodns = [
@@ -1265,8 +1266,8 @@ beautifulsoup4 = [
]
bot-core = []
certifi = [
- {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
- {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
+ {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"},
+ {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"},
]
cffi = [
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
@@ -1400,16 +1401,16 @@ execnet = [
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
]
fakeredis = [
- {file = "fakeredis-1.7.1-py3-none-any.whl", hash = "sha256:be3668e50f6b57d5fc4abfd27f9f655bed07a2c5aecfc8b15d0aad59f997c1ba"},
- {file = "fakeredis-1.7.1.tar.gz", hash = "sha256:7c2c4ba1b42e0a75337c54b777bf0671056b4569650e3ff927e4b9b385afc8ec"},
+ {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.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"},
- {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"},
+ {file = "filelock-3.7.0-py3-none-any.whl", hash = "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6"},
+ {file = "filelock-3.7.0.tar.gz", hash = "sha256:b795f1b42a61bbf8ec7113c341dad679d772567b936fbd1bf43c9a238e673e20"},
]
flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
@@ -1555,8 +1556,8 @@ humanfriendly = [
{file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"},
]
identify = [
- {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"},
- {file = "identify-2.4.12.tar.gz", hash = "sha256:3f3244a559290e7d3deb9e9adc7b33594c1bc85a9dd82e0f1be519bf12a1ec17"},
+ {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"},
@@ -1784,8 +1785,8 @@ lxml = [
{file = "lxml-4.8.0.tar.gz", hash = "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23"},
]
markdownify = [
- {file = "markdownify-0.10.3-py3-none-any.whl", hash = "sha256:edad0ad3896ec7460d05537ad804bbb3614877c6cd0df27b56dee218236d9ce2"},
- {file = "markdownify-0.10.3.tar.gz", hash = "sha256:782e310390cd5e4bde7543ceb644598c78b9824ee9f8d7ef9f9f4f8782e46974"},
+ {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"},
@@ -1892,38 +1893,38 @@ pre-commit = [
{file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
]
psutil = [
- {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"},
- {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618"},
- {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2"},
- {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd"},
- {file = "psutil-5.9.0-cp27-none-win32.whl", hash = "sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3"},
- {file = "psutil-5.9.0-cp27-none-win_amd64.whl", hash = "sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c"},
- {file = "psutil-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492"},
- {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3"},
- {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2"},
- {file = "psutil-5.9.0-cp310-cp310-win32.whl", hash = "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d"},
- {file = "psutil-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b"},
- {file = "psutil-5.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56"},
- {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203"},
- {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d"},
- {file = "psutil-5.9.0-cp36-cp36m-win32.whl", hash = "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64"},
- {file = "psutil-5.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94"},
- {file = "psutil-5.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0"},
- {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce"},
- {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5"},
- {file = "psutil-5.9.0-cp37-cp37m-win32.whl", hash = "sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9"},
- {file = "psutil-5.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4"},
- {file = "psutil-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2"},
- {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d"},
- {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a"},
- {file = "psutil-5.9.0-cp38-cp38-win32.whl", hash = "sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666"},
- {file = "psutil-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841"},
- {file = "psutil-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf"},
- {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07"},
- {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d"},
- {file = "psutil-5.9.0-cp39-cp39-win32.whl", hash = "sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845"},
- {file = "psutil-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3"},
- {file = "psutil-5.9.0.tar.gz", hash = "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25"},
+ {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"},
@@ -1982,8 +1983,8 @@ pyflakes = [
{file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
]
pyparsing = [
- {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"},
- {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"},
+ {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"},
@@ -2098,8 +2099,8 @@ rapidfuzz = [
{file = "rapidfuzz-2.0.7.tar.gz", hash = "sha256:93bf42784fd74ebf1a8e89ca1596e9bea7f3ac4a61b825ecc6eb2d9893ad6844"},
]
redis = [
- {file = "redis-4.1.4-py3-none-any.whl", hash = "sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a"},
- {file = "redis-4.1.4.tar.gz", hash = "sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306"},
+ {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"},
@@ -2241,70 +2242,70 @@ virtualenv = [
{file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
]
wrapt = [
- {file = "wrapt-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7"},
- {file = "wrapt-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c"},
- {file = "wrapt-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb"},
- {file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd"},
- {file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291"},
- {file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33"},
- {file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6"},
- {file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b"},
- {file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5"},
- {file = "wrapt-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330"},
- {file = "wrapt-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c"},
- {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561"},
- {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa"},
- {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a"},
- {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131"},
- {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8"},
- {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763"},
- {file = "wrapt-1.14.0-cp310-cp310-win32.whl", hash = "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff"},
- {file = "wrapt-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d"},
- {file = "wrapt-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627"},
- {file = "wrapt-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775"},
- {file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23"},
- {file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3"},
- {file = "wrapt-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0"},
- {file = "wrapt-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425"},
- {file = "wrapt-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48"},
- {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb"},
- {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e"},
- {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3"},
- {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8"},
- {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd"},
- {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036"},
- {file = "wrapt-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8"},
- {file = "wrapt-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06"},
- {file = "wrapt-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4"},
- {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80"},
- {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce"},
- {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279"},
- {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653"},
- {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0"},
- {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9"},
- {file = "wrapt-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68"},
- {file = "wrapt-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3"},
- {file = "wrapt-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d"},
- {file = "wrapt-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38"},
- {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7"},
- {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1"},
- {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8"},
- {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd"},
- {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe"},
- {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0"},
- {file = "wrapt-1.14.0-cp38-cp38-win32.whl", hash = "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f"},
- {file = "wrapt-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e"},
- {file = "wrapt-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1"},
- {file = "wrapt-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4"},
- {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758"},
- {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d"},
- {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b"},
- {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6"},
- {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0"},
- {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c"},
- {file = "wrapt-1.14.0-cp39-cp39-win32.whl", hash = "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350"},
- {file = "wrapt-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc"},
- {file = "wrapt-1.14.0.tar.gz", hash = "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311"},
+ {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"},
diff --git a/pyproject.toml b/pyproject.toml
index 402d05f70..2d6adb9c5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ python = "3.9.*"
"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/5a06fa5f3e28d2b7191722e1a84c541560008aea.zip"}
# See https://bot-core.pythondiscord.com/ for docs.
-bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v6.4.0.zip", extras = ["async-rediscache"]}
+bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v7.0.0.zip", extras = ["async-rediscache"]}
aiodns = "3.0.0"
aiohttp = "3.8.1"
@@ -25,7 +25,7 @@ emoji = "1.7.0"
feedparser = "6.0.8"
rapidfuzz = "2.0.7"
lxml = "4.8.0"
-markdownify = "0.10.3"
+markdownify = "0.6.1"
more_itertools = "8.12.0"
python-dateutil = "2.8.2"
python-frontmatter = "1.0.0"
diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py
index 193f1d822..0a58126e7 100644
--- a/tests/bot/exts/backend/test_error_handler.py
+++ b/tests/bot/exts/backend/test_error_handler.py
@@ -48,6 +48,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase):
cog = ErrorHandler(self.bot)
cog.try_silence = AsyncMock()
cog.try_get_tag = AsyncMock()
+ 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"]):
@@ -76,6 +77,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase):
cog = ErrorHandler(self.bot)
cog.try_silence = AsyncMock()
cog.try_get_tag = AsyncMock()
+ cog.try_run_eval = AsyncMock()
error = errors.CommandNotFound()
@@ -83,6 +85,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase):
cog.try_silence.assert_not_awaited()
cog.try_get_tag.assert_not_awaited()
+ cog.try_run_eval.assert_not_awaited()
self.ctx.send.assert_not_awaited()
async def test_error_handler_user_input_error(self):
@@ -477,11 +480,11 @@ class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase):
@patch("bot.exts.backend.error_handler.log")
async def test_handle_api_error(self, log_mock):
- """Should `ctx.send` on HTTP error codes, `log.debug|warning` depends on code."""
+ """Should `ctx.send` on HTTP error codes, and log at correct level."""
test_cases = (
{
"error": ResponseCodeError(AsyncMock(status=400)),
- "log_level": "debug"
+ "log_level": "error"
},
{
"error": ResponseCodeError(AsyncMock(status=404)),
@@ -505,6 +508,8 @@ class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase):
self.ctx.send.assert_awaited_once()
if case["log_level"] == "warning":
log_mock.warning.assert_called_once()
+ elif case["log_level"] == "error":
+ log_mock.error.assert_called_once()
else:
log_mock.debug.assert_called_once()